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

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

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