1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  * Copyright (C) 2016 Mopria Alliance, Inc.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.bips;
19 
20 import android.net.Uri;
21 import android.os.Handler;
22 import android.print.PrintJobId;
23 import android.printservice.PrintJob;
24 import android.util.Log;
25 
26 import com.android.bips.discovery.DiscoveredPrinter;
27 import com.android.bips.discovery.MdnsDiscovery;
28 import com.android.bips.ipp.Backend;
29 import com.android.bips.ipp.JobStatus;
30 import com.android.bips.jni.BackendConstants;
31 import com.android.bips.jni.LocalPrinterCapabilities;
32 
33 import java.util.function.Consumer;
34 
35 /**
36  * Manage the process of delivering a print job
37  */
38 class LocalPrintJob implements MdnsDiscovery.Listener {
39     private static final String TAG = LocalPrintJob.class.getSimpleName();
40     private static final boolean DEBUG = false;
41 
42     /** Maximum time to wait to find a printer before failing the job */
43     private static final int DISCOVERY_TIMEOUT = 30 * 1000;
44 
45     // Internal job states
46     private static final int STATE_INIT = 0;
47     private static final int STATE_DISCOVERY = 1;
48     private static final int STATE_DELIVERING = 2;
49     private static final int STATE_CANCEL = 3;
50     private static final int STATE_DONE = 4;
51 
52     private final BuiltInPrintService mPrintService;
53     private final PrintJob mPrintJob;
54     private final Backend mBackend;
55     private final Handler mMainHandler;
56 
57     private int mState;
58     private Consumer<LocalPrintJob> mCompleteConsumer;
59     private Uri mPath;
60 
61     /**
62      * Construct the object; use {@link #start(Consumer)} to begin job processing.
63      */
LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob)64     LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) {
65         mPrintService = printService;
66         mBackend = backend;
67         mPrintJob = printJob;
68         mMainHandler = new Handler(printService.getMainLooper());
69         mState = STATE_INIT;
70 
71         // Tell the job it is blocked (until start())
72         mPrintJob.start();
73         mPrintJob.block(printService.getString(R.string.waiting_to_send));
74     }
75 
76     /**
77      * Begin the process of delivering the job. Internally, discovers the target printer,
78      * obtains its capabilities, delivers the job to the printer, and waits for job completion.
79      *
80      * @param callback Callback to be issued when job processing is complete
81      */
start(Consumer<LocalPrintJob> callback)82     void start(Consumer<LocalPrintJob> callback) {
83         if (DEBUG) Log.d(TAG, "start() " + mPrintJob);
84         if (mState != STATE_INIT) {
85             Log.w(TAG, "Invalid start state " + mState);
86             return;
87         }
88         mPrintJob.start();
89 
90         // Acquire a lock so that WiFi isn't put to sleep while we send the job
91         mPrintService.lockWifi();
92 
93         mState = STATE_DISCOVERY;
94         mCompleteConsumer = callback;
95         mPrintService.getDiscovery().start(this);
96 
97         mMainHandler.postDelayed(() -> {
98             if (DEBUG) Log.d(TAG, "Discovery timeout");
99             if (mState == STATE_DISCOVERY) {
100                 mPrintService.getDiscovery().stop(LocalPrintJob.this);
101                 finish(false, mPrintService.getString(R.string.printer_offline));
102             }
103         }, DISCOVERY_TIMEOUT);
104     }
105 
cancel()106     void cancel() {
107         if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState);
108 
109         switch (mState) {
110             case STATE_DISCOVERY:
111                 // Cancel immediately
112                 mPrintService.getDiscovery().stop(this);
113                 mState = STATE_CANCEL;
114                 finish(false, null);
115                 break;
116 
117             case STATE_DELIVERING:
118                 // Request cancel and wait for completion
119                 mState = STATE_CANCEL;
120                 mBackend.cancel();
121                 break;
122         }
123     }
124 
getPrintJobId()125     PrintJobId getPrintJobId() {
126         return mPrintJob.getId();
127     }
128 
129     @Override
onPrinterFound(DiscoveredPrinter printer)130     public void onPrinterFound(DiscoveredPrinter printer) {
131         if (mState != STATE_DISCOVERY) return;
132         if (printer.getId(mPrintService).equals(mPrintJob.getInfo().getPrinterId())) {
133             if (DEBUG) Log.d(TAG, "onPrinterFound() " + printer.name + " state=" + mState);
134             mPath = printer.path;
135             mPrintService.getCapabilitiesCache().request(printer, true,
136                     this::handleCapabilities);
137             mPrintService.getDiscovery().stop(this);
138         }
139     }
140 
141     @Override
onPrinterLost(DiscoveredPrinter printer)142     public void onPrinterLost(DiscoveredPrinter printer) {
143         // Ignore (the capability request, if any, will fail)
144     }
145 
getPrintJob()146     PrintJob getPrintJob() {
147         return mPrintJob;
148     }
149 
handleCapabilities(LocalPrinterCapabilities capabilities)150     private void handleCapabilities(LocalPrinterCapabilities capabilities) {
151         if (DEBUG) Log.d(TAG, "Capabilities for " + mPath + " are " + capabilities);
152         if (mState != STATE_DISCOVERY) return;
153 
154         if (capabilities == null) {
155             finish(false, mPrintService.getString(R.string.printer_offline));
156         } else {
157             if (DEBUG) Log.d(TAG, "Starting backend print of " + mPrintJob);
158             mMainHandler.removeCallbacksAndMessages(null);
159             mState = STATE_DELIVERING;
160             mBackend.print(mPath, mPrintJob, capabilities, this::handleJobStatus);
161         }
162     }
163 
handleJobStatus(JobStatus jobStatus)164     private void handleJobStatus(JobStatus jobStatus) {
165         if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus);
166         switch (jobStatus.getJobState()) {
167             case BackendConstants.JOB_STATE_DONE:
168                 switch (jobStatus.getJobResult()) {
169                     case BackendConstants.JOB_DONE_OK:
170                         finish(true, null);
171                         break;
172                     case BackendConstants.JOB_DONE_CANCELLED:
173                         mState = STATE_CANCEL;
174                         finish(false, null);
175                         break;
176                     case BackendConstants.JOB_DONE_CORRUPT:
177                         finish(false, mPrintService.getString(R.string.unreadable_input));
178                         break;
179                     default:
180                         // Job failed
181                         finish(false, null);
182                         break;
183                 }
184                 break;
185 
186             case BackendConstants.JOB_STATE_BLOCKED:
187                 if (mState == STATE_CANCEL) return;
188                 int blockedId = jobStatus.getBlockedReasonId();
189                 blockedId = (blockedId == 0) ? R.string.printer_check : blockedId;
190                 String blockedReason = mPrintService.getString(blockedId);
191                 mPrintJob.block(blockedReason);
192                 break;
193 
194             case BackendConstants.JOB_STATE_RUNNING:
195                 if (mState == STATE_CANCEL) return;
196                 mPrintJob.start();
197                 break;
198         }
199     }
200 
201     /**
202      * Terminate the job, issuing appropriate notifications.
203      *
204      * @param success true if the printer reported successful job completion
205      * @param error   reason for job failure if known
206      */
finish(boolean success, String error)207     private void finish(boolean success, String error) {
208         mPrintService.unlockWifi();
209         mBackend.closeDocument();
210         mMainHandler.removeCallbacksAndMessages(null);
211         if (success) {
212             // Job must not be blocked before completion
213             mPrintJob.start();
214             mPrintJob.complete();
215         } else if (mState == STATE_CANCEL) {
216             mPrintJob.cancel();
217         } else {
218             mPrintJob.fail(error);
219         }
220         mState = STATE_DONE;
221         mCompleteConsumer.accept(LocalPrintJob.this);
222     }
223 }