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