/* * Professional Android, 4th Edition * Reto Meier and Ian Lake * Copyright 2018 John Wiley Wiley & Sons, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.professionalandroid.apps.myapplication; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.media.RingtoneManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; import android.support.annotation.RequiresApi; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.app.RemoteInput; import android.support.v4.app.TaskStackBuilder; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.ProgressBar; import android.widget.TextView; import com.firebase.jobdispatcher.Constraint; import com.firebase.jobdispatcher.FirebaseJobDispatcher; import com.firebase.jobdispatcher.GooglePlayDriver; import com.firebase.jobdispatcher.Trigger; import java.time.LocalDateTime; import java.util.Calendar; import java.util.concurrent.TimeUnit; import static java.lang.System.currentTimeMillis; public class MyActivity extends AppCompatActivity { /* * Listing 11-5: Moving processing to a background Handler Thread * Listing 11-6: Sending information between Threads with Messages */ private HandlerThread mWorkerThread; private Handler mHandler; private static final int BACKGROUND_WORK = 1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mWorkerThread = new HandlerThread("WorkerThread"); mWorkerThread.start(); mHandler = new Handler(mWorkerThread.getLooper()); // Listing 11-6 mHandler = new Handler(mWorkerThread.getLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == BACKGROUND_WORK) { // TODO [ ... Time consuming operations ... ] } // else, handle a different type of message return false; } }); } // This method is called on the main Thread. private void doBackgroundExecution() { mHandler.post(new Runnable() { public void run() { // TODO [ ... Time consuming operations ... ] } }); // Listing 11-6 mHandler.sendEmptyMessage(BACKGROUND_WORK); } @Override public void onDestroy() { super.onDestroy(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { mWorkerThread.quitSafely(); } else mWorkerThread.quit(); } /* * Listing 11-1: An Asynchronous Task defi nition */ // The Views in your UI that you want to update from the AsyncTask private ProgressBar asyncProgress; private TextView asyncTextView; private class MyAsyncTask extends AsyncTask<String, Integer, String> { @Override protected String doInBackground(String... parameter) { // Moved to a background Thread. String result = ""; int myProgress = 0; int inputLength = parameter[0].length(); // Perform background processing task, update myProgress] for (int i = 1; i <= inputLength; i++) { myProgress = i; result = result + parameter[0].charAt(inputLength - i); try { Thread.sleep(100); } catch (InterruptedException e) { } publishProgress(myProgress); } // Return the value to be passed to onPostExecute return result; } @Override protected void onPreExecute() { // Synchronized to UI Thread. // Update the UI to indicate that background loading is occurring asyncProgress.setVisibility(View.VISIBLE); } @Override protected void onProgressUpdate(Integer... progress) { // Synchronized to UI Thread. // Update progress bar, Notification, or other UI elements asyncProgress.setProgress(progress[0]); } @Override protected void onPostExecute(String result) { // Synchronized to UI Thread. // Report results via UI update, Dialog, or Notifications asyncProgress.setVisibility(View.GONE); asyncTextView.setText(result); } } private void listing11_2() { // Listing 11-2: Executing an Async Task String input = "redrum ... redrum"; new MyAsyncTask().execute(input); } private void listing11_3() { // Listing 11-3: Executing Async Tasks in parallel String input = "redrum ... redrum"; new MyAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, input); } /* * Listing 11-10: Scheduling a job that requires unmetered network and charging */ // Can be any integer, just needs to be unique across your app private static final int BACKGROUND_UPLOAD_JOB_ID = 13; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public static void scheduleBackgroundUpload(Context context) { // Access the Job Scheduler JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); // Get a reference to my Job Service implementation ComponentName jobServiceName = new ComponentName( context, BackgroundJobService.class); // Build a Job Info to run my Job Service jobScheduler.schedule( new JobInfo.Builder(BACKGROUND_UPLOAD_JOB_ID, jobServiceName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresCharging(true) // Wait at most a day before relaxing our network constraints .setOverrideDeadline(TimeUnit.DAYS.toMillis(1)) .build()); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private static void listing11_11(Context context) { JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); ComponentName jobServiceName = new ComponentName(context, BackgroundJobService.class); // Listing 11-11: Scheduling a job with customized back-off criteria jobScheduler.schedule( new JobInfo.Builder(BACKGROUND_UPLOAD_JOB_ID, jobServiceName) // Require a network connection .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // Require the device has been idle .setRequiresDeviceIdle(true) // Force Job to ignore constraints after 1 day .setOverrideDeadline(TimeUnit.DAYS.toMillis(1)) // Retry after 30 seconds, with linear back-off .setBackoffCriteria(30000, JobInfo.BACKOFF_POLICY_LINEAR) // Reschedule after the device has been rebooted .setPersisted(true) .build()); } /* * Listing 11-14: Scheduling a job that requires unmetered network and * charging using the Firebase Job Dispatcher */ private static final String BACKGROUND_UPLOAD_JOB_TAG = "background_upload"; public static void scheduleFirebaseBackgroundUpload(Context context) { FirebaseJobDispatcher jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); jobDispatcher.mustSchedule( jobDispatcher.newJobBuilder() .setTag(BACKGROUND_UPLOAD_JOB_TAG) .setService(FirebaseJobService.class) .setConstraints( Constraint.ON_UNMETERED_NETWORK, Constraint.DEVICE_CHARGING) .setTrigger(Trigger.executionWindow( 0, // can start immediately (int) TimeUnit.DAYS.toSeconds(1))) // wait at most a day .build()); } private void listing11_15() { // Listing 11-15: Using the Notification Manager NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); } /* * Listing 11-16: Creating a Notification Channel */ private static final String MESSAGES_CHANNEL = "messages"; public void createMessagesNotificationChannel(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = context.getString(R.string.messages_channel_name); NotificationChannel channel = new NotificationChannel( MESSAGES_CHANNEL, name, NotificationManager.IMPORTANCE_HIGH); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } private void listing11_17(Context context) { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); // Listing 11-17: Creating and posting a Notification final int NEW_MESSAGE_ID = 0; createMessagesNotificationChannel(context); NotificationCompat.Builder builder = new NotificationCompat.Builder( context, MESSAGES_CHANNEL); // These would be dynamic in a real app String title = "Reto Meier"; String text = "Interested in a new book recommendation?" + " I have one you should check out!"; builder.setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(text); notificationManager.notify(NEW_MESSAGE_ID, builder.build()); } private void listing11_18(Context context, NotificationCompat.Builder builder) { // Listing 11-18: Adding a content Intent to start an Activity // This could be any Intent. Here we use the app's // launcher activity as a simple example Intent launchIntent = context.getPackageManager() .getLaunchIntentForPackage(context.getPackageName()); PendingIntent contentIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(launchIntent) .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(contentIntent); } private void listing11_19(NotificationCompat.Builder builder, Context context) { String title = "This is a title"; String text = "This is the body text, that goes on for some length"; Bitmap profilePicture = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_background); // Listing 11-19: Applying a Big Text Style to a Notification builder.setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(text) .setLargeIcon(profilePicture) .setStyle(new NotificationCompat.BigTextStyle().bigText(text)); } private void listing11_20(NotificationCompat.Builder builder, Context context) { String title = "This is a title"; String text = "This is the body text, that goes on for some length"; Bitmap profilePicture = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_notification); Bitmap aBigBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_background); // Listing 11-20: Applying a Big Picture Style to a Notification builder.setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(text) .setLargeIcon(profilePicture) .setStyle(new NotificationCompat.BigPictureStyle().bigPicture(aBigBitmap)); } private void listing11_21(NotificationCompat.Builder builder) { String userDisplayName = "Ian Lake"; long message1TimeInMillis = currentTimeMillis() - (1000*20); long message2TimeInMillis = message1TimeInMillis + (1000*10); long message3TimeInMillis = message1TimeInMillis + (1000*5); String fromMe = null; // Listing 11-21: Creating a Messaging Style Notification builder .setShowWhen(true) // Show the time the Notification was posted .setStyle(new NotificationCompat.MessagingStyle(userDisplayName) .addMessage("Hi Reto!", message1TimeInMillis, "Ian Lake") .addMessage("How's it going?", message2TimeInMillis, "Ian Lake") .addMessage("Very well indeed. And you?", message3TimeInMillis, fromMe)); } private void listing11_22(NotificationChannel channel, NotificationCompat.Builder builder) { // Listing 11-22: Customizing a Notification's alerts // For Android 8.0+ higher devices: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); channel.setVibrationPattern(new long[]{1000, 1000, 1000, 1000, 1000}); channel.setLightColor(Color.RED); } else { // For Android 7.1 or lower devices: builder.setPriority(NotificationCompat.PRIORITY_HIGH) .setSound( RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setVibrate(new long[]{1000, 1000, 1000, 1000, 1000}) .setLights(Color.RED, 0, 1); } } private void listing11_23(NotificationCompat.Builder builder) { // Listing 11-23: Setting a Notification category and sender builder.setCategory(NotificationCompat.CATEGORY_CALL) .addPerson("tel:5558675309"); } private void listing11_24(Context context, NotificationCompat.Builder builder) { Uri emailUri = Uri.parse("[email protected]"); // Listing 11-24: Adding a Notification action Intent deleteAction = new Intent(context, DeleteBroadcastReceiver.class); deleteAction.setData(emailUri); PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, deleteAction, PendingIntent.FLAG_UPDATE_CURRENT); builder.addAction( new NotificationCompat.Action.Builder( R.drawable.delete, context.getString(R.string.delete_action), deleteIntent).build()); } private void listing11_25(Context context, NotificationCompat.Builder builder, Uri chatThreadUri) { // Listing 11-25: Adding a direct reply action // The key you'll use to later retrieve the reply final String KEY_TEXT_REPLY = "KEY_TEXT_REPLY"; Intent replyAction = new Intent(context, ReplyBroadcastReceiver.class); replyAction.setData(chatThreadUri); PendingIntent replyIntent = PendingIntent.getBroadcast(context, 0, replyAction, PendingIntent.FLAG_UPDATE_CURRENT); // Construct the RemoteInput RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY) .setLabel(context.getString(R.string.reply_hint_text)) .build(); builder.addAction( new NotificationCompat.Action.Builder( R.drawable.reply, context.getString(R.string.reply_action), replyIntent) .addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .extend(new NotificationCompat.Action.WearableExtender() .setHintDisplayActionInline(true)) .build()); } private void listing11_26(NotificationCompat.Builder builder) { String[] emailSubjects = {"Testing", "Feedback"}; String accountName = "Home Gmail"; // Listing 11-26: Building an InboxStyle group summary Notification NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); for (String emailSubject : emailSubjects) inboxStyle.addLine(emailSubject); builder.setSubText(accountName) .setGroup(accountName) .setGroupSummary(true) .setStyle(inboxStyle); } @Override protected void onResume() { super.onResume(); // Listing 11-30: Receiving data from a Firebase Notification Intent intent = getIntent(); if (intent != null) { String value = intent.getStringExtra("your_key"); // Change your behavior based on the value such as starting // the appropriate deep link activity } } @RequiresApi(api = Build.VERSION_CODES.M) private void listing11_32_to_34() { // Listing 11-32: Creating an alarm that triggers at the top of the hour // Get a reference to the Alarm Manager AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // Find the trigger time Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); calendar.add(Calendar.HOUR, 1); long time = calendar.getTimeInMillis(); // Create a Pending Intent that will broadcast and action String ALARM_ACTION = "ALARM_ACTION"; Intent intentToFire = new Intent(ALARM_ACTION); PendingIntent alarmIntent = PendingIntent.getBroadcast(this, 0, intentToFire, 0); // Set the alarm alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, alarmIntent); // LISTING 11-33: Canceling an Alarm alarmManager.cancel(alarmIntent); // Listing 11-34: Setting an Alarm Clock // Create a Pending Intent that can be used to show or edit the alarm clock // when the alarm clock icon is touched Intent alarmClockDetails = new Intent(this, AlarmClockActivity.class); PendingIntent showIntent = PendingIntent.getActivity(this, 0, alarmClockDetails, 0); // Set the alarm clock, which will fire the alarmIntent at the set time alarmManager.setAlarmClock( new AlarmManager.AlarmClockInfo(time, showIntent), alarmIntent); } /* * Listing 11-36: Creating a Service Connection for Service binding */ // Reference to the service private MyBoundService serviceRef; // Handles the connection between the service and activity private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // Called when the connection is made. serviceRef = ((MyBoundService.MyBinder)service).getService(); } public void onServiceDisconnected(ComponentName className) { // Received when the service unexpectedly disconnects. serviceRef = null; } }; // Listing 11-37: Binding to a Service private void listing11_37() { // Bind to the service Intent bindIntent = new Intent(MyActivity.this, MyBoundService.class); bindService(bindIntent, mConnection, Context.BIND_AUTO_CREATE); } private void listing11_39() { // Listing 11-39: Starting a Service // Explicitly start My Service Intent intent = new Intent(this, MyService.class); intent.setAction("Upload"); intent.putExtra("TRACK_NAME", "Best of Chet Haase"); startService(intent); } private void listing11_40() { // Listing 11-40: Stopping a Service // Stop a service explicitly. stopService(new Intent(this, MyService.class)); } }