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