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 NotificationController(Context context)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 onUpdateNotifications(List<PrintJobInfo> printJobs)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 */ updateNotifications(List<PrintJobInfo> printJobs)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 createSimpleNotification(PrintJobInfo printJob)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 */ createCancelAction(PrintJobInfo printJob)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 */ createNotification(@onNull PrintJobInfo printJob, @Nullable Action firstAction, @Nullable Action secondAction)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 createPrintingNotification(PrintJobInfo printJob)222 private void createPrintingNotification(PrintJobInfo printJob) { 223 createNotification(printJob, createCancelAction(printJob), null); 224 } 225 createFailedNotification(PrintJobInfo printJob)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 createBlockedNotification(PrintJobInfo printJob)234 private void createBlockedNotification(PrintJobInfo printJob) { 235 createNotification(printJob, createCancelAction(printJob), null); 236 } 237 createCancellingNotification(PrintJobInfo printJob)238 private void createCancellingNotification(PrintJobInfo printJob) { 239 createNotification(printJob, null, null); 240 } 241 computeNotificationTitle(PrintJobInfo printJob)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 createContentIntent(PrintJobId printJobId)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 createCancelIntent(PrintJobInfo printJob)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 createRestartIntent(PrintJobId printJobId)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 shouldNotifyForState(int state)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 computeNotificationIcon(PrintJobInfo printJob)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 computeChannel(PrintJobInfo printJob)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 onReceive(Context context, Intent intent)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 handleCancelPrintJob(final Context context, final PrintJobId printJobId)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 handleRestartPrintJob(final Context context, final PrintJobId printJobId)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