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