Home | History | Annotate | Download | only in model
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.printspooler.model;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.app.Notification;
     22 import android.app.Notification.Action;
     23 import android.app.Notification.InboxStyle;
     24 import android.app.NotificationManager;
     25 import android.app.PendingIntent;
     26 import android.content.BroadcastReceiver;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.graphics.drawable.BitmapDrawable;
     30 import android.graphics.drawable.Icon;
     31 import android.net.Uri;
     32 import android.os.AsyncTask;
     33 import android.os.PowerManager;
     34 import android.os.PowerManager.WakeLock;
     35 import android.os.RemoteException;
     36 import android.os.ServiceManager;
     37 import android.os.UserHandle;
     38 import android.print.IPrintManager;
     39 import android.print.PrintJobId;
     40 import android.print.PrintJobInfo;
     41 import android.print.PrintManager;
     42 import android.provider.Settings;
     43 import android.util.ArraySet;
     44 import android.util.Log;
     45 
     46 import com.android.printspooler.R;
     47 
     48 import java.util.ArrayList;
     49 import java.util.List;
     50 
     51 /**
     52  * This class is responsible for updating the print notifications
     53  * based on print job state transitions.
     54  */
     55 final class NotificationController {
     56     public static final boolean DEBUG = false;
     57 
     58     public static final String LOG_TAG = "NotificationController";
     59 
     60     private static final String INTENT_ACTION_CANCEL_PRINTJOB = "INTENT_ACTION_CANCEL_PRINTJOB";
     61     private static final String INTENT_ACTION_RESTART_PRINTJOB = "INTENT_ACTION_RESTART_PRINTJOB";
     62 
     63     private static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID";
     64 
     65     private static final String PRINT_JOB_NOTIFICATION_GROUP_KEY = "PRINT_JOB_NOTIFICATIONS";
     66     private static final String PRINT_JOB_NOTIFICATION_SUMMARY = "PRINT_JOB_NOTIFICATIONS_SUMMARY";
     67 
     68     private final Context mContext;
     69     private final NotificationManager mNotificationManager;
     70 
     71     /**
     72      * Mapping from printJobIds to their notification Ids.
     73      */
     74     private final ArraySet<PrintJobId> mNotifications;
     75 
     76     public NotificationController(Context context) {
     77         mContext = context;
     78         mNotificationManager = (NotificationManager)
     79                 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
     80         mNotifications = new ArraySet<>(0);
     81     }
     82 
     83     public void onUpdateNotifications(List<PrintJobInfo> printJobs) {
     84         List<PrintJobInfo> notifyPrintJobs = new ArrayList<>();
     85 
     86         final int printJobCount = printJobs.size();
     87         for (int i = 0; i < printJobCount; i++) {
     88             PrintJobInfo printJob = printJobs.get(i);
     89             if (shouldNotifyForState(printJob.getState())) {
     90                 notifyPrintJobs.add(printJob);
     91             }
     92         }
     93 
     94         updateNotifications(notifyPrintJobs);
     95     }
     96 
     97     /**
     98      * Update notifications for the given print jobs, remove all other notifications.
     99      *
    100      * @param printJobs The print job that we want to create notifications for.
    101      */
    102     private void updateNotifications(List<PrintJobInfo> printJobs) {
    103         ArraySet<PrintJobId> removedPrintJobs = new ArraySet<>(mNotifications);
    104 
    105         final int numPrintJobs = printJobs.size();
    106 
    107         // Create summary notification
    108         if (numPrintJobs > 1) {
    109             createStackedNotification(printJobs);
    110         } else {
    111             mNotificationManager.cancel(PRINT_JOB_NOTIFICATION_SUMMARY, 0);
    112         }
    113 
    114         // Create per print job notification
    115         for (int i = 0; i < numPrintJobs; i++) {
    116             PrintJobInfo printJob = printJobs.get(i);
    117             PrintJobId printJobId = printJob.getId();
    118 
    119             removedPrintJobs.remove(printJobId);
    120             mNotifications.add(printJobId);
    121 
    122             createSimpleNotification(printJob);
    123         }
    124 
    125         // Remove notifications for print jobs that do not exist anymore
    126         final int numRemovedPrintJobs = removedPrintJobs.size();
    127         for (int i = 0; i < numRemovedPrintJobs; i++) {
    128             PrintJobId removedPrintJob = removedPrintJobs.valueAt(i);
    129 
    130             mNotificationManager.cancel(removedPrintJob.flattenToString(), 0);
    131             mNotifications.remove(removedPrintJob);
    132         }
    133     }
    134 
    135     private void createSimpleNotification(PrintJobInfo printJob) {
    136         switch (printJob.getState()) {
    137             case PrintJobInfo.STATE_FAILED: {
    138                 createFailedNotification(printJob);
    139             } break;
    140 
    141             case PrintJobInfo.STATE_BLOCKED: {
    142                 if (!printJob.isCancelling()) {
    143                     createBlockedNotification(printJob);
    144                 } else {
    145                     createCancellingNotification(printJob);
    146                 }
    147             } break;
    148 
    149             default: {
    150                 if (!printJob.isCancelling()) {
    151                     createPrintingNotification(printJob);
    152                 } else {
    153                     createCancellingNotification(printJob);
    154                 }
    155             } break;
    156         }
    157     }
    158 
    159     /**
    160      * Create an {@link Action} that cancels a {@link PrintJobInfo print job}.
    161      *
    162      * @param printJob The {@link PrintJobInfo print job} to cancel
    163      *
    164      * @return An {@link Action} that will cancel a print job
    165      */
    166     private Action createCancelAction(PrintJobInfo printJob) {
    167         return new Action.Builder(
    168                 Icon.createWithResource(mContext, R.drawable.stat_notify_cancelling),
    169                 mContext.getString(R.string.cancel), createCancelIntent(printJob)).build();
    170     }
    171 
    172     /**
    173      * Create a notification for a print job.
    174      *
    175      * @param printJob the job the notification is for
    176      * @param firstAction the first action shown in the notification
    177      * @param secondAction the second action shown in the notification
    178      */
    179     private void createNotification(@NonNull PrintJobInfo printJob, @Nullable Action firstAction,
    180             @Nullable Action secondAction) {
    181         Notification.Builder builder = new Notification.Builder(mContext)
    182                 .setContentIntent(createContentIntent(printJob.getId()))
    183                 .setSmallIcon(computeNotificationIcon(printJob))
    184                 .setContentTitle(computeNotificationTitle(printJob))
    185                 .setWhen(System.currentTimeMillis())
    186                 .setOngoing(true)
    187                 .setShowWhen(true)
    188                 .setColor(mContext.getColor(
    189                         com.android.internal.R.color.system_notification_accent_color))
    190                 .setGroup(PRINT_JOB_NOTIFICATION_GROUP_KEY);
    191 
    192         if (firstAction != null) {
    193             builder.addAction(firstAction);
    194         }
    195 
    196         if (secondAction != null) {
    197             builder.addAction(secondAction);
    198         }
    199 
    200         if (printJob.getState() == PrintJobInfo.STATE_STARTED
    201                 || printJob.getState() == PrintJobInfo.STATE_QUEUED) {
    202             float progress = printJob.getProgress();
    203             if (progress >= 0) {
    204                 builder.setProgress(Integer.MAX_VALUE, (int) (Integer.MAX_VALUE * progress),
    205                         false);
    206             } else {
    207                 builder.setProgress(Integer.MAX_VALUE, 0, true);
    208             }
    209         }
    210 
    211         CharSequence status = printJob.getStatus(mContext.getPackageManager());
    212         if (status != null) {
    213             builder.setContentText(status);
    214         } else {
    215             builder.setContentText(printJob.getPrinterName());
    216         }
    217 
    218         mNotificationManager.notify(printJob.getId().flattenToString(), 0, builder.build());
    219     }
    220 
    221     private void createPrintingNotification(PrintJobInfo printJob) {
    222         createNotification(printJob, createCancelAction(printJob), null);
    223     }
    224 
    225     private void createFailedNotification(PrintJobInfo printJob) {
    226         Action.Builder restartActionBuilder = new Action.Builder(
    227                 Icon.createWithResource(mContext, R.drawable.ic_restart),
    228                 mContext.getString(R.string.restart), createRestartIntent(printJob.getId()));
    229 
    230         createNotification(printJob, createCancelAction(printJob), restartActionBuilder.build());
    231     }
    232 
    233     private void createBlockedNotification(PrintJobInfo printJob) {
    234         createNotification(printJob, createCancelAction(printJob), null);
    235     }
    236 
    237     private void createCancellingNotification(PrintJobInfo printJob) {
    238         createNotification(printJob, null, null);
    239     }
    240 
    241     private void createStackedNotification(List<PrintJobInfo> printJobs) {
    242         Notification.Builder builder = new Notification.Builder(mContext)
    243                 .setContentIntent(createContentIntent(null))
    244                 .setWhen(System.currentTimeMillis())
    245                 .setOngoing(true)
    246                 .setShowWhen(true)
    247                 .setGroup(PRINT_JOB_NOTIFICATION_GROUP_KEY)
    248                 .setGroupSummary(true);
    249 
    250         final int printJobCount = printJobs.size();
    251 
    252         InboxStyle inboxStyle = new InboxStyle();
    253 
    254         int icon = com.android.internal.R.drawable.ic_print;
    255         for (int i = printJobCount - 1; i>= 0; i--) {
    256             PrintJobInfo printJob = printJobs.get(i);
    257 
    258             inboxStyle.addLine(computeNotificationTitle(printJob));
    259 
    260             // if any print job is in an error state show an error icon for the summary
    261             if (printJob.getState() == PrintJobInfo.STATE_FAILED
    262                     || printJob.getState() == PrintJobInfo.STATE_BLOCKED) {
    263                 icon = com.android.internal.R.drawable.ic_print_error;
    264             }
    265         }
    266 
    267         builder.setSmallIcon(icon);
    268         builder.setLargeIcon(
    269                 ((BitmapDrawable) mContext.getResources().getDrawable(icon, null)).getBitmap());
    270         builder.setNumber(printJobCount);
    271         builder.setStyle(inboxStyle);
    272         builder.setColor(mContext.getColor(
    273                 com.android.internal.R.color.system_notification_accent_color));
    274 
    275         mNotificationManager.notify(PRINT_JOB_NOTIFICATION_SUMMARY, 0, builder.build());
    276     }
    277 
    278     private String computeNotificationTitle(PrintJobInfo printJob) {
    279         switch (printJob.getState()) {
    280             case PrintJobInfo.STATE_FAILED: {
    281                 return mContext.getString(R.string.failed_notification_title_template,
    282                         printJob.getLabel());
    283             }
    284 
    285             case PrintJobInfo.STATE_BLOCKED: {
    286                 if (!printJob.isCancelling()) {
    287                     return mContext.getString(R.string.blocked_notification_title_template,
    288                             printJob.getLabel());
    289                 } else {
    290                     return mContext.getString(
    291                             R.string.cancelling_notification_title_template,
    292                             printJob.getLabel());
    293                 }
    294             }
    295 
    296             default: {
    297                 if (!printJob.isCancelling()) {
    298                     return mContext.getString(R.string.printing_notification_title_template,
    299                             printJob.getLabel());
    300                 } else {
    301                     return mContext.getString(
    302                             R.string.cancelling_notification_title_template,
    303                             printJob.getLabel());
    304                 }
    305             }
    306         }
    307     }
    308 
    309     private PendingIntent createContentIntent(PrintJobId printJobId) {
    310         Intent intent = new Intent(Settings.ACTION_PRINT_SETTINGS);
    311         if (printJobId != null) {
    312             intent.putExtra(EXTRA_PRINT_JOB_ID, printJobId.flattenToString());
    313             intent.setData(Uri.fromParts("printjob", printJobId.flattenToString(), null));
    314         }
    315         return PendingIntent.getActivity(mContext, 0, intent, 0);
    316     }
    317 
    318     private PendingIntent createCancelIntent(PrintJobInfo printJob) {
    319         Intent intent = new Intent(mContext, NotificationBroadcastReceiver.class);
    320         intent.setAction(INTENT_ACTION_CANCEL_PRINTJOB + "_" + printJob.getId().flattenToString());
    321         intent.putExtra(EXTRA_PRINT_JOB_ID, printJob.getId());
    322         return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT);
    323     }
    324 
    325     private PendingIntent createRestartIntent(PrintJobId printJobId) {
    326         Intent intent = new Intent(mContext, NotificationBroadcastReceiver.class);
    327         intent.setAction(INTENT_ACTION_RESTART_PRINTJOB + "_" + printJobId.flattenToString());
    328         intent.putExtra(EXTRA_PRINT_JOB_ID, printJobId);
    329         return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT);
    330     }
    331 
    332     private static boolean shouldNotifyForState(int state) {
    333         switch (state) {
    334             case PrintJobInfo.STATE_QUEUED:
    335             case PrintJobInfo.STATE_STARTED:
    336             case PrintJobInfo.STATE_FAILED:
    337             case PrintJobInfo.STATE_COMPLETED:
    338             case PrintJobInfo.STATE_CANCELED:
    339             case PrintJobInfo.STATE_BLOCKED: {
    340                 return true;
    341             }
    342         }
    343         return false;
    344     }
    345 
    346     private static int computeNotificationIcon(PrintJobInfo printJob) {
    347         switch (printJob.getState()) {
    348             case PrintJobInfo.STATE_FAILED:
    349             case PrintJobInfo.STATE_BLOCKED: {
    350                 return com.android.internal.R.drawable.ic_print_error;
    351             }
    352             default: {
    353                 if (!printJob.isCancelling()) {
    354                     return com.android.internal.R.drawable.ic_print;
    355                 } else {
    356                     return R.drawable.stat_notify_cancelling;
    357                 }
    358             }
    359         }
    360     }
    361 
    362     public static final class NotificationBroadcastReceiver extends BroadcastReceiver {
    363         @SuppressWarnings("hiding")
    364         private static final String LOG_TAG = "NotificationBroadcastReceiver";
    365 
    366         @Override
    367         public void onReceive(Context context, Intent intent) {
    368             String action = intent.getAction();
    369             if (action != null && action.startsWith(INTENT_ACTION_CANCEL_PRINTJOB)) {
    370                 PrintJobId printJobId = intent.getExtras().getParcelable(EXTRA_PRINT_JOB_ID);
    371                 handleCancelPrintJob(context, printJobId);
    372             } else if (action != null && action.startsWith(INTENT_ACTION_RESTART_PRINTJOB)) {
    373                 PrintJobId printJobId = intent.getExtras().getParcelable(EXTRA_PRINT_JOB_ID);
    374                 handleRestartPrintJob(context, printJobId);
    375             }
    376         }
    377 
    378         private void handleCancelPrintJob(final Context context, final PrintJobId printJobId) {
    379             if (DEBUG) {
    380                 Log.i(LOG_TAG, "handleCancelPrintJob() printJobId:" + printJobId);
    381             }
    382 
    383             // Call into the print manager service off the main thread since
    384             // the print manager service may end up binding to the print spooler
    385             // service which binding is handled on the main thread.
    386             PowerManager powerManager = (PowerManager)
    387                     context.getSystemService(Context.POWER_SERVICE);
    388             final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
    389                     LOG_TAG);
    390             wakeLock.acquire();
    391 
    392             new AsyncTask<Void, Void, Void>() {
    393                 @Override
    394                 protected Void doInBackground(Void... params) {
    395                     // We need to request the cancellation to be done by the print
    396                     // manager service since it has to communicate with the managing
    397                     // print service to request the cancellation. Also we need the
    398                     // system service to be bound to the spooler since canceling a
    399                     // print job will trigger persistence of current jobs which is
    400                     // done on another thread and until it finishes the spooler has
    401                     // to be kept around.
    402                     try {
    403                         IPrintManager printManager = IPrintManager.Stub.asInterface(
    404                                 ServiceManager.getService(Context.PRINT_SERVICE));
    405                         printManager.cancelPrintJob(printJobId, PrintManager.APP_ID_ANY,
    406                                 UserHandle.myUserId());
    407                     } catch (RemoteException re) {
    408                         Log.i(LOG_TAG, "Error requesting print job cancellation", re);
    409                     } finally {
    410                         wakeLock.release();
    411                     }
    412                     return null;
    413                 }
    414             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
    415         }
    416 
    417         private void handleRestartPrintJob(final Context context, final PrintJobId printJobId) {
    418             if (DEBUG) {
    419                 Log.i(LOG_TAG, "handleRestartPrintJob() printJobId:" + printJobId);
    420             }
    421 
    422             // Call into the print manager service off the main thread since
    423             // the print manager service may end up binding to the print spooler
    424             // service which binding is handled on the main thread.
    425             PowerManager powerManager = (PowerManager)
    426                     context.getSystemService(Context.POWER_SERVICE);
    427             final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
    428                     LOG_TAG);
    429             wakeLock.acquire();
    430 
    431             new AsyncTask<Void, Void, Void>() {
    432                 @Override
    433                 protected Void doInBackground(Void... params) {
    434                     // We need to request the restart to be done by the print manager
    435                     // service since the latter must be bound to the spooler because
    436                     // restarting a print job will trigger persistence of current jobs
    437                     // which is done on another thread and until it finishes the spooler has
    438                     // to be kept around.
    439                     try {
    440                         IPrintManager printManager = IPrintManager.Stub.asInterface(
    441                                 ServiceManager.getService(Context.PRINT_SERVICE));
    442                         printManager.restartPrintJob(printJobId, PrintManager.APP_ID_ANY,
    443                                 UserHandle.myUserId());
    444                     } catch (RemoteException re) {
    445                         Log.i(LOG_TAG, "Error requesting print job restart", re);
    446                     } finally {
    447                         wakeLock.release();
    448                     }
    449                     return null;
    450                 }
    451             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
    452         }
    453     }
    454 }
    455