nnhomescreen Framework Module

Handles downloading the specifications for the homescreen and applying them to the layout, including the contained banners and buttons.

ModularHomeScreenFragment

The container for all of the homescreen content. Uses custom RecyclerView.Adapter (ModularHomeScreenAdapter) to display elements, which have their own custom Views based on purpose (banner, image, Instagram etc – see views folder).

NNBannerFragment

Fragment that contains a ViewPager to display a banner. Has associated classes for downloading and parsing the JSONs that define the banners.

nnviews Framework Module

Various common UI elements.

NNStartupActivity

Abstract class to be implemented in app startup Activity. Defines tasks to be run before showing the first screen e.g. downloading settings, version check.

imaging folder

Various classes for handling images:

  • NNCircleImageView – Generate circular ImageView. Extends NNImageView
  • NNImageCarousel – Create image carousel from a list of image urls
  • NNImageView – includes default imageView behaviour as well as a custom builder for parameterised calls. Uses NNImageHandler.
  • NNPagerAnimation – Defines animation for ViewPagers. Includes NNPagerIndicators to show pager location.

layout folder

  • NNScrollView – adds custom behaviour to the standard ScrollView
  • SelectorFrameLayout – where a FrameLayout contains children it will update the selector no matter what the children do with the touch events
  • SelectorRelativeLayout – same as above but using RelativeLayout
  • WrapContentViewPager – Used in banner fragments to allow the viewpager to wrap its height around the content.

ui folder

Various classes mostly associated with applying custom fonts to UI elements. Also NNCarousel, an extension of ViewPager with view recycling and on request loading.

nnsettings Framework Module

Framework to acquire and implement settings from CMS.

NNSettings/SettingsManager/SettingsORM etc.

Acquires settings from the settings database, which is stored locally after being periodically downloaded from CMS. Used very frequently.

EnvironmentsActivity/EnvironmentManager/Environments

Sets the environment – QA, production etc.

UnpackAssets

Acquires a JSON list of assets and saves them in the device storage, if run mode is FRESH_INSTALL

NNVersionCheck/NNVersionCheckInterface/VersionCheckPOJO

Acquires a JSON list of app versions, compares it to the local version and, if not up to date, forces a restart with an update prompt. Not implemented in all apps, based on client preference.

nnbase Framework Module

This is the root framework and is a dependency of several of the others. It is essential for every app.

NNApplication

This file, or a subclass of it, should be declared in the AndroidManifest.xml as:

<application
android:name=".NNApplication"
... >
...
</application>

This means that the entire app will be based on the NNApplication class. This provides core functionality such as:

  • getContext() – passes application context into any class
  • getInstance() – passes application into any class
  • Instantiates Foreground which provides Lifecycle tracking
  • Sets up NNActionManager (Singleton) which processes actions, and can restart and kill the app
  • Checks the package version number of the app against the prefs version number and consequently:
  • Keeps a record of the run mode: FRESH_INSTALL, UPDATE or RUN
  • Checks for changes in the Connectivity status of the device e.g. lost internet connection using a Broadcast receiver

Identity

  • Determines the platform – Amazon, Blackberry or Google
  • Finds the device ID
  • Finds the Android ID

Foreground

Tracks the Lifecycle of the application.

NNActionManager/NNActionManagerInterface

Enum Singleton – holds a delegate which is the app specific implementation of the Action Manager. Has functions to restart and kill the app. Most commonly used to process actions. Allows the ability to go to a specified page in the app, based on a specific string e.g. NN4MSF::GOTO_PRODUCT::123-45678-123

  • Always begins with NN4M followed by rcode (2-letter client code)
  • parts of an action are always split on double colon ::
  • the second part decides where to go
  • the third part onwards is the data to pass to that page

ContentFilterManager/FilterableContent

Classes for taking JSON response items and filtering them.

DataValidator

Determines validity of String inputs against patterns such as HTML, JSON, Email address.

DiskManager

Handles common file input and output.

PermissionHelper/PermissionCallback

Handles Permission requests

NNTrackingEvent

Tracks events!

utilities folder

Various helpers for general usage. Including:

  • CollectionUtils for checking whether a Java collection is empty
  • ExceptionLogging – logs exceptions using Fabric/Crashlytics
  • NNBitmapHandler – for decoding Bitmaps
  • NNUtils – random collection of other helpers
  • Prefs – for dealing with the default shared preferences
  • Security – encryption/decryption
  • UnzipUtility – unzip files

Lock orientation of the screen for your app

In AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.android.myapp">

    <application
        android:allowBackup="true"
        tools:ignore="GoogleAppIndexingWarning">

        <!-- Screen locked to landscape for easier play -->
        <!-- configChanges attribute makes the following actions NOT cause a config change  -->
        <!-- screenOrientation attribute sets the default animation-->
        <activity android:name=".MainActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:screenOrientation="landscape">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

Create Fragments

Add fragments to your app.

Create a new class for our Fragment:

public class AppFragment extends Fragment {

    public AppFragment() { // Empty constructor
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // Essential
        View rootView = inflater.inflate(R.layout.our_fragment, container, false); // Use a custom layout file (see below)

        ImageView imageView = rootView.findViewById(R.id.ourImageView); // Here we find a View within the layout

        imageView.setImageResource(R.drawable.anImage); // And allocate an image to it

        return rootView;
    }
}

Our layout file for the Fragment, our_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/ourImageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="An Image"
        tools:srcCompat="@drawable/anImage" /> // Placeholder image
</LinearLayout>

Create an instance of our Fragment in the Activity and use FragmentManager to add it to the layout for the Activity:

        AppFragment aFragment = new AppFragment(); // New instance of our custom Fragment

        FragmentManager fragmentManager = getSupportFragmentManager();

        fragmentManager.beginTransaction()
                .add(R.id.fragment_container, aFragment) // Adds the new fragment to an element of the Activity layout (see below)
                .commit();

Our layout file for the Activity, activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.android.app.MainActivity">

    <LinearLayout
        android:id="@+id/main_activity_linear_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <FrameLayout android:id="@+id/fragment_container" // View that will contain the Fragment
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:scaleType="centerInside"/>

    </LinearLayout>
</ScrollView>

Customising RecyclerViews according to position +/- orientation

If wanting to set differing layouts for a RecyclerView in a particular ViewHolder position +/- orientation (portrait or landscape), you can do the following:

Use the values, values-port and values-land folders to set booleans (or other) to indicate your preferences for layout according to orientation e.g. in values-port\bools.xml add

<bool name="use_expanded_layout">false</bool>

to state that you do not want to use your expanded layout in portrait mode (if desired).
Overriding getViewType will give you the position of the ViewHolder by which you can conditionally set which viewType to use. In your RecyclerView.Adapter file:

private final int CONDENSED_VIEWTYPE_ID = 83; // Randomly generated unique ID to correspond to condensed layout
private final int EXPANDED_VIEWTYPE_ID = 928; // Randomly generated unique ID to correspond to expanded layout
private boolean mUseExpandedLayout; // Declare a variable to determine layout preference
private final Context mContext; // We'll need this

    public RecyclerViewAdapter(@NonNull Context context, RecyclerViewAdapterOnClickHandler clickHandler) { // In the constructor...
        ...
        mContext = context;
        mUseExpandedLayout= context.getResources().getBoolean(R.bool.use_expanded_layout); // ... get the preference from your boolean. If in portrait orientation it will take this value from the values-port\bools.xml file if it exists, otherwise the values\bools.xml file
    }

    @Override
    public int getItemViewType(int position) { // Override getViewType, which gives you the ViewHolder position
        if (!mExpandedLayout || position != 0) { // Use this position to determine which viewType to apply. Here, if the boolean is false or position is not 0...
            return CONDENSED_VIEWTYPE_ID; // ...use the condensed viewType
        }
        return EXPANDED_VIEWTYPE_ID; // Otherwise use the expanded viewType
    }

    @Override
    public RecyclerViewAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { // In the onCreateViewHolder override...

        View view;

        if (viewType == CONDENSED_VIEWTYPE_ID) { // ...if the viewType has the condensed ID...
            view = LayoutInflater
                    .from(mContext)
                    .inflate(R.layout.condensed_list_item, viewGroup, false); // ...apply the condensed layout
        } else if (viewType == EXPANDED_VIEWTYPE_ID) { // If the viewType has the expanded ID...
            view = LayoutInflater
                    .from(mContext)
                    .inflate(R.layout.expanded_list_item, viewGroup, false); // ...apply the expanded layout
        } else {
            throw new IllegalArgumentException("Invalid layout type");
        }

        return new RecyclerViewAdapterViewHolder(view); // Creates the ViewHolder with the appropriate layout attached
    }

Data Binding Library

Links the UI to actual data without having to use findViewById for every item.

In build.gradle, add 'dataBinding.enabled = true':

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion "28.0.3"
    ...
    dataBinding.enabled = true;
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
}

Create a plain old Java object to act as a placeholder for the data:

/**
 * BoardingPassInfo is a simple POJO that contains information about, you guessed it, a boarding
 * pass! Normally, it is best practice in Java to declare member variables as private and provide
 * getters, but we are leaving these fields public for ease of use.
 */
public class BoardingPassInfo {

    public String passengerName;
    public String flightCode;
    public String originCode;
    public String destCode;

    public Timestamp boardingTime;
    public Timestamp departureTime;
    public Timestamp arrivalTime;

