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 foo.bar.printservice;
18 
19 import android.annotation.NonNull;
20 import android.app.PendingIntent;
21 import android.content.Intent;
22 import android.graphics.BitmapFactory;
23 import android.graphics.drawable.Icon;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.os.Build;
27 import android.os.CancellationSignal;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.os.ParcelFileDescriptor;
32 import android.print.PrintAttributes;
33 import android.print.PrintAttributes.Margins;
34 import android.print.PrintAttributes.MediaSize;
35 import android.print.PrintAttributes.Resolution;
36 import android.print.PrintJobId;
37 import android.print.PrinterCapabilitiesInfo;
38 import android.print.PrinterId;
39 import android.print.PrinterInfo;
40 import android.printservice.CustomPrinterIconCallback;
41 import android.printservice.PrintJob;
42 import android.printservice.PrintService;
43 import android.printservice.PrinterDiscoverySession;
44 import android.util.ArrayMap;
45 import android.util.Log;
46 import com.android.internal.os.SomeArgs;
47 
48 import java.io.BufferedInputStream;
49 import java.io.BufferedOutputStream;
50 import java.io.File;
51 import java.io.FileInputStream;
52 import java.io.FileOutputStream;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.OutputStream;
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Map;
59 
60 public class MyPrintService extends PrintService {
61 
62     private static final String LOG_TAG = "MyPrintService";
63 
64     private static final long STANDARD_DELAY_MILLIS = 10000000;
65 
66     static final String INTENT_EXTRA_ACTION_TYPE = "INTENT_EXTRA_ACTION_TYPE";
67     static final String INTENT_EXTRA_PRINT_JOB_ID = "INTENT_EXTRA_PRINT_JOB_ID";
68 
69     static final int ACTION_TYPE_ON_PRINT_JOB_PENDING = 1;
70     static final int ACTION_TYPE_ON_REQUEST_CANCEL_PRINT_JOB = 2;
71 
72     private static final Object sLock = new Object();
73 
74     private static MyPrintService sInstance;
75 
76     private Handler mHandler;
77 
78     private AsyncTask<ParcelFileDescriptor, Void, Void> mFakePrintTask;
79 
80     private FakePrinterDiscoverySession mSession;
81 
82     private final Map<PrintJobId, PrintJob> mProcessedPrintJobs =
83             new ArrayMap<PrintJobId, PrintJob>();
84 
peekInstance()85     public static MyPrintService peekInstance() {
86         synchronized (sLock) {
87             return sInstance;
88         }
89     }
90 
91     @Override
onConnected()92     protected void onConnected() {
93         Log.i(LOG_TAG, "#onConnected()");
94         mHandler = new MyHandler(getMainLooper());
95         synchronized (sLock) {
96             sInstance = this;
97         }
98     }
99 
100     @Override
onDisconnected()101     protected void onDisconnected() {
102         Log.i(LOG_TAG, "#onDisconnected()");
103         if (mSession != null) {
104             mSession.cancellAddingFakePrinters();
105         }
106         synchronized (sLock) {
107             sInstance = null;
108         }
109     }
110 
111     @Override
onCreatePrinterDiscoverySession()112     protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
113         Log.i(LOG_TAG, "#onCreatePrinterDiscoverySession()");
114         return new FakePrinterDiscoverySession();
115     }
116 
117     @Override
onRequestCancelPrintJob(final PrintJob printJob)118     protected void onRequestCancelPrintJob(final PrintJob printJob) {
119         Log.i(LOG_TAG, "#onRequestCancelPrintJob()");
120         mProcessedPrintJobs.put(printJob.getId(), printJob);
121         Intent intent = new Intent(this, MyDialogActivity.class);
122         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
123         intent.putExtra(INTENT_EXTRA_PRINT_JOB_ID, printJob.getId());
124         intent.putExtra(INTENT_EXTRA_ACTION_TYPE, ACTION_TYPE_ON_REQUEST_CANCEL_PRINT_JOB);
125         startActivity(intent);
126     }
127 
128     @Override
onPrintJobQueued(final PrintJob printJob)129     public void onPrintJobQueued(final PrintJob printJob) {
130         Log.i(LOG_TAG, "#onPrintJobQueued()");
131         mProcessedPrintJobs.put(printJob.getId(), printJob);
132         if (printJob.isQueued()) {
133             printJob.start();
134         }
135 
136         Intent intent = new Intent(this, MyDialogActivity.class);
137         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
138         intent.putExtra(INTENT_EXTRA_PRINT_JOB_ID, printJob.getId());
139         intent.putExtra(INTENT_EXTRA_ACTION_TYPE, ACTION_TYPE_ON_PRINT_JOB_PENDING);
140         startActivity(intent);
141     }
142 
handleRequestCancelPrintJob(PrintJobId printJobId)143     void handleRequestCancelPrintJob(PrintJobId printJobId) {
144         PrintJob printJob = mProcessedPrintJobs.get(printJobId);
145         if (printJob == null) {
146             return;
147         }
148         mProcessedPrintJobs.remove(printJobId);
149         if (printJob.isQueued() || printJob.isStarted() || printJob.isBlocked()) {
150             mHandler.removeMessages(MyHandler.MSG_HANDLE_DO_PRINT_JOB);
151             mHandler.removeMessages(MyHandler.MSG_HANDLE_FAIL_PRINT_JOB);
152             printJob.cancel();
153         }
154     }
155 
handleFailPrintJobDelayed(PrintJobId printJobId)156     void handleFailPrintJobDelayed(PrintJobId printJobId) {
157         Message message = mHandler.obtainMessage(
158                 MyHandler.MSG_HANDLE_FAIL_PRINT_JOB, printJobId);
159         mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS);
160     }
161 
handleFailPrintJob(PrintJobId printJobId)162     void handleFailPrintJob(PrintJobId printJobId) {
163         PrintJob printJob = mProcessedPrintJobs.get(printJobId);
164         if (printJob == null) {
165             return;
166         }
167         mProcessedPrintJobs.remove(printJobId);
168         if (printJob.isQueued() || printJob.isStarted()) {
169             printJob.fail(getString(R.string.fail_reason));
170         }
171     }
172 
handleBlockPrintJobDelayed(PrintJobId printJobId)173     void handleBlockPrintJobDelayed(PrintJobId printJobId) {
174         Message message = mHandler.obtainMessage(
175                 MyHandler.MSG_HANDLE_BLOCK_PRINT_JOB, printJobId);
176         mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS);
177     }
178 
handleBlockPrintJob(PrintJobId printJobId)179     void handleBlockPrintJob(PrintJobId printJobId) {
180         final PrintJob printJob = mProcessedPrintJobs.get(printJobId);
181         if (printJob == null) {
182             return;
183         }
184 
185         if (printJob.isStarted()) {
186             printJob.block("Gimme some rest, dude");
187         }
188     }
189 
handleBlockAndDelayedUnblockPrintJob(PrintJobId printJobId)190     void handleBlockAndDelayedUnblockPrintJob(PrintJobId printJobId) {
191         handleBlockPrintJob(printJobId);
192 
193         Message message = mHandler.obtainMessage(
194                 MyHandler.MSG_HANDLE_UNBLOCK_PRINT_JOB, printJobId);
195         mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS);
196     }
197 
handleUnblockPrintJob(PrintJobId printJobId)198     void handleUnblockPrintJob(PrintJobId printJobId) {
199         final PrintJob printJob = mProcessedPrintJobs.get(printJobId);
200         if (printJob == null) {
201             return;
202         }
203 
204         if (printJob.isBlocked()) {
205             printJob.start();
206         }
207     }
208 
handleQueuedPrintJobDelayed(PrintJobId printJobId)209     void handleQueuedPrintJobDelayed(PrintJobId printJobId) {
210         final PrintJob printJob = mProcessedPrintJobs.get(printJobId);
211         if (printJob == null) {
212             return;
213         }
214 
215         if (printJob.isQueued()) {
216             printJob.start();
217         }
218         Message message = mHandler.obtainMessage(
219                 MyHandler.MSG_HANDLE_DO_PRINT_JOB, printJobId);
220         mHandler.sendMessageDelayed(message, STANDARD_DELAY_MILLIS);
221     }
222 
223     /**
224      * Pretend that the print job has progressed.
225      *
226      * @param printJobId ID of the print job to progress
227      * @param progress the new value to progress to
228      */
handlePrintJobProgress(@onNull PrintJobId printJobId, int progress)229     void handlePrintJobProgress(@NonNull PrintJobId printJobId, int progress) {
230         final PrintJob printJob = mProcessedPrintJobs.get(printJobId);
231         if (printJob == null) {
232             return;
233         }
234 
235         if (printJob.isQueued()) {
236             printJob.start();
237         }
238 
239         if (progress == 100) {
240             handleQueuedPrintJob(printJobId);
241         } else {
242             if (Build.VERSION.SDK_INT >= 24) {
243                 printJob.setProgress((float) progress / 100);
244                 printJob.setStatus("Printing progress: " + progress + "%");
245             }
246 
247             Message message = mHandler.obtainMessage(
248                     MyHandler.MSG_HANDLE_PRINT_JOB_PROGRESS, progress + 10, 0, printJobId);
249             mHandler.sendMessageDelayed(message, 1000);
250         }
251     }
252 
handleQueuedPrintJob(PrintJobId printJobId)253     void handleQueuedPrintJob(PrintJobId printJobId) {
254         final PrintJob printJob = mProcessedPrintJobs.get(printJobId);
255         if (printJob == null) {
256             return;
257         }
258 
259         if (printJob.isQueued()) {
260             printJob.start();
261         }
262 
263         try {
264             final File file = File.createTempFile(this.getClass().getSimpleName(), ".pdf",
265                     getFilesDir());
266             mFakePrintTask = new AsyncTask<ParcelFileDescriptor, Void, Void>() {
267                 @Override
268                 protected Void doInBackground(ParcelFileDescriptor... params) {
269                     InputStream in = new BufferedInputStream(new FileInputStream(
270                             params[0].getFileDescriptor()));
271                     OutputStream out = null;
272                     try {
273                         out = new BufferedOutputStream(new FileOutputStream(file));
274                         final byte[] buffer = new byte[8192];
275                         while (true) {
276                             if (isCancelled()) {
277                                 break;
278                             }
279                             final int readByteCount = in.read(buffer);
280                             if (readByteCount < 0) {
281                                 break;
282                             }
283                             out.write(buffer, 0, readByteCount);
284                         }
285                     } catch (IOException ioe) {
286                         throw new RuntimeException(ioe);
287                     } finally {
288                         if (in != null) {
289                             try {
290                                 in.close();
291                             } catch (IOException ioe) {
292                                 /* ignore */
293                             }
294                         }
295                         if (out != null) {
296                             try {
297                                 out.close();
298                             } catch (IOException ioe) {
299                                 /* ignore */
300                             }
301                         }
302                         if (isCancelled()) {
303                             file.delete();
304                         }
305                     }
306                     return null;
307                 }
308 
309                 @Override
310                 protected void onPostExecute(Void result) {
311                     if (printJob.isStarted()) {
312                         printJob.complete();
313                     }
314 
315                     file.setReadable(true, false);
316 
317                     // Quick and dirty to show the file - use a content provider instead.
318                     Intent intent = new Intent(Intent.ACTION_VIEW);
319                     intent.setDataAndType(Uri.fromFile(file), "application/pdf");
320                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
321                     startActivity(intent, null);
322 
323                     mFakePrintTask = null;
324                 }
325 
326                 @Override
327                 protected void onCancelled(Void result) {
328                     if (printJob.isStarted()) {
329                         printJob.cancel();
330                     }
331                 }
332             };
333             mFakePrintTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
334                     printJob.getDocument().getData());
335         } catch (IOException e) {
336             Log.e(LOG_TAG, "Could not create temporary file: %s", e);
337             return;
338         }
339     }
340 
341     private final class MyHandler extends Handler {
342         public static final int MSG_HANDLE_DO_PRINT_JOB = 1;
343         public static final int MSG_HANDLE_FAIL_PRINT_JOB = 2;
344         public static final int MSG_HANDLE_BLOCK_PRINT_JOB = 3;
345         public static final int MSG_HANDLE_UNBLOCK_PRINT_JOB = 4;
346         public static final int MSG_HANDLE_PRINT_JOB_PROGRESS = 5;
347 
MyHandler(Looper looper)348         public MyHandler(Looper looper) {
349             super(looper);
350         }
351 
352         @Override
handleMessage(Message message)353         public void handleMessage(Message message) {
354             switch (message.what) {
355                 case MSG_HANDLE_DO_PRINT_JOB: {
356                     PrintJobId printJobId = (PrintJobId) message.obj;
357                     handleQueuedPrintJob(printJobId);
358                 } break;
359 
360                 case MSG_HANDLE_FAIL_PRINT_JOB: {
361                     PrintJobId printJobId = (PrintJobId) message.obj;
362                     handleFailPrintJob(printJobId);
363                 } break;
364 
365                 case MSG_HANDLE_BLOCK_PRINT_JOB: {
366                     PrintJobId printJobId = (PrintJobId) message.obj;
367                     handleBlockPrintJob(printJobId);
368                 } break;
369 
370                 case MSG_HANDLE_UNBLOCK_PRINT_JOB: {
371                     PrintJobId printJobId = (PrintJobId) message.obj;
372                     handleUnblockPrintJob(printJobId);
373                 } break;
374 
375                 case MSG_HANDLE_PRINT_JOB_PROGRESS: {
376                     PrintJobId printJobId = (PrintJobId) message.obj;
377                     handlePrintJobProgress(printJobId, message.arg1);
378                 } break;
379             }
380         }
381     }
382 
383     private final class FakePrinterDiscoverySession extends  PrinterDiscoverySession {
384         private final Handler mSesionHandler = new SessionHandler(getMainLooper());
385 
386         private final List<PrinterInfo> mFakePrinters = new ArrayList<PrinterInfo>();
387 
FakePrinterDiscoverySession()388         public FakePrinterDiscoverySession() {
389             for (int i = 0; i < 6; i++) {
390                 String name = "Printer " + i;
391 
392                 PrinterInfo.Builder builder = new PrinterInfo.Builder(generatePrinterId(name), name,
393                         (i == 1 || i == 2) ? PrinterInfo.STATUS_UNAVAILABLE
394                                 : PrinterInfo.STATUS_IDLE);
395 
396                 if (i != 3) {
397                     builder.setDescription("Launch a menu to select behavior.");
398                 }
399 
400                 if (i != 4) {
401                     if (Build.VERSION.SDK_INT >= 24) {
402                         builder.setIconResourceId(R.drawable.printer);
403                     }
404                 }
405 
406                 if (i % 2 == 0) {
407                     if (Build.VERSION.SDK_INT >= 24) {
408                         Intent infoIntent = new Intent(MyPrintService.this, InfoActivity.class);
409                         infoIntent.putExtra(InfoActivity.PRINTER_NAME, name);
410 
411                         PendingIntent infoPendingIntent = PendingIntent.getActivity(
412                                 getApplicationContext(),
413                                 i, infoIntent, PendingIntent.FLAG_UPDATE_CURRENT);
414 
415                         builder.setInfoIntent(infoPendingIntent);
416                     }
417                 }
418 
419                 if (i == 5) {
420                     if (Build.VERSION.SDK_INT >= 24) {
421                         builder.setHasCustomPrinterIcon(true);
422                     }
423                 }
424 
425                 mFakePrinters.add(builder.build());
426             }
427         }
428 
429         @Override
onDestroy()430         public void onDestroy() {
431             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onDestroy()");
432             mSesionHandler.removeMessages(SessionHandler.MSG_ADD_FIRST_BATCH_FAKE_PRINTERS);
433         }
434 
435         @Override
onStartPrinterDiscovery(List<PrinterId> priorityList)436         public void onStartPrinterDiscovery(List<PrinterId> priorityList) {
437             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStartPrinterDiscovery()");
438             Message message1 = mSesionHandler.obtainMessage(
439                     SessionHandler.MSG_ADD_FIRST_BATCH_FAKE_PRINTERS, this);
440             mSesionHandler.sendMessageDelayed(message1, 0);
441         }
442 
443         @Override
onStopPrinterDiscovery()444         public void onStopPrinterDiscovery() {
445             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStopPrinterDiscovery()");
446             cancellAddingFakePrinters();
447         }
448 
449         @Override
onStartPrinterStateTracking(PrinterId printerId)450         public void onStartPrinterStateTracking(PrinterId printerId) {
451             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStartPrinterStateTracking()");
452 
453             final int printerCount = mFakePrinters.size();
454             for (int i = printerCount - 1; i >= 0; i--) {
455                 PrinterInfo printer = mFakePrinters.remove(i);
456 
457                 if (printer.getId().equals(printerId)) {
458                     PrinterCapabilitiesInfo.Builder b = new PrinterCapabilitiesInfo.Builder(
459                             printerId)
460                                     .setMinMargins(new Margins(200, 200, 200, 200))
461                                     .addMediaSize(MediaSize.ISO_A4, true)
462                                     .addMediaSize(MediaSize.NA_GOVT_LETTER, false)
463                                     .addMediaSize(MediaSize.JPN_YOU4, false)
464                                     .addResolution(new Resolution("R1", getString(
465                                             R.string.resolution_200x200), 200, 200), false)
466                                     .addResolution(new Resolution("R2", getString(
467                                             R.string.resolution_300x300), 300, 300), true)
468                                     .setColorModes(PrintAttributes.COLOR_MODE_COLOR
469                                             | PrintAttributes.COLOR_MODE_MONOCHROME,
470                                             PrintAttributes.COLOR_MODE_MONOCHROME);
471 
472                     if (Build.VERSION.SDK_INT >= 23) {
473                         b.setDuplexModes(PrintAttributes.DUPLEX_MODE_LONG_EDGE
474                                         | PrintAttributes.DUPLEX_MODE_NONE,
475                                 PrintAttributes.DUPLEX_MODE_LONG_EDGE);
476                     }
477 
478                     PrinterCapabilitiesInfo capabilities = b.build();
479 
480                     printer = new PrinterInfo.Builder(printer)
481                             .setCapabilities(capabilities)
482                             .build();
483                 }
484 
485                 mFakePrinters.add(printer);
486             }
487 
488             addPrinters(mFakePrinters);
489         }
490 
491         @Override
onRequestCustomPrinterIcon(final PrinterId printerId, final CancellationSignal cancellationSignal, final CustomPrinterIconCallback callbacks)492         public void onRequestCustomPrinterIcon(final PrinterId printerId,
493                 final CancellationSignal cancellationSignal,
494                 final CustomPrinterIconCallback callbacks) {
495             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onRequestCustomPrinterIcon() " + printerId);
496 
497             SomeArgs args = SomeArgs.obtain();
498             args.arg1 = cancellationSignal;
499             args.arg2 = callbacks;
500 
501             Message msg = mSesionHandler.obtainMessage(
502                     SessionHandler.MSG_SUPPLY_CUSTOM_PRINTER_ICON, args);
503 
504             // Pretend the bitmap icon takes 5 seconds to load
505             mSesionHandler.sendMessageDelayed(msg, 5000);
506         }
507 
508         @Override
onValidatePrinters(List<PrinterId> printerIds)509         public void onValidatePrinters(List<PrinterId> printerIds) {
510             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onValidatePrinters() " + printerIds);
511         }
512 
513         @Override
onStopPrinterStateTracking(PrinterId printerId)514         public void onStopPrinterStateTracking(PrinterId printerId) {
515             Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStopPrinterStateTracking()");
516         }
517 
addFirstBatchFakePrinters()518         private void addFirstBatchFakePrinters() {
519             List<PrinterInfo> printers = mFakePrinters.subList(0, mFakePrinters.size());
520             addPrinters(printers);
521         }
522 
cancellAddingFakePrinters()523         private void cancellAddingFakePrinters() {
524             mSesionHandler.removeMessages(SessionHandler.MSG_ADD_FIRST_BATCH_FAKE_PRINTERS);
525         }
526 
527         final class SessionHandler extends Handler {
528             public static final int MSG_ADD_FIRST_BATCH_FAKE_PRINTERS = 1;
529             public static final int MSG_SUPPLY_CUSTOM_PRINTER_ICON = 2;
530 
SessionHandler(Looper looper)531             public SessionHandler(Looper looper) {
532                 super(looper);
533             }
534 
535             @Override
handleMessage(Message message)536             public void handleMessage(Message message) {
537                 switch (message.what) {
538                     case MSG_ADD_FIRST_BATCH_FAKE_PRINTERS: {
539                         addFirstBatchFakePrinters();
540                     } break;
541                     case MSG_SUPPLY_CUSTOM_PRINTER_ICON: {
542                         SomeArgs args = (SomeArgs) message.obj;
543                         CancellationSignal cancellationSignal = (CancellationSignal) args.arg1;
544                         CustomPrinterIconCallback callbacks = (CustomPrinterIconCallback) args.arg2;
545                         args.recycle();
546 
547                         if (!cancellationSignal.isCanceled()) {
548                             callbacks.onCustomPrinterIconLoaded(Icon.createWithBitmap(
549                                     BitmapFactory.decodeResource(getResources(),
550                                     R.raw.red_printer)));
551                         }
552                     } break;
553                 }
554             }
555         }
556     }
557 }
558