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