    public String departureTerminal;
    public String departureGate;
    public String seatNumber;

    public int barCodeImageResource;

    public long getMinutesUntilBoarding() {
        long millisUntilBoarding = boardingTime.getTime() - System.currentTimeMillis();
        return TimeUnit.MILLISECONDS.toMinutes(millisUntilBoarding);
    }
}

Add layout as the root tag to the UI (in this case, activity_main.xml file). Android will automatically create a Binding class for any layout with these tags:

<?xml version="1.0" encoding="utf-8"?>
<layout>
<ScrollView xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.constraint.ConstraintLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:text="@string/passenger_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
            
            ...

    </android.support.constraint.ConstraintLayout>
</ScrollView>
</layout>

In your Activity, create a Binding instance, and point the instance to the correct content view using DatabindingUtil:

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding mBinding; // ActivityMainBinding is automatically generated based on the activity_main.xml file name

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); // Passes in the Activity and sets the content view to the correct layout file
        BoardingPassInfo boardingPass = generateBoardingPassInfo(); // Whatever method to produce an instance of your placeholder class populated with data
        displayBoardingPassInfo(boardingPass); // Passes the placeholder instance to the method below
    }

    private void displayBoardingPassInfo(BoardingPassInfo info) {

        mBinding.textViewPassengerName.setText(info.passengerName);
        mBinding.textViewFlightCode.setText(info.flightCode);
        mBinding.textViewOriginAirport.setText(info.originCode);
        mBinding.textViewDestinationAirport.setText(info.destCode);
        mBinding.textViewTerminal.setText(info.departureTerminal);
        mBinding.textViewGate.setText(info.departureGate);
        mBinding.textViewSeat.setText(info.seatNumber);

        SimpleDateFormat formatter = new SimpleDateFormat("hh:mm a", Locale.getDefault());
        String boardingTime = formatter.format(info.boardingTime);
        mBinding.textViewBoardingTime.setText(boardingTime);
        String departureTime = formatter.format(info.departureTime);
        mBinding.textViewDepartureTime.setText(departureTime);
        String arrivalTime = formatter.format(info.arrivalTime);
        mBinding.textViewArrivalTime.setText(arrivalTime);

        long totalMinutesUntilBoarding = info.getMinutesUntilBoarding();
        long hoursUntilBoarding = TimeUnit.MINUTES.toHours(totalMinutesUntilBoarding);
        long minutesMinusHoursUntilBoarding = totalMinutesUntilBoarding - (hoursUntilBoarding*60);
        String hoursAndMinutesUntilBoarding = hoursUntilBoarding + ":" + minutesMinusHoursUntilBoarding;
        mBinding.textViewBoardingInCountdown.setText(hoursAndMinutesUntilBoarding);
    }
}

ud851-Exercises-student\Lesson11-Completeing-The-UI\T11.02

Sticky Intents & acquiring System Service status

As mentioned in the Broadcast Receiver post, updates will only be applied when the app is visible (between onResume and onPause). Therefore system changes which occur outside this window could leave the app with outdated information on resume. Therefore we should also check at onResume what the current system status is.

Depending on the version of Android the device is running you may be able to acquire a status directly from e.g. BatteryManager:

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // If using a modern enough version of Android...
            BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE); // ...ask the appropriate service for a status check
            doSomething(batteryManager.isCharging()); // Returns a boolean you can feed back into your own methods
        } else // Otherwise use sticky intents (see below)

Certain system broadcast intents do not disappear the moment they have fired and are processed. Instead they remain available to the system until superceded by a more up-to-date intent. These are known as sticky intents, and are the way of determining system status in older Android versions:

   {
            IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); // Create a new IntentFilter for the appropriate sticky Intent
            Intent chargingStatus = registerReceiver(null, intentFilter); // The same code you would use to register a Broadcast Receiver, but leave the first parameter null
            int batteryChargingStatus = chargingStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); // Get the info you want from the sticky Intent, with a default value
            boolean isCharging = batteryChargingStatus == BatteryManager.BATTERY_STATUS_CHARGING || batteryChargingStatus == BatteryManager.BATTERY_STATUS_FULL; // This will return a boolean of true if the phone is charging or fully charged
            doSomething(isCharging); // Feed the boolean into your own methods
   }

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.06

Broadcast Receiver

Listens out for designated System Broadcast Intents via an IntentFilter (declared in the AndroidManifest.xml) so that your app can respond to changes in the system. It can be triggered even when the app is not running. There are two types:

  • Static (Manifest-declared) – triggered whenever the broadcast intent occurs, even if the app is offline
  • Dynamic (Context-registered) – tied to the app’s lifecycle

It is preferred that dynamic broadcast receivers or job scheduling is used over static broadcast receivers, as abuse of statics could result in multiple apps responding to a system event. For this reason, some broadcast intents will not let you create a corresponding static receiver. These intents have FLAG_RECEIVER_REGISTERED_ONLY flag set.

For a static broadcast receiver you will register the receiver in the AndroidManifest.xml:

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
</receiver>

Then implement the receiver like so:

public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        @Override
        public void onReceive(Context context, Intent intent) {
            // Do something
        }
    }

For a dynamic receiver you register and un-register the receiver in the onResume() and onPause method overrides respectively:

public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        BroadcastReceiver br = new MyBroadcastReceiver();
        @Override
        public void onReceive(Context context, Intent intent) {
            //Do something
        }
    
    @Override
    protected void onResume() {
    super.onResume();
    IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
    filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    this.registerReceiver(br, filter);
    @Override
    protected void onPause() {
    super.onPause();
    this.unRegisterReceiver(br);
    }
}

The potential issue with the above example of a Dynamic receiver is that it will only check for changes once onResume has initiated, and so if the system status changes while the app is not visible, it may hold the incorrect status when the app starts. Adding the code to the onCreate and onDestroy methods would result in unnecessary actions occurring in the background when the app is not visible. To overcome this, check for the current system status on onResume in addition to registering the broadcast receiver. See an example here.

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.05

FirebaseJobDispatcher

The Firebase alternative to JobScheduler – is able to schedule tasks with complex conditions and execute them when those conditions are met. Provides compatibility all the way back to API 9 (Gingerbread). Requires Google Play Services to be installed on the target phone as it depends on GooglePlayDriver.

Add dependencies to build.gradle:

implementation 'com.firebase:firebase-jobdispatcher:0.8.5'

Create a JobService class and add the 2 obligatory overrides:

public class MyJobService extends JobService {
    @Override
    public boolean onStartJob(JobParameters job) {
        // Do some work here
        return false; // Answers the question: "Is there still work going on?"
    }

    @Override
    public boolean onStopJob(JobParameters job) { // This method is called if the system has determined that you must stop execution of your job even before you've had a chance to call jobFinished
        return false; // Answers the question: "Should this job be retried?"
    }
}

As the JobService runs on the main thread, potentially slow processes should be handed off to an AsyncTask. So alternatively:

public class MyJobService extends JobService {
    private AsyncTask mNetworkTask;
    @Override
    public boolean onStartJob(final JobParameters job) {
        mNetworkTask = new AsyncTask() {
            @Override
            protected Object doInBackground(Object[] objects) {
                // Do something
                return null;
            }

            @Override
            protected void onPostExecute(Object o) { // When your AsyncTask has finished you must inform the JobService
                jobFinished(job, false); // Second parameter tells the Jobservice whether the task needs to be rescheduled (depends on RetryStrategy, set in FirebaseJobDispatcher code below)
            }
        };
        mNetworkTask.execute();
        return true; // There is still work going on
    }

    @Override
    public boolean onStopJob(JobParameters job) { // This method is called if the system has determined that you must stop execution of your job even before you've had a chance to call jobFinished
        return false; // Answers the question: "Should this job be retried?"
    }
}

Add the service to the AndroidManifest.xml:

<service
    android:exported="false"
    android:name=".MyJobService">
    <intent-filter>
        <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
    </intent-filter>
</service>

The actual code:

FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); // Create a new dispatcher using the Google Play driver
Bundle myExtrasBundle = new Bundle();
myExtrasBundle.putString("some_key", "some_value");

Job myJob = dispatcher.newJobBuilder()
    .setService(MyJobService.class) // the JobService that will be called - ESSENTIAL
    .setTag("my-unique-tag") // uniquely identifies the job - ESSENTIAL
    .setRecurring(false) // one-off job
    .setLifetime(Lifetime.UNTIL_NEXT_BOOT) // don't persist past a device reboot
    .setTrigger(Trigger.executionWindow(0, 60)) // start between 0 and 60 seconds from now
    .setReplaceCurrent(false) // don't overwrite an existing job with the same tag
    .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) // retry with exponential backoff
    .setConstraints( // constraints that need to be satisfied for the job to run
        Constraint.ON_UNMETERED_NETWORK, // only run on an unmetered network
        Constraint.DEVICE_CHARGING // only run when the device is charging
    )
    .setExtras(myExtrasBundle)
    .build();

dispatcher.mustSchedule(myJob); // Throws exception if fails (ScheduleFailedException)
OR
dispatcher.schedule(myJob); // Returns one of (int value) SCHEDULE_RESULT_SUCCESS, SCHEDULE_RESULT_UNKNOWN_ERROR, SCHEDULE_RESULT_NO_DRIVER_AVAILABLE, SCHEDULE_RESULT_UNSUPPORTED_TRIGGER, SCHEDULE_RESULT_BAD_SERVICE

To cancel a job:

dispatcher.cancel("my-unique-tag");

To cancel all jobs:

dispatcher.cancelAll();

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.04

JobScheduler

Beginning with Android Lollipop, JobScheduler is able to schedule tasks with complex conditions and execute them when those conditions are met. Consider FirebaseJobDispatcher instead for greater backwards compatibility.

JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); // Get a JobScheduler
JobInfo job = new JobInfo.Builder( // Build a new JobInfo object
  MY_BACKGROUND_JOB, new ComponentName(context, MyJobService.class)) // Point to your JobService class
    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // Only trigger when on an unmetered network e.g. Wifi
    .setRequiresCharging(true) // Only trigger when being charged
    .setBackOffCriteria(TWO_MINUTES,BACKOFF_POLICY_EXPONENTIAL) // After first fail, wait 2 minutes, and increase wait time exponentially with every subsequent fail
    .setMinimumLatency(FIFTEEN_MINUTES) // Minimum wait before triggering is 15 minutes
.build();

js.schedule(job) // Call the job using this code

Notification Actions

Since Android Jelly Bean it has been possible to add up to 3 action buttons at the bottom of your notification, each launching a different PendingIntent:

Building on the example given in the Notification post:

new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
                .setColor(ContextCompat.getColor(context, R.color.colorPrimary))
                .setSmallIcon(R.drawable.ic_app_smallIcon)
                .setLargeIcon(R.drawable.ic_app_largeIcon)
                ... // Code clipped for brevity
                .addAction(firstAction); // Add 1st action, taking a Notification.Action parameter (see method below)
                .addAction(secondAction); // Add 2nd action, taking a Notification.Action parameter
                .setAutoCancel(true); // Notification automatically disappears once clicked
        notificationManager.notify(NOTIFICATION_ID, builder.build()); // Trigger the notification
    }

Typical code to create the first Notification.Action method:

    private static final int IGNORE_NOTIFICATION_PENDING_INTENT_ID = 3418; // Constant declared
    public static NotificationCompat.Action firstAction(Context context) { // Method that returns a Notification.Action object
        Intent intent = new Intent(context, AppIntentService.class); // Sets the Intent to an IntentService class (see below)
        intent.setAction(AppTasks.ACTION_DISMISS_NOTIFICATION); // Sets the action to a predefined String in another class (see below)
        PendingIntent pendingIntent = PendingIntent.getService(context, IGNORE_NOTIFICATION_PENDING_INTENT_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT); // IGNORE_NOTIFICATION_PENDING_INTENT_ID will be a unique ID. Flag set to CANCEL
        NotificationCompat.Action action = new NotificationCompat.Action(R.drawable.ic_app_notification, "Ignore", pendingIntent); // Sets an icon, label and PendingIntent
        return action;
    }

IntentService class:

public class AppIntentService extends IntentService {

    public AppIntentService() {
        super("AppIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        String action = intent.getAction(); // Gets the String from the .setAction in the Notification.Action method
        AppTasks.executeTask(this, action); // Calls a method from another class (see below)
    }
}

Additional class:

public class AppTasks {

    public static final String ACTION_DISMISS_NOTIFICATION = "dismiss-notification";

    public static void executeTask(Context context, String action) {
        if (ACTION_DISMISS_NOTIFICATION.equals(action)){ // Takes the action passed in from the IntentService class and, in this case, clears all notifications
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // This code would probably be better in another class
        notificationManager.cancelAll(); // This code would probably be better in another class
        }
    }
}

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.03

Notifications

How to create the ever-essential Notification. You can also add Notification Actions. Notifications are also intrinsic to the concept of Foreground Services e.g. a music app that persists controls in the notification shade.

Notification channels are a means of categorising your notifications so that users have fine grained control over which notifications they receive and which they deactivate (rather than having to decide all or nothing). An example would be to have separate notification channels for essential information and promotional offers:

A Notification can be set up as follows:

public static void appNotification (Context context) {
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // Get the NotificationManager system service
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // For Oreo and above we create a notification channel
            NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "Awesome App", NotificationManager.IMPORTANCE_HIGH); // Set a unique ID for the channel, give it a name that will be displayed in the notification. and set the importance
            notificationManager.createNotificationChannel(notificationChannel); // Get the NotificationManager to create the channel
        }
    NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) // NotificationCompat provides functionality to replace deprecated Notification methods and maintain backwards compatibility
                .setColor(ContextCompat.getColor(context, R.color.colorPrimary)) // Sets the background colour of the notification. ContextCompat provides functionality to replace deprecated Context methods and maintain backwards compatibility
                .setSmallIcon(R.drawable.ic_app_smallIcon) // Sets the small icon drawable which will appear in the status bar
                .setLargeIcon(aBitmap) // Sets the large icon bitmap which will appear in the notification content view
                .setContentTitle(context.getString(R.string.notification_title)) // Sets the title
                .setContentText(context.getString(R.string.notification_body)) // Sets the text
                .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_body))) // Sets the style of the text
                .setDefaults(Notification.DEFAULT_VIBRATE) // Set the defaults - Notification.DEFAULT_SOUND, Notification.DEFAULT_VIBRATE, Notification.DEFAULT_LIGHTS. Or for all default values, use Notification.DEFAULT_ALL
                .setContentIntent(makePendingIntent(context)) // Feed in the PendingIntent (see method below)
                .setAutoCancel(true); // Notification automatically disappears once clicked
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) { // For some build versions the priority needs to be set
            builder.setPriority(NotificationCompat.PRIORITY_HIGH);
        notificationManager.notify(NOTIFICATION_ID, builder.build()); // Trigger the notification
    }
    private static PendingIntent makePendingIntent (Context context) {
        Intent intent = new Intent(context, MainActivity.class);
        return PendingIntent.getActivity(context, PENDING_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.02

PendingIntent

This configures the permissions for services, private activities and broadcast-protected Intents to be started via Intent from e.g. a notification or another app, even if your app is not currently running. Hence the PendingIntent static methods: getService(), getActivity() or getActivities() and getBroadcast(). Each method requires 4 parameters e.g. for getActivity():

  • context: The Context in which this PendingIntent should start the activity
  • requestCode: Private request code for the sender, unique to this Intent
  • intent: Intent of the activity to be launched
  • flags: May be FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT, FLAG_UPDATE_CURRENT, or any of the flags as supported by Intent.fillIn() to control which unspecified parts of the intent that can be supplied when the actual send happens

A PendingIntent can be st up as follows:

    private static final int PENDING_INTENT_ID = 58; // Provide unique ID no. for the PendingIntent
    private static PendingIntent contentIntent (Context context) { // In this case we've set up a helper class
        Intent intent = new Intent(context, MainActivity.class); // Create the Intent - this opens the MainActivity
        return PendingIntent.getActivity(context, PENDING_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); // Creates the PendingIntent
    }

Quantity Strings (plurals)

When wanting to generate Strings that accommodate a range of numerical values, we need a way of determining the correct syntax according to the language. For example, in English we refer to ‘zero books’, ‘one book’ and ‘two (or more) books’. Android supports a set of options to cater for these variations, using the values: zero, one, two, few, many, other.

Methods such as getQuantityString can be used in conjunction with these options to provide the String you need to produce proper grammar. These are used exclusively for plurals and not for e.g. adding numerics to a String for information purposes: Inbox(12). Do not use quantity strings as a substitute for ‘if’ logic, as other languages handle grammatical rules differently.

It’s often possible to avoid quantity strings by using quantity-neutral formulations such as “Books: 1”. This makes your life and your translators’ lives easier, if it’s an acceptable style for your application.

The plurals can be stored in any xml resource file. It is the plurals element's name which Android identifies. The resource name is R.plurals.plural_name:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <plurals
        name="plural_name">
        <item
            quantity=["zero" | "one" | "two" | "few" | "many" | "other"]
            >text_string</item>
    </plurals>
</resources>

As a working example:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <plurals name="numberOfSongsAvailable">
        <!--
             As a developer, you should always supply "one" and "other"
             strings. Your translators will know which strings are actually
             needed for their language. Always include %d in "one" because
             translators will need to use %d for languages where "one"
             doesn't mean 1.
          -->
        <item quantity="one">%d song found.</item>
        <item quantity="other">%d songs found.</item>
    </plurals>
</resources>

Associated Java:

int count = getNumberOfSongsAvailable();
Resources res = getResources();
String songsFound = res.getQuantityString(R.plurals.numberOfSongsAvailable, count, count);

When using the getQuantityString() method, you need to pass the count twice if your string includes string formatting with a number. For example, for the string %d songs found, the first count parameter selects the appropriate plural string and the second count parameter is inserted into the %d placeholder. If your plural strings do not include string formatting, you don't need to pass the third parameter to getQuantityString.

Intent Service

A Service which runs off a completely separate thread to the main. All IntentService requests are handled on a single background thread and are issued in order. Therefore IntentServices are good for tasks that need to happen in order.

Services must be registered in the AndroidManifest.xml:

        <service
            android:name=".sync.myIntentService"
            android:exported="false"
            ></service>

An Intent Service can be started in a very similar way to an Activity:

Intent myIntent = new Intent(this, myIntentService.class);
startService(myIntent);

Extra data can be attached to the Intent when starting the Service, as with Activities:

Intent myIntent = new Intent(this, myIntentService.class);
myIntent.setAction("Some specific action");
startService(myIntent);

To create the Service, extend IntentService. Override the onHandle Intent method to tell it what to do in the background:

public class MyIntentService extends IntentService {

    @Override
    protected void onHandleIntent(Intent intent) {
        String action = intent.getAction(); //Add this line if extra data attached
        //Do background work here
    }
}

The IntentService will then stop itself when it is finished.

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.01

ViewModelFactory

ViewModelFactory allows us to produce custom ViewModels that allow for the inclusion of arguments in the Constructor (standard ViewModels do not).

To implement a ViewModelFactory, first create a ViewModelFactory class (this example corresponds to the code in the @Entity, @DAO, Build a Database and LiveData posts):

public class AddContactViewModelFactory extends ViewModelProvider.NewInstanceFactory {

    private final AppDatabase mDb;
    private final int mContactId;

    public AddTaskViewModelFactory(AppDatabase mDb, int mContactId) { // Arguments to be passed in
        this.mDb = mDb;
        this.mContactId = mContactId;
    }


    // Note: This can be reused with minor modifications
    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) { // When ViewModelFactory is created it returns a new ViewModel with the arguments fed in
        //noinspection unchecked
        return (T) new AddContactViewModel(mDb, mContactId);
    }
}

Then create the class declared in the ViewModelFactory's create() override:

public class AddContactViewModel extends ViewModel {

    private LiveData<ContactEntry> contactEntry;

    public AddTaskViewModel(AppDatabase database, int contactId) { // Constructor which takes the arguments
        contactEntry = database.contactDao().loadContactById(contactId); // Code which retrieves the LiveData object through the @DAO
    }

    public LiveData<ContactEntry> getContactEntry() { // Method of delivering the data to the Activity
        return contactEntry;
    }
}

In the Activity:

AddContactViewModelFactory addContactViewModelFactory = new AddContactViewModelFactory(mDb, mContactId); // Feed in arguments to the ViewModelFactory
final  AddContactViewModel viewModel = ViewModelProviders.of(this, addContactViewModelFactory).get(AddContactViewModel.class); // Create the View Model, including the ViewModelFactory in the arguments
                viewModel.getContactEntry().observe(this, new Observer<ContactEntry>() { // Observe the LiveData object which is cached in the ViewModel
    @Override
    public void onChanged(@Nullable ContactEntry contactEntry) {
        viewModel.getContactEntry().removeObserver(this); // Not quite sure why we've removed the Observer here
        populateUI(contactEntry); // Apply changes to UI
    }
});

ViewModel

ViewModel preserves data beyond the lifecycle of individual Activities i.e. it is lifecycle-aware – it will hold data for the UI even if e.g. the screen is rotated, causing the Activity to be remade. It persists from onCreate() until onCleared() (after finish() is applied to the Activity), thus helping to avoid re-querying a data source repeatedly upon Activity changes. This goes beyond the savedInstanceState which is limited to small amounts of data which can be easily serialised and de-serialised.

It also helps to avoid zombie threads (memory leaks), which were begun but not terminated as the Activity which created them was destroyed before they completed. When the Activity or Fragment is finished any Observers are closed, so no orphaned processes are left running.

Works in collaboration with LiveData. For more information see here. If your ViewModel needs access to the application context then extend AndroidViewModel rather than ViewModel (as below).

Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

To add the dependencies for ViewModel, see here.

To implement a ViewModel, first create a ViewModel class(this example corresponds to the code in the @Entity, @DAO, Build a Database and LiveData posts):

public class MainViewModel extends AndroidViewModel {

    LiveData<List<ContactEntry>> contacts;

    public MainViewModel(@NonNull Application application) {
        super(application);
        contacts = AppDatabase.getInstance(this.getApplication()).contactDao().loadAllContacts(); // Gets the List of contact entries from the instance of the database (see @DAO post)
    }

    public LiveData<List<ContactEntry>> getContacts() { // Create a getter for the LivaData List above
        return contacts;
    }
}

In your Activity, set an Observer on the getContacts() method in the ViewModel, and tell it what to do if the data changes:

MainViewModel viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
viewModel.getContacts().observe(this, new Observer<List<ContactEntry>>() {
            @Override
            public void onChanged(@Nullable List<ContactEntry> contactEntries) {
                mAdapter.setContacts(contactEntries); // Code to run if data changes
            }
        });

For applications which require variables to be passed to the ViewModel refer to the ViewModelFactory post. The Factory holds a template for creating ViewModels that you can customise to allow passing arguments into the ViewModel constructor.

LiveData

LiveData is an observable data holder class. This means it sits between the database and UI, and monitors changes in the database. On observing a change, the LiveData object (will have its setValue method called and) will notify the Observers which will reflect the new data in the UI. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

Runs by default outside of the main thread.

Often used in conjunction with ViewModel.

To add the dependencies for LiveData, see here.

To observe data in a database, simply wrap the return type in a LiveData<> tag (this example corresponds to the code in the @Entity, @DAO and Build a Database posts). In the @DAO file:

@Dao
public interface ContactDao {

    @Query("SELECT * FROM contacts ORDER BY age")
    LiveData<List<ContactEntry>> loadAllContacts();

    @Insert
    void insertContact(ContactEntry contactEntry);
 
    @Update(onConflict = OnConflictStrategy.REPLACE)
    void updateContact(ContactEntry contactEntry);
 
    @Delete
    void deleteContact(ContactEntry contactEntry);
 
    @Query("SELECT * FROM contacts WHERE name = :name")
    ContactEntry loadEntryByName(String name);
}

Any references/calls to this method will need to have their type wrapped in LiveData<> also.
We set up an Observer in our Activity (do this just once at onCreate and not e.g. in onResume so that the data will only be refreshed when it is changed):

        final LiveData<List<ContactEntry>> contacts = mDb.contactDao().loadAllContacts(); // mDb is instance of database (extends RoomDatabase). contactDao is the @DAO for the database (see above)
        contacts.observe(this, new Observer<List<ContactEntry>>() { // LivecycleOwner is set to 'this' in this example
            @Override
            public void onChanged(@Nullable List<ContactEntry> contactEntries) {
                // Do something with the new data
            }
        });

@TypeConverters – SQL/Room

When storing data in SQLite databases it must conform to the accepted data types e.g. a date must be stored in TEXT, REAL or INTEGER format. Therefore we can specify code to convert a date from a timestamp to a Date object (and back), and Room will use these when reading or writing data. TypeConverters must be declared when Building A Database.

public class DateConverter {
@TypeConverter
public static Date toDate(Long timestamp) { // Room will use this when reading from the database
return timestamp == null ? null : new Date(timestamp);
}

@TypeConverter
public static Long toTimestamp(Date date) { { // Room will use this when writing to the database
return date == null ? null : date.getTime();
}
}

@DAO (Data Access Object) – Room

An interface (API) which allows us to interact with our SQLite database tables. This example corresponds to the @Entity and Build A Database entries. Once these have been coded use LiveData to observe the data.

Create a custom class to define your table, using Entity to annotate it

@Dao // Required
public interface ContactDao {

    @Query("SELECT * FROM contacts ORDER BY age") // Database query
    List<ContactEntry> loadAllContacts(); // When loadAllContacts() is  called it will return a List<ContactEntry> containing the results of the query

    @Insert
    void insertContact(ContactEntry contactEntry); // Inserts a new ContactEntry into the table

    @Update(onConflict = OnConflictStrategy.REPLACE) // Set to replace (other options: Abort, Fail, Ignore, Rollback)
    void updateContact(ContactEntry contactEntry); // Update an existing Contact entry

    @Delete
    void deleteContact(ContactEntry contactEntry); // Delete an existing Contact entry

    @Query("SELECT * FROM contacts WHERE name = :name") // We can use the 'name' variable in the query by prefixing it with :
    ContactEntry loadEntryByName(String name); // Returns the ContactEntry which matches the name input by the user
}

To generate the table you will need to Build A Database. To define the table you will need to use @Entity.

FAB – Floating Action Button

A floating action button (FAB) performs the primary, or most common, action on a screen. It appears in front of all screen content, typically as a circular shape with an icon in its center. FABs come in three types: regular, mini, and extended.

Only use a FAB if it is the most suitable way to present a screen’s primary action.

FloatingActionButton fabButton = findViewById(R.id.fab); // Replace 'fab' with View id

fabButton.setOnClickListener(new View.OnClickListener() { // Set onClickListener
    @Override
    public void onClick(View view) {
        // Create a new intent to start another Activity
        Intent addNewIntent = new Intent(MainActivity.this, NewActivity.class);
        startActivity(addNewIntent);
    }
 });

ud851-Exercises-student\Lesson09-ToDo-List\T09.01

ItemTouchHelper

Add an ItemTouchHelper to recognize when a user swipes to e.g. delete an item. An ItemTouchHelper enables touch behaviour (like swipe and move) on each ViewHolder, and uses callbacks to signal when a user is performing these actions:

Create a Contract class:

new ItemTouchHelper (new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { // Detects side swipes
            @Override
            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                return false; // Do nothing if the user tries to 'move' the object
            }

            // Called when a user swipes left or right on a ViewHolder
            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
                // Here is where you'll implement swipe to delete
            }
        }).attachToRecyclerView(mRecyclerView); // Attach to the relevant View

ud851-Exercises-student\Lesson09-ToDo-List\T09.01

Cursors

This interface provides random read-write access to the result set returned by a database query.

The cursor begins at position -1 (before the first row). On moveToNext() it will point to the first row and return either true (next row exists) or false (already at end of rows). Other options include moveToPrevious(), moveToFirst() and moveToLast(). getColumnIndex (String columnName) will return the index of the column with the given name (columns are numbered beginning with 0). Use one of the get methods to retrieve the actual value (e.g. getString(int columnIndex), getInt(int columnIndex)), passing in the column index as an argument. getCount() returns the number of rows that are in the Cursor. close() should be called when you are finished interacting with the Cursor to prevent memory leaks leading to app crashes. The following code snippet iterates through all of the rows in the Cursor and prints out what was in the table:

int wordCol = cursor.getColumnIndex(ExampleContract.COLUMN_WORD);
int defCol = cursor.getColumnIndex(ExampleContract.COLUMN_DEFINITION);

while (cursor.moveToNext()) {
  String word = cursor.getString(wordCol);
  String definition = cursor.getString(defCol);
  Log.v("Cursor Example", word + " - " + definition);
}

cursor.close();

This code could be run in the postExecute() method of an AsyncTask so it has access to the data from the Cursor.
ud851-Exercises-student\Lesson08-Quiz-Example\T08.03

Content Resolver

Use a Content Resolver to handle the SQL commands (query(), insert(), update() or delete()) to a Content Provider’s data source. Since this can be a demanding task it should not be run on the main thread (e.g. AsyncTask):

Content URI (e.g. content://com.example.android.exampleapp/data) is comprised of: Content Provider Prefix: content:// Content Authority: com.example.android.exampleapp Path to Specific Data: data Against each argument is the SQL equivalent. For query():

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query // returns a Cursor object which contains all of the information requested. query() can be replaced with insert/update/delete()
    (ExampleAppContract.CONTENT_URI, // FROM table_name. Developer for Content Provider should have specified this constant in their app
     null, // Projection - col,col,col,... The columns to return for each row
     null, // selection - WHERE col = value. specifies the criteria for selecting rows.
     null, // selectionArgs - No exact equivalent. Selection arguments replace ? placeholders in the selection clause.
     null); // sortOrder - ORDER BY col,col,...

For insert():

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.insert
    (ExampleAppContract.CONTENT_URI, // FROM table_name. Developer for Content Provider should have specified this constant in their app
     null); // ContentValues - the values to be inserted in array of key-value pairings (key is column name)

For update():

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.update
    (ExampleAppContract.CONTENT_URI, // FROM table_name. Developer for Content Provider should have specified this constant in their app
     null, // ContentValues - the values to be updated in array of key-value pairings (key is column name)
     null, // selection - WHERE filter specifies the criteria for selecting rows.
     null); // selectionArgs - No exact equivalent. Selection arguments replace ? placeholders in the selection clause.

For delete():

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.delete
    (ExampleAppContract.CONTENT_URI, // FROM table_name. Developer for Content Provider should have specified this constant in their app
     null, // selection - WHERE filter specifies the criteria for selecting rows.
     null); // selectionArgs - No exact equivalent. Selection arguments replace ? placeholders in the selection clause.

ud851-Exercises-student\Lesson08-Quiz-Example\T08.02

PreferenceChangeListener

The differences between PreferenceChangeListener and SharedPreferenceChangeListener are:

  • SharedPreferenceChangeListener is triggered after any value is saved to the SharedPreferences file.
  • PreferenceChangeListener is triggered before a value is saved to the SharedPreferences file. Because of this, it can prevent an invalid update to a preference.
  • PreferenceChangeListeners are also attached to a single preference.

So it can be used as follows:

  • User updates a preference.
  • PreferenceChangeListener triggered for that preference e.g. to validate input.
  • The new value is saved to the SharedPreference file.
  • onSharedPreferenceChanged listeners are triggered.

Implement Preference.OnPreferenceChangeListener:

public class SettingsFragment extends PreferenceFragmentCompat implements OnSharedPreferenceChangeListener, Preference.OnPreferenceChangeListener

In the override for onCreatePreferences attach the listener to the chosen Preference:

@Override
public void onCreatePreferences(Bundle bundle, String s) {
         /* Other preference setup code code */
        //...
        Preference preference = findPreference(getString(R.string.pref_xxxx_key)); // Key for the Preference to watch. Customise
        preference.setOnPreferenceChangeListener(this); // Attach the listener to the Preference
}

In this example we will use the onPreferenceChange override to validate the input and restrict it to a number between 0.1 and 3:

@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
   Toast error = Toast.makeText(getContext(), "Please select a number between 0.1 and 3", Toast.LENGTH_SHORT); // Error Toast if input invalid

   String xxxxKey = getString(R.string.pref_xxxx_key);
   if (preference.getKey().equals(xxxxKey)) {
       String stringInput = (String) newValue; // Cast the Object o a String
       try {
           float input = Float.parseFloat(stringInput); // Try converting to a float
           if (input > 3 || input <= 0) { // If outside the accepted range...
               error.show(); // ...show the error Toast...
               return false; // ...and fail to register the new change
           }
       } catch (NumberFormatException nfe) { // If cannot convert to float then throw an exception
           error.show();
           return false;
       }
   }
   return true; // Otherwise accept the value
}

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.10

Setting Preference Summary/Label for ListPreference

ListPreference items in Preference screens do not automatically provide a label showing their current value. You can create labels for all list Preference items as so:

In the Settings Fragment onCreatePreferences override, get the Shared Preferences and the Preference screen. Then iterate through all of the Preferences in the screen, checking whether they are tickboxes. Any that aren't will have their value stored and passed to a setPreferenceSummary function along with the Preference itself:

    @Override
    public void onCreatePreferences(Bundle bundle, String s) {

        addPreferencesFromResource(R.xml.pref_xxxxx); // Reference to xml resource
        SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); // Retrieve the Shared Preferences from the Preference screen
        PreferenceScreen preferenceScreen = getPreferenceScreen(); // Retrieve the Preference screen
        int count = preferenceScreen.getPreferenceCount(); // The number of Preferences within the screen

        for (int i = 0; i < count; i++ ) { // For each Preference...
            Preference p = preferenceScreen.getPreference(i); // ...retrieve the Preference...
            if (!(p instanceof CheckBoxPreference)) { // ...make sure it isn't a checkbox...
                String value = sharedPreferences.getString(p.getKey(), ""); // ...then retrieve its value (can't use getString on a Boolean, so another reason to exclude tickboxes)...
                setPreferenceSummary(p, value); // ...and send the Preference and its value to the setPreferenceSummary function
            }
        }
    }

The setPreferenceSummary function is as follows:

    private void setPreferenceSummary(Preference preference, String value) {
        if (preference instanceof ListPreference) { // If it is a ListPreference...
            ListPreference listPreference = (ListPreference) preference; // ...cast it to an object of type ListPreference...
            int prefIndex = listPreference.findIndexOfValue(value); // ...retrieve the index of that value...
            if (prefIndex >= 0) {
                listPreference.setSummary(listPreference.getEntries()[prefIndex]); // ...and get the corresponding label for it, setting it as the Preference Summary
            }
        } else if (preference instanceof EditTextPreference) {
            // For EditTextPreferences, set the summary to the value's simple string representation.
            preference.setSummary(value);
        }
    }

Implement an OnSharedPreferenceChangeListener to make sure changes are implemented without having to force an onCreate (e.g. device rotation). Within the required onSharedPreferenceChanged override, again check to see if the preference is not a checkbox, and if so send the preference to the setPreferenceSummary function along with its value:

public class SettingsFragment extends PreferenceFragmentCompat implements OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        Preference preference = findPreference(key);
        if (preference != null) {
            if (!(preference instanceof CheckBoxPreference)) {
                String value = sharedPreferences.getString(preference.getKey(), "");
                setPreferenceSummary(preference, value);
            }
        }
    }
}

Register and unregister the OnSharedPreferenceChangeListener with the following overrides:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.08

Settings or Preferences – Making Changes In App

How to make user changes to options within the Settings Fragment affect changes in the app.

In the Activity call a function to setup shared preferences, then create that function. We will also link the shared preference object with the onSharedPreferenceChangeListener using .registerOnSharedPreferenceChangeListener:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_xxxxx);
        mTargetView =  findViewById(R.id.activity_xxxxx); // View to be affected by the preference change
        setupSharedPreferences(); // Call custom function to set up preferences
    }

    private void setupSharedPreferences() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Get a reference to the default shared preferences from the PreferenceManager class
        mTargetView.setShowText(sharedPreferences.getBoolean(getString(R.string.pref_xxxxx), getResources().getBoolean(R.bool.pref_xxxxx_default))); // Get the value of a preference and use it to call a function (in this case, the custom function mTargetView.setShowText) which will change behaviour in the app
        sharedPreferences.registerOnSharedPreferenceChangeListener(this); // We can use 'this' as context as the Activity will implement OnSharedPreferenceChangeListener (see next)
    }

To make sure the Activity implements the change without having to force an onCreate (e.g.rotate device) implement an OnSharedPreferenceChangeListener within the relevant Activity:

public class xxxxxActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // This function already passes us a reference to the sharedPreferences and key, so we don't need to use the PreferenceManager
        if (key.equals(getString(R.string.pref_show_bass_key))) {
            mTargetView.setShowBass(sharedPreferences.getBoolean(key, getResources().getBoolean(R.bool.pref_show_text_default)));
        }
    }
}

Unregister the OnSharedPreferenceChangeListener when the Activity is shut down using .unregisterOnSharedPreferenceChangeListener:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
    }

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.05

Settings or Preferences Fragment

How to set up a settings Activity with a PreferenceFragment.

Create an Activity named SettingsActivity (including the activity_settings.xml layout file) with the following code:

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
        ActionBar actionBar = this.getSupportActionBar();

        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true); // Enables the up button in the Action Bar
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            NavUtils.navigateUpFromSameTask(this); // Navigates up to parent activity (add supporting code to AndroidManifest.xml) OR...
            onBackPressed(); // ...replicates the function of the Back button (i.e. returns to last Activity) DO NOT ADD BOTH
        }
        return super.onOptionsItemSelected(item);
    }
}

To construct a menu with the option to enter the Settings fragment, create a new resource folder called 'menu' (if it doesn't already exist) and create 'xxxxx_menu.xml' (customise) as follows:

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">

    <item android:id="@+id/action_settings" // Rename the id as necessary
     android:title="Settings" // Text that will appear in the menu listing - move to strings.xml
     android:orderInCategory="100" // Order of item in menu
     app:showAsAction="never" /> // Determine whether it will appear in the action bar (if room) as opposed to the dropdown menu

</menu>

Add the following code to your Activity to add the menu:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.xxxxx_menu, menu); // Customise according to xml name
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(item.getItemId() == R.id.action_settings) {
            Intent startSettingsActivity = new Intent(this, SettingsActivity.class);
            startActivity(startSettingsActivity);
            return true; // Deliver explicit Intent to start Settings Activity on click
        }
        return super.onOptionsItemSelected(item);
    }

Within the AndroidManifest.xml file, make the SettingsActivity a child of the main activity (DO NOT do this if you intend to access your Settings screen from multiple Activities as this will lock it to one parent Activity. Instead, leave out lines 12-15 and use the onBackPressed() option is your Settings Activity) and also make the main activity launch mode 'singleTop' to prevent the Activity being remade on returning:

        <activity
         android:name=".xxxxxActivity" // Customise
         android:launchMode="singleTop"> // Stops parent activity being remade on exiting Settings
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity // Register
         android:name=".SettingsActivity"
         android:label="Settings" // ADD THESE LINES. Move text value to strings.xml
         android:parentActivityName=".xxxxxActivity"> // ADD THESE LINES ONLY IF NAVIGATING TO PARENT. Customise
            <meta-data // ADD THESE LINES ONLY IF NAVIGATING TO PARENT
             android:name="android.support.PARENT_ACTIVITY" // ADD THESE LINES ONLY IF NAVIGATING TO PARENT
             android:value=".xxxxxActivity" /> // ADD THESE LINES ONLY IF NAVIGATING TO PARENT. Customise
        </activity>

In the res -> xml folder create a file called pref_xxxxx.xml (customise) to define the options within the Settings Fragment:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <CheckBoxPreference // In this example we have a tickbox option android:defaultValue="true" // Default value of true/ticked
     android:key="show_text" // The reference of the value which can be changed
     android:summaryOff="@string/hidden" // Text to be shown when tickbox is not checked
     android:summaryOn="@string/shown" // Text to be shown when tickbox is checked
     android:title="@string/show_text" // Title of option
     />

</PreferenceScreen>

Create a class called SettingsFragment as follows (may require the addition of: implementation 'com.android.support:preference-v7:28.0.0' to your gradle file):

public class SettingsFragment extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle bundle, String s) {
        addPreferencesFromResource(R.xml.pref_xxxxx); // Add the preferences xml to display the options (customise)
    }
}

Adjust the activity_settings.xml file to reflect a Fragment layout:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_settings"
android:name="xxx.xxxxx.xxx.xxxxx.SettingsFragment" // Customise xxxxx with app name e.g. android.com.example.app
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>

Add a theme in the styles.xml file:

        <item name="preferenceTheme">@style/PreferenceThemeOverlay</item> // App will crash without this addition

To make user changes to the Settings Fragment take effect, follow this post: Settings or Preferences - Making Changes In App

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.01 & 02

onSaveInstanceState

How to persist data across changes in state such as device rotation. The onSavedInstanceState function stores information in a Bundle and is limited in the data it can contain (small amounts of data which can be easily serialised and de-serialised). It does not persist on app closure. **Try to keep the contents to considerably less than 100KB as this resides in RAM**

This example overrides the onPause and onSaveInstanceState functions to append an update to the screen when these events occur. When the screen is rotated the onSaveInstanceState function is called – it takes the contents of the screen (in particular the tv_lifecycle_events_display TextView) and stores it in the savedInstanceState Bundle. When onCreate is called after screen rotation, it checks for the LIFECYCLE_CALLBACKS_TEXT_KEY in the Bundle and, if present, writes the data back to the screen.

Add the following to your Activity file:

private static final String LIFECYCLE_CALLBACKS_TEXT_KEY = "callbacks"; // Key string constant for the data you will store
private static final String ON_PAUSE = "onPause"; // The text that will be displayed onscreen when the onPause state occurs
private static final String ON_SAVE_INSTANCE_STATE = "onSaveInstanceState"; // The text that will be displayed onscreen when the onSaveInstanceState state occurs

    protected void onCreate(Bundle savedInstanceState) { // Standard onCreate override
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        private TextView mLifecycleDisplay = findViewById(R.id.tv_lifecycle_events_display); // TextView the data will be printed to


        /*
         * If savedInstanceState is not null, that means our Activity is not being started for the
         * first time. Even if the savedInstanceState is not null, it is smart to check if the
         * bundle contains the key we are looking for. In our case, the key we are looking for maps
         * to the contents of the TextView that displays our list of callbacks. If the bundle
         * contains that key, we set the contents of the TextView accordingly.
         */
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(LIFECYCLE_CALLBACKS_TEXT_KEY)) { // If key is present...
                String allPreviousLifecycleCallbacks = savedInstanceState.getString(LIFECYCLE_CALLBACKS_TEXT_KEY);
                mLifecycleDisplay.setText(allPreviousLifecycleCallbacks); // ... display all of the previous contents to the TextView
            }
        }


    @Override
    protected void onPause() {
        super.onPause();
        mLifecycleDisplay.append(ON_PAUSE + "\n"); // Displays the event onscreen
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) { // Override onSaveInstanceState
        super.onSaveInstanceState(outState); // Call super class - ALWAYS DO THIS
        mLifecycleDisplay.append(ON_SAVE_INSTANCE_STATE + "\n"); // Displays the event onscreen
        String lifecycleDisplayTextViewContents = mLifecycleDisplay.getText().toString(); // Gets all of the content from the TextView
        outState.putString(LIFECYCLE_CALLBACKS_TEXT_KEY, lifecycleDisplayTextViewContents); // Puts TextView content into savedInstanceState Bundle
    }

}

	

Explicit Intent with URL

Open a third party app to view a webpage using implicit Intent.

    private void openWebPage(String url) {
        /*
         * We wanted to demonstrate the Uri.parse method because its usage occurs frequently. You
         * could have just as easily passed in a Uri as the parameter of this method.
         */
        Uri webpage = Uri.parse(url);

        /*
         * Here, we create the Intent with the action of ACTION_VIEW. This action allows the user
         * to view particular content. In this case, our webpage URL.
         */
        Intent intent = new Intent(Intent.ACTION_VIEW, webpage);

        /*
         * This is a check we perform with every implicit Intent that we launch. In some cases,
         * the device where this code is running might not have an Activity to perform the action
         * with the data we've specified. Without this check, in those cases your app would crash.
         */
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivity(intent);
        }
    }

ClickListener on list Adapter

Add a ClickListener to a ListAdapter, in this case a RecyclerView.Adapter.

Within list Adapter class (called ItemListAdapter in this example):
    private final ListItemClickListener mOnClickListener; // Declare field for OnClickListener
    public interface ListItemClickListener { // Create custom interface for your OnClickListener
        void onListItemClick (int item);
    }
    public ItemListAdapter(int numberOfItems, ListItemClickListener listItemClickListener) { // Add as parameter to Adapter constructor
        mNumberItems = numberOfItems;
        mOnClickListener = listItemClickListener;
    }
    class NumberViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { // In class that generates item View implement View.OnClickListener
        public NumberViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this); // Set an
        }
        @Override // Override onClick
        public void onClick(View v) {
            mOnClickListener.onListItemClick(getAdapterPosition()); // Call custom interface instance
        }
    }

Within Activity.

public class MainActivity extends AppCompatActivity implements ItemListAdapter.ListItemClickListener { // Implement the custom interface
        mAdapter = new ItemListAdapter(NUM_LIST_ITEMS, this); // Second parameter produces custom OnClickListener in constructor
    @Override // Override the onListItemClick from the custom interface
    public void onListItemClick(int item) {
        if (mToast != null) {
            mToast.cancel();
        }
        Context context = this;
        CharSequence text = "Item number: " + item;
        int duration = Toast.LENGTH_SHORT;
        mToast = Toast.makeText(context, text, duration);
        mToast.show();
    }
}

ud851-Exercises-student\Lesson03-Green-Recycler-View\T03.07-Exercise-RecyclerViewClickHandling
ud851-Sunshine-student\ud851-Sunshine-student\S03.02-Exercise-RecyclerViewClickHandling

ClickListener

Add a ClickListener to an item. Options 2 or 3 recommended.

Method 1 - clutters up onCreate, especially if several click events.
Does not promote code reuse.
public class AwesomeButtonActivity extends AppCompatActivity {

    private Button awesomeButton;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        awesomeButton = new Button(this);

        awesomeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                awesomeButtonClicked();
            }
        });
    }

    private void awesomeButtonClicked() {
        awesomeButton.setText("AWESOME!");
    }
}

Method 2 - Assigns OnClickListener to a field. Allows reuse.

public class AwesomeButtonActivity extends AppCompatActivity {

    private Button awesomeButton;
    
    private View.OnClickListener awesomeOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            awesomeButtonClicked();
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        awesomeButton = new Button(this);

        awesomeButton.setOnClickListener(awesomeOnClickListener);
    }

    private void awesomeButtonClicked() {
        awesomeButton.setText("AWESOME!");
    }
}

Method 3 - declare a class to implement OnClickListener.
Easy to add extra functionality to e.g. count how many times clicked.

public class AwesomeButtonActivity extends AppCompatActivity {

    private Button awesomeButton;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        awesomeButton = new Button(this);

        awesomeButton.setOnClickListener(new AwesomeButtonClick());
    }

    private void awesomeButtonClicked() {
        awesomeButton.setText("AWESOME!");
    }
    
    class AwesomeButtonClick implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            awesomeButtonClicked();
        }
    }
}

Method 4 - have Activity implement OnClickListener. Gets messy with multiple click items.
Exposes onClick to anyone who has access to the Activity.
Could be problematic if other interfaces with onClick are implemented.
Does not promote code reuse.


public class AwesomeButtonActivity extends AppCompatActivity implements View.OnClickListener {

    private Button awesomeButton;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        awesomeButton = new Button(this);

        awesomeButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        awesomeButtonClicked();
    }

    private void awesomeButtonClicked() {
        awesomeButton.setText("AWESOME!");
    }
    
}

Configure RecyclerView with Adapter, ViewHolder & LayoutManager

***ListView is outdated. Use this instead***
The Adapter acquires data from some source, stores the data in a ViewHolder (which holds a findViewById reference to the root View object for the item), caching the View objects represented in the layout. The number of ViewHolders created will equal the number of Views that can fit on the device screen plus a couple above and a couple below (approx) off-screen to make the recycling smooth. Therefore findViewById is only called when these Views are created and not for every item in the list of data.
A LinearLayoutManager is responsible for measuring and positioning item views within a RecyclerView into a linear list. This means that it can produce either a horizontal or vertical list depending on which parameter you pass in to the LinearLayoutManager constructor. It will take old Views which have scrolled off-screen and repopulate them with the new data from the ViewHolders.

This example demonstrates View recycling by showing a list (0-99) with the number on the LHS of the item View, and on the RHS the index of the ViewHolder. In the case of Nexus 6 there are 11 (0-10) of these, along with a green-shaded background of varying saturation to visually enforce the concept. Create an Adapter class.

// Avoid unnecessary garbage collection by using RecyclerView and ViewHolders.
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.NumberViewHolder> {

    private static final String TAG = RecyclerAdapter.class.getSimpleName(); // Standard practice for logging

    /*
     * The number of ViewHolders that have been created. Typically, you can figure out how many
     * there should be by determining how many list items fit on your screen at once and add 2 to 4
     * to that number. That isn't the exact formula, but will give you an idea of how many
     * ViewHolders have been created to display any given RecyclerView.
     *
     * Here's some ASCII art to hopefully help you understand:
     *
     *    ViewHolders on screen:
     *
     *        *-----------------------------*
     *        |         ViewHolder index: 0 |
     *        *-----------------------------*
     *        |         ViewHolder index: 1 |
     *        *-----------------------------*
     *        |         ViewHolder index: 2 |
     *        *-----------------------------*
     *        |         ViewHolder index: 3 |
     *        *-----------------------------*
     *        |         ViewHolder index: 4 |
     *        *-----------------------------*
     *        |         ViewHolder index: 5 |
     *        *-----------------------------*
     *        |         ViewHolder index: 6 |
     *        *-----------------------------*
     *        |         ViewHolder index: 7 |
     *        *-----------------------------*
     *
     *    Extra ViewHolders (off screen)
     *
     *        *-----------------------------*
     *        |         ViewHolder index: 8 |
     *        *-----------------------------*
     *        |         ViewHolder index: 9 |
     *        *-----------------------------*
     *        |         ViewHolder index: 10|
     *        *-----------------------------*
     *        |         ViewHolder index: 11|
     *        *-----------------------------*
     *
     *    Total number of ViewHolders = 11
     */
    private static int viewHolderCount;

    private int mNumberItems;

    /**
     * Constructor for RecyclerAdapter that accepts a number of items to display
     *
     * @param numberOfItems Number of items to display in list
     */
    public RecyclerAdapter(int numberOfItems) {
        mNumberItems = numberOfItems;
        viewHolderCount = 0;
    }

    /**
     *
     * This gets called when each new ViewHolder is created. This happens when the RecyclerView
     * is laid out. Enough ViewHolders will be created to fill the screen and allow for scrolling.
     *
     * @param viewGroup The ViewGroup that these ViewHolders are contained within.
     * @param viewType  If your RecyclerView has more than one type of item (which ours doesn't) you
     *                  can use this viewType integer to provide a different layout. See
     *                  {@link android.support.v7.widget.RecyclerView.Adapter#getItemViewType(int)}
     *                  for more details.
     * @return A new NumberViewHolder that holds the View for each list item
     */
    @Override
    public NumberViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        int layoutIdForListItem = R.layout.number_list_item; // number_list_item = layout xml for each item View
        Context context = viewGroup.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        boolean shouldAttachToParentImmediately = false;

        View view = inflater.inflate(layoutIdForListItem, viewGroup, shouldAttachToParentImmediately);
        NumberViewHolder viewHolder = new NumberViewHolder(view); // NumberViewHolder is a ViewHolder class we have defined below

        viewHolder.viewHolderIndex.setText("ViewHolder index: " + viewHolderCount); // Prints out the index number of the ViewHolder that is currently being used

        int backgroundColorForViewHolder = ColorUtils
                .getViewHolderBackgroundColorFromInstance(context, viewHolderCount); // This function exists in a separate class
        viewHolder.itemView.setBackgroundColor(backgroundColorForViewHolder); // Sets varying saturation of green as background for visual impact

        viewHolderCount++;
        Log.d(TAG, "onCreateViewHolder: number of ViewHolders created: "
                + viewHolderCount); // Prints number of ViewHolders to Log.d
        return viewHolder;
    }

    /**
     * OnBindViewHolder is called by the RecyclerView to display the data at the specified
     * position. In this method, we update the contents of the ViewHolder to display the correct
     * indices in the list for this particular position, using the "position" argument that is conveniently
     * passed into us.
     *
     * @param holder   The ViewHolder which should be updated to represent the contents of the
     *                 item at the given position in the data set.
     * @param position The position of the item within the adapter's data set.
     */
    @Override
    public void onBindViewHolder(NumberViewHolder holder, int position) {
        Log.d(TAG, "#" + position); // Prints Viewholder index to Log.d as Views are recycled
        holder.bind(position); // Binds the index number (0-99) to the ViewHolder (see bind function below)
    }

    /**
     * This method simply returns the number of items to display. It is used behind the scenes
     * to help layout our Views and for animations.
     *
     * @return The number of items available
     */
    @Override
    public int getItemCount() {
        return mNumberItems;
    }

    /**
     * Cache of the children views for a list item.
     */
    class NumberViewHolder extends RecyclerView.ViewHolder {

        // Will display the position in the list, ie 0 through getItemCount() - 1
        TextView listItemNumberView;
        // Will display which ViewHolder is displaying this data
        TextView viewHolderIndex;

        /**
         * Constructor for our ViewHolder. Within this constructor, we get a reference to our
         * TextViews.
         * @param itemView The View that you inflated in
         *                 {@link RecyclerAdapter#onCreateViewHolder(ViewGroup, int)}
         */
        public NumberViewHolder(View itemView) {
            super(itemView);

            listItemNumberView = (TextView) itemView.findViewById(R.id.tv_item_number); // Reference to TextView on LHS of View
            viewHolderIndex = (TextView) itemView.findViewById(R.id.tv_view_holder_instance); // Reference to TextView on RHS of View
        }

        /**
         * A method we wrote for convenience. This method will take an integer as input and
         * use that integer to display the appropriate text within a list item.
         * @param listIndex Position of the item in the list
         */
        void bind(int listIndex) {
            listItemNumberView.setText(String.valueOf(listIndex)); // Sets the item number (0-99) to the TextView on LHS of item View
        }
    }
}

Add relevant code to Activity.

public class MainActivity extends AppCompatActivity {

    private static final int NUM_LIST_ITEMS = 100; //Sets number of list items to 100

    /*
     * References to RecyclerView and Adapter
     */
    private RecyclerAdapter mAdapter;
    private RecyclerView mNumbersList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /*
         * Using findViewById, we get a reference to our RecyclerView from xml. This allows us to
         * do things like set the adapter of the RecyclerView and toggle the visibility.
         */
        mNumbersList = (RecyclerView) findViewById(R.id.rv_numbers);

        /*
         * A LinearLayoutManager is responsible for measuring and positioning item views within a
         * RecyclerView into a linear list. This means that it can produce either a horizontal or
         * vertical list depending on which parameter you pass in to the LinearLayoutManager
         * constructor. By default, if you don't specify an orientation, you get a vertical list.
         * In our case, we want a vertical list, so we don't need to pass in an orientation flag to
         * the LinearLayoutManager constructor.
         *
         * There are other LayoutManagers available to display your data in uniform grids,
         * staggered grids, and more! See the developer documentation for more details.
         */
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        mNumbersList.setLayoutManager(layoutManager);

        /*
         * Use this setting to improve performance if you know that changes in content do not
         * change the child layout size in the RecyclerView
         */
        mNumbersList.setHasFixedSize(true);

        /*
         * The RecyclerAdapter is responsible for displaying each item in the list.
         */
        mAdapter = new RecyclerAdapter(NUM_LIST_ITEMS);
        mNumbersList.setAdapter(mAdapter);
    }
}

ud851-Exercises-student\Lesson03-Green-Recycler-View\T03.07-Exercise-RecyclerViewClickHandling

Toast

Add a toast message

Context context = getApplicationContext(); // Or MainActivity.this or whatever
CharSequence text = "Hello toast!";
int duration = Toast.LENGTH_SHORT; // Alternatively LENGTH_LONG

Toast toast = Toast.makeText(context, text, duration);
toast.show(); // Don't forget this bit

Parse JSON

Basic example of parsing data from a JSON.

Example JSON file
{
   "temp": {
      "min":"11.34",
      "max":"19.01"
   },
   "weather": {
      "id":"801",
      "condition":"Clouds",
      "description":"few clouds"
   },
   "pressure":"1023.51",
   "humidity":"87"
}

Function to extract the 'condition' from the JSON file.

String getCondition(String JSONString) {
   JSONObject forecast = new JSONObject(JSONString);
   JSONObject weather = forecast.getJSONObject("weather");
   return weather.getString("condition");
}

AsyncTask

How to set up an AsyncTask to take a task off the main thread

Can override the following:
Params -> doInBackground // Runs on background thread – only method we MUST override
Progress -> onProgressUpdate // Runs on main thread Call publishProgress as many times as you want during process
Result -> onPostExecute // Runs on main thread
(Also a onPreExecute function) // Runs on main thread

Initiates the task on a background thread and when it has finished runs onPostExecute. Takes three parameters:

  • Params - the type of the parameters sent to the task upon execution
  • Progress - the type of the progress units published during the background computation
  • Result - the type of the result of the background computation
    public class AsyncQueryTask extends AsyncTask<URL, Void, String> { // The first param is a ... which is technically passed as an array
        @Override
        protected String doInBackground(URL... params) { // First param (array) is passed in
            URL searchUrl = params[0]; // In this instance we only use the first URL in the array
            String githubSearchResults = null;
            try {
                githubSearchResults = NetworkUtils.getResponseFromHttpUrl(searchUrl);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return githubSearchResults;
        }

        @Override
        protected void onPostExecute(String s) { // String from doInBackground passed in here
            if (s != null & !s.equals("")) {
                mSearchResultsTextView.setText(s);
            }
        }
    }

Call 'execute' with params to begin the task

new AsyncQueryTask().execute(searchUrl);

If writing a self-executing Asynctask, call .execute() at the end of the instantiation:

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... voids) {
                // Do something
            }
        }.execute();

ud851-Exercises-student\Lesson02-GitHub-Repo-Search\T02.06-Exercise-AddPolish

Build a URL with Uri Builder includes HttpURLConnection function

URL is a specific class of Uri. Use the following function to compile a valid URL. HttpURLConnection will require this when making a connection.

Method to build URL
    final static String BASE_URL =
            "https://api.github.com/search/repositories"; // Query the GitHub repositories
    final static String PARAM_QUERY = "q";
    final static String PARAM_SORT = "sort";
    final static String sortBy = "stars"; // Sorted by the number of stars the repo has

    public static URL buildUrl(String searchQuery) {
        Uri builtURI = Uri.parse(BASE_URL)
                .buildUpon()
                .appendQueryParameter(PARAM_QUERY, searchQuery)
                .appendQueryParameter(PARAM_SORT, sortBy)
                .build();
        try {
            URL url = new URL(builtURI.toString());
            Log.d("buildUrl produced URL ", url.toString()); // Produces URL: https://api.github.com/search/repositories?q=android&sort=stars
            return url;
        } catch (MalformedURLException e) {
            e.printStackTrace();
            Log.d("buildUrl", "Exception thrown");
        }
        return null;
    }

A typical helper method for fetching the results of the query based on the URL.

    public static String getResponseFromHttpUrl(URL url) throws IOException {
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        try {
            InputStream in = urlConnection.getInputStream();

            Scanner scanner = new Scanner(in);
            scanner.useDelimiter("\\A"); // \A represents the beginning of the stream. Forces the scanner to read the entire contents of the stream
// It buffers the data and also converts the stream from UTF-8 (JSON/JS) to UTF-16 (which Android uses)
            boolean hasInput = scanner.hasNext();
            if (hasInput) {
                return scanner.next();
            } else {
                return null;
            }
        } finally {
            urlConnection.disconnect();
        }
    }

ud851-Exercises-student\Lesson02-GitHub-Repo-Search\T02.06-Exercise-AddPolish

Create Menu in ActionBar

Create a menu in the ActionBar. Can be in the main bar or under the menu icon.

Create 'main.xml' in 'menu' folder in 'res' if does not exist.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item android:id="@+id/action_search" // Add an item tag for each item in the menu
        android:title="@string/search"
        android:orderInCategory="1" // Order item will show in menu list
        app:showAsAction="ifRoom" // "ifRoom" will display menu item in ActionBar if room
        />

</menu>

Override onCreateOptionsMenu in Activity Java file.

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

Override onOptionsItemSelectedin Activity Java file.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int menuItem = item.getItemId();
    if (menuItem == R.id.action_search) { // Change 'action_search' to id of menu layout item
        Context context = MainActivity.this; // Change 'MainActivity' to current Activity
        CharSequence text = "Click handled"; // What happens when menu item is clicked
        int duration = Toast.LENGTH_SHORT; // What happens when menu item is clicked
        Toast toast = Toast.makeText(context, text, duration); // What happens when menu item is clicked
        toast.show(); // What happens when menu item is clicked
        return true;
    }
    return super.onOptionsItemSelected(item);
}

ud851-Exercises-student\Lesson02-GitHub-Repo-Search\T02.06-Exercise-AddPolish

LinearLayout with EditText and ScrollView

LinearLayout with and EditText, a TextView and a ScrollView with TextView.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingTop="16dp"
    android:paddingLeft="16dp"
    android:paddingRight="16dp">
	
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/et_search_box"
        android:textSize="22sp"
        android:hint="Enter a query and click Search"/>

    <TextView
        android:id="@+id/tv_url_display"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22sp"
        android:layout_marginTop="8dp"
        android:text="The search URL will show here when you click Search"/>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/tv_github_search_results_json"
            android:textSize="18sp"
            android:text="Make a search!"/>
    </ScrollView>

</LinearLayout>

Adding An Activity, Up Navigation, Accessing With Explicit Intents, Passing Data

How to add an Activity to your app and navigate to it using an explicit Intent.

Create an empty Activity through the IDE. IDE will automatically add the essential code to the AndroidManifests.xml file. Make any desirable additions.

        <activity android:name="com.example.android.explicitintent.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.example.android.explicitintent.ChildActivity"
            android:label="@string/action_settings" // Name at top of screen for Activity
            android:parentActivityName=".MainActivity"> // Make sure the back arrow functions properly by declaring parent activity
            <meta-data // To support Android 4.0 and lower
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".MainActivity" />
        </activity>

In new Activity, enable Up navigation.

getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Or getActionBar() on older versions before support library

In Parent Activity, set up an explicit Intent.

        private Button doSomethingCoolButton = (Button) findViewById(R.id.b_do_something_cool);
        private String text = "Send me!"; // Text to be passed to new Activity
        mDoSomethingCoolButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Context context = MainActivity.this;

                /* This is the class that we want to start (and open) when the button is clicked. */
                Class destinationActivity = ChildActivity.class;

                /*
                 * Here, we create the Intent that will start the Activity we specified above in
                 * the destinationActivity variable. The constructor for an Intent also requires a
                 * context, which we stored in the variable named "context".
                 */
                Intent startChildActivityIntent = new Intent(context, destinationActivity);

                startChildActivityIntent.putExtra(Intent.EXTRA_TEXT, text); // Add text as extra data

                /*
                 * Once the Intent has been created, we can use Activity's method, "startActivity"
                 * to start the ChildActivity.
                 */
                startActivity(startChildActivityIntent);
            }
        });

In new Activity, access and use the extra data.

        mDisplayText = (TextView) findViewById(R.id.tv_display); // Find a TextView

        // Use the getIntent method to store the Intent that started this Activity in a variable
        Intent intent = getIntent();

        // Check if this Intent has the extra text we passed from MainActivity
        if (intent.hasExtra(Intent.EXTRA_TEXT)) {
            String enteredText = intent.getStringExtra(Intent.EXTRA_TEXT);
            mDisplayText.setText(enteredText); // Assign text to TextView
        }