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 
NotificationController(Context context)63     public NotificationController(Context context) {
64         mContext = context;
65         mNotificationManager = (NotificationManager)
66                 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
67     }
68 
onUpdateNotifications(List<PrintJobInfo> printJobs)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 
updateNotification(List<PrintJobInfo> printJobs)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 
createSimpleNotification(PrintJobInfo printJob)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 
createPrintingNotification(PrintJobInfo printJob)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.getColor(
129                         com.android.internal.R.color.system_notification_accent_color));
130         mNotificationManager.notify(0, builder.build());
131     }
132 
createFailedNotification(PrintJobInfo printJob)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.getColor(
147                         com.android.internal.R.color.system_notification_accent_color));
148         mNotificationManager.notify(0, builder.build());
149     }
150 
createBlockedNotification(PrintJobInfo printJob)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.getColor(
163                         com.android.internal.R.color.system_notification_accent_color));
164            mNotificationManager.notify(0, builder.build());
165     }
166 
createCancellingNotification(PrintJobInfo printJob)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.getColor(
177                         com.android.internal.R.color.system_notification_accent_color));
178         mNotificationManager.notify(0, builder.build());
179     }
180 
createStackedNotification(List<PrintJobInfo> printJobs)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.getColor(
210                 com.android.internal.R.color.system_notification_accent_color));
211 
212         mNotificationManager.notify(0, builder.build());
213     }
214 
computeNotificationTitle(PrintJobInfo printJob)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 
removeNotification()246     private void removeNotification() {
247         mNotificationManager.cancel(0);
248     }
249 
createContentIntent(PrintJobId printJobId)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 
createCancelIntent(PrintJobInfo printJob)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 
createRestartIntent(PrintJobId printJobId)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 
shouldNotifyForState(int state)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 
computeNotificationIcon(PrintJobInfo printJob)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
onReceive(Context context, Intent intent)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 
handleCancelPrintJob(final Context context, final PrintJobId printJobId)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 
handleRestartPrintJob(final Context context, final PrintJobId printJobId)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