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