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 NotificationController(Context context)76 public NotificationController(Context context) { 77 mContext = context; 78 mNotificationManager = (NotificationManager) 79 mContext.getSystemService(Context.NOTIFICATION_SERVICE); 80 mNotifications = new ArraySet<>(0); 81 } 82 onUpdateNotifications(List<PrintJobInfo> printJobs)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 */ updateNotifications(List<PrintJobInfo> printJobs)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 createSimpleNotification(PrintJobInfo printJob)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 */ createCancelAction(PrintJobInfo printJob)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 */ createNotification(@onNull PrintJobInfo printJob, @Nullable Action firstAction, @Nullable Action secondAction)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 createPrintingNotification(PrintJobInfo printJob)221 private void createPrintingNotification(PrintJobInfo printJob) { 222 createNotification(printJob, createCancelAction(printJob), null); 223 } 224 createFailedNotification(PrintJobInfo printJob)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 createBlockedNotification(PrintJobInfo printJob)233 private void createBlockedNotification(PrintJobInfo printJob) { 234 createNotification(printJob, createCancelAction(printJob), null); 235 } 236 createCancellingNotification(PrintJobInfo printJob)237 private void createCancellingNotification(PrintJobInfo printJob) { 238 createNotification(printJob, null, null); 239 } 240 createStackedNotification(List<PrintJobInfo> printJobs)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 computeNotificationTitle(PrintJobInfo printJob)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 createContentIntent(PrintJobId printJobId)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 createCancelIntent(PrintJobInfo printJob)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 createRestartIntent(PrintJobId printJobId)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 shouldNotifyForState(int state)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 computeNotificationIcon(PrintJobInfo printJob)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 onReceive(Context context, Intent intent)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 handleCancelPrintJob(final Context context, final PrintJobId printJobId)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 handleRestartPrintJob(final Context context, final PrintJobId printJobId)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