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 foo.bar.printservice;
18
19import android.annotation.NonNull;
20import android.app.PendingIntent;
21import android.content.Intent;
22import android.graphics.BitmapFactory;
23import android.graphics.drawable.Icon;
24import android.net.Uri;
25import android.os.AsyncTask;
26import android.os.Build;
27import android.os.CancellationSignal;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.Message;
31import android.os.ParcelFileDescriptor;
32import android.print.PrintAttributes;
33import android.print.PrintAttributes.Margins;
34import android.print.PrintAttributes.MediaSize;
35import android.print.PrintAttributes.Resolution;
36import android.print.PrintJobId;
37import android.print.PrinterCapabilitiesInfo;
38import android.print.PrinterId;
39import android.print.PrinterInfo;
40import android.printservice.CustomPrinterIconCallback;
41import android.printservice.PrintJob;
42import android.printservice.PrintService;
43import android.printservice.PrinterDiscoverySession;
44import android.util.ArrayMap;
45import android.util.Log;
46import com.android.internal.os.SomeArgs;
47
48import java.io.BufferedInputStream;
49import java.io.BufferedOutputStream;
50import java.io.File;
51import java.io.FileInputStream;
52import java.io.FileOutputStream;
53import java.io.IOException;
54import java.io.InputStream;
55import java.io.OutputStream;
56import java.util.ArrayList;
57import java.util.List;
58import java.util.Map;
59
60public 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
83    public static MyPrintService peekInstance() {
84        synchronized (sLock) {
85            return sInstance;
86        }
87    }
88
89    @Override
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
99    protected void onDisconnected() {
100        Log.i(LOG_TAG, "#onDisconnected()");
101        synchronized (sLock) {
102            sInstance = null;
103        }
104    }
105
106    @Override
107    protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
108        Log.i(LOG_TAG, "#onCreatePrinterDiscoverySession()");
109        return new FakePrinterDiscoverySession();
110    }
111
112    @Override
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
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
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
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
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
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
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
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
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
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     */
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
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
340        public MyHandler(Looper looper) {
341            super(looper);
342        }
343
344        @Override
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
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
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
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
436        public void onStopPrinterDiscovery() {
437            Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStopPrinterDiscovery()");
438            cancellAddingFakePrinters();
439        }
440
441        @Override
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
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
501        public void onValidatePrinters(List<PrinterId> printerIds) {
502            Log.i(LOG_TAG, "FakePrinterDiscoverySession#onValidatePrinters() " + printerIds);
503        }
504
505        @Override
506        public void onStopPrinterStateTracking(PrinterId printerId) {
507            Log.i(LOG_TAG, "FakePrinterDiscoverySession#onStopPrinterStateTracking()");
508        }
509
510        private void addFirstBatchFakePrinters() {
511            List<PrinterInfo> printers = mFakePrinters.subList(0, mFakePrinters.size());
512            addPrinters(printers);
513        }
514
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
523            public SessionHandler(Looper looper) {
524                super(looper);
525            }
526
527            @Override
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