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
17package com.android.printspooler.model;
18
19import android.app.Notification;
20import android.app.Notification.InboxStyle;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.graphics.drawable.BitmapDrawable;
27import android.net.Uri;
28import android.os.AsyncTask;
29import android.os.PowerManager;
30import android.os.PowerManager.WakeLock;
31import android.os.RemoteException;
32import android.os.ServiceManager;
33import android.os.UserHandle;
34import android.print.IPrintManager;
35import android.print.PrintJobId;
36import android.print.PrintJobInfo;
37import android.print.PrintManager;
38import android.provider.Settings;
39import android.util.Log;
40
41import com.android.printspooler.R;
42
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * This class is responsible for updating the print notifications
48 * based on print job state transitions.
49 */
50final 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