2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.printspooler.model;
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;
46 import com.android.printspooler.R;
48 import java.util.ArrayList;
49 import java.util.List;
52 * This class is responsible for updating the print notifications
53 * based on print job state transitions.
55 final class NotificationController {
56 public static final boolean DEBUG = false;
58 public static final String LOG_TAG = "NotificationController";
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";
63 private static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID";
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";
68 private final Context mContext;
69 private final NotificationManager mNotificationManager;
72 * Mapping from printJobIds to their notification Ids.
74 private final ArraySet<PrintJobId> mNotifications;
76 public NotificationController(Context context) {
78 mNotificationManager = (NotificationManager)
79 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
80 mNotifications = new ArraySet<>(0);
83 public void onUpdateNotifications(List<PrintJobInfo> printJobs) {
84 List<PrintJobInfo> notifyPrintJobs = new ArrayList<>();
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);
94 updateNotifications(notifyPrintJobs);
98 * Update notifications for the given print jobs, remove all other notifications.
100 * @param printJobs The print job that we want to create notifications for.
102 private void updateNotifications(List<PrintJobInfo> printJobs) {
103 ArraySet<PrintJobId> removedPrintJobs = new ArraySet<>(mNotifications);
105 final int numPrintJobs = printJobs.size();
107 // Create summary notification
108 if (numPrintJobs > 1) {
109 createStackedNotification(printJobs);
111 mNotificationManager.cancel(PRINT_JOB_NOTIFICATION_SUMMARY, 0);
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();
119 removedPrintJobs.remove(printJobId);
120 mNotifications.add(printJobId);
122 createSimpleNotification(printJob);
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);
130 mNotificationManager.cancel(removedPrintJob.flattenToString(), 0);
131 mNotifications.remove(removedPrintJob);
135 private void createSimpleNotification(PrintJobInfo printJob) {
136 switch (printJob.getState()) {
137 case PrintJobInfo.STATE_FAILED: {
138 createFailedNotification(printJob);
141 case PrintJobInfo.STATE_BLOCKED: {
142 if (!printJob.isCancelling()) {
143 createBlockedNotification(printJob);
145 createCancellingNotification(printJob);
150 if (!printJob.isCancelling()) {
151 createPrintingNotification(printJob);
153 createCancellingNotification(printJob);
160 * Create an {@link Action} that cancels a {@link PrintJobInfo print job}.
162 * @param printJob The {@link PrintJobInfo print job} to cancel
164 * @return An {@link Action} that will cancel a print job
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();
173 * Create a notification for a print job.
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
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())
188 .setColor(mContext.getColor(
189 com.android.internal.R.color.system_notification_accent_color))
190 .setGroup(PRINT_JOB_NOTIFICATION_GROUP_KEY);
192 if (firstAction != null) {
193 builder.addAction(firstAction);
196 if (secondAction != null) {
197 builder.addAction(secondAction);
200 if (printJob.getState() == PrintJobInfo.STATE_STARTED
201 || printJob.getState() == PrintJobInfo.STATE_QUEUED) {
202 float progress = printJob.getProgress();
204 builder.setProgress(Integer.MAX_VALUE, (int) (Integer.MAX_VALUE * progress),
207 builder.setProgress(Integer.MAX_VALUE, 0, true);
211 CharSequence status = printJob.getStatus(mContext.getPackageManager());
212 if (status != null) {
213 builder.setContentText(status);
215 builder.setContentText(printJob.getPrinterName());
218 mNotificationManager.notify(printJob.getId().flattenToString(), 0, builder.build());
221 private void createPrintingNotification(PrintJobInfo printJob) {
222 createNotification(printJob, createCancelAction(printJob), null);
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()));
230 createNotification(printJob, createCancelAction(printJob), restartActionBuilder.build());
233 private void createBlockedNotification(PrintJobInfo printJob) {
234 createNotification(printJob, createCancelAction(printJob), null);
237 private void createCancellingNotification(PrintJobInfo printJob) {
238 createNotification(printJob, null, null);
241 private void createStackedNotification(List<PrintJobInfo> printJobs) {
242 Notification.Builder builder = new Notification.Builder(mContext)
243 .setContentIntent(createContentIntent(null))
244 .setWhen(System.currentTimeMillis())
247 .setGroup(PRINT_JOB_NOTIFICATION_GROUP_KEY)
248 .setGroupSummary(true);
250 final int printJobCount = printJobs.size();
252 InboxStyle inboxStyle = new InboxStyle();
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);
258 inboxStyle.addLine(computeNotificationTitle(printJob));
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;
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));
275 mNotificationManager.notify(PRINT_JOB_NOTIFICATION_SUMMARY, 0, builder.build());
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());
285 case PrintJobInfo.STATE_BLOCKED: {
286 if (!printJob.isCancelling()) {
287 return mContext.getString(R.string.blocked_notification_title_template,
288 printJob.getLabel());
290 return mContext.getString(
291 R.string.cancelling_notification_title_template,
292 printJob.getLabel());
297 if (!printJob.isCancelling()) {
298 return mContext.getString(R.string.printing_notification_title_template,
299 printJob.getLabel());
301 return mContext.getString(
302 R.string.cancelling_notification_title_template,
303 printJob.getLabel());
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));
315 return PendingIntent.getActivity(mContext, 0, intent, 0);
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);
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);
332 private static boolean shouldNotifyForState(int 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: {
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;
353 if (!printJob.isCancelling()) {
354 return com.android.internal.R.drawable.ic_print;
356 return R.drawable.stat_notify_cancelling;
362 public static final class NotificationBroadcastReceiver extends BroadcastReceiver {
363 @SuppressWarnings("hiding")
364 private static final String LOG_TAG = "NotificationBroadcastReceiver";
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);
378 private void handleCancelPrintJob(final Context context, final PrintJobId printJobId) {
380 Log.i(LOG_TAG, "handleCancelPrintJob() printJobId:" + printJobId);
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,
392 new AsyncTask<Void, Void, Void>() {
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.
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);
414 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
417 private void handleRestartPrintJob(final Context context, final PrintJobId printJobId) {
419 Log.i(LOG_TAG, "handleRestartPrintJob() printJobId:" + printJobId);
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,
431 new AsyncTask<Void, Void, Void>() {
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.
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);
451 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);