• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.print.PrintJobId;
22 import android.printservice.PrintJob;
23 import android.util.Log;
24 
25 import com.android.bips.discovery.ConnectionListener;
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.CapabilitiesCache;
30 import com.android.bips.ipp.CertificateStore;
31 import com.android.bips.ipp.JobStatus;
32 import com.android.bips.jni.BackendConstants;
33 import com.android.bips.jni.LocalPrinterCapabilities;
34 import com.android.bips.p2p.P2pPrinterConnection;
35 import com.android.bips.p2p.P2pUtils;
36 
37 import java.util.function.Consumer;
38 
39 /**
40  * Manage the process of delivering a print job
41  */
42 class LocalPrintJob implements MdnsDiscovery.Listener, ConnectionListener,
43         CapabilitiesCache.OnLocalPrinterCapabilities {
44     private static final String TAG = LocalPrintJob.class.getSimpleName();
45     private static final boolean DEBUG = false;
46     private static final String IPPS_SCHEME = "ipps";
47 
48     /** Maximum time to wait to find a printer before failing the job */
49     private static final int DISCOVERY_TIMEOUT = 2 * 60 * 1000;
50 
51     // Internal job states
52     private static final int STATE_INIT = 0;
53     private static final int STATE_DISCOVERY = 1;
54     private static final int STATE_CAPABILITIES = 2;
55     private static final int STATE_DELIVERING = 3;
56     private static final int STATE_SECURITY = 4;
57     private static final int STATE_CANCEL = 5;
58     private static final int STATE_DONE = 6;
59 
60     private final BuiltInPrintService mPrintService;
61     private final PrintJob mPrintJob;
62     private final Backend mBackend;
63 
64     private int mState;
65     private Consumer<LocalPrintJob> mCompleteConsumer;
66     private Uri mPath;
67     private DelayedAction mDiscoveryTimeout;
68     private P2pPrinterConnection mConnection;
69     private LocalPrinterCapabilities mCapabilities;
70     private CertificateStore mCertificateStore;
71 
72     /**
73      * Construct the object; use {@link #start(Consumer)} to begin job processing.
74      */
LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob)75     LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) {
76         mPrintService = printService;
77         mBackend = backend;
78         mPrintJob = printJob;
79         mCertificateStore = mPrintService.getCertificateStore();
80         mState = STATE_INIT;
81 
82         // Tell the job it is blocked (until start())
83         mPrintJob.start();
84         mPrintJob.block(printService.getString(R.string.waiting_to_send));
85     }
86 
87     /**
88      * Begin the process of delivering the job. Internally, discovers the target printer,
89      * obtains its capabilities, delivers the job to the printer, and waits for job completion.
90      *
91      * @param callback Callback to be issued when job processing is complete
92      */
start(Consumer<LocalPrintJob> callback)93     void start(Consumer<LocalPrintJob> callback) {
94         if (DEBUG) Log.d(TAG, "start() " + mPrintJob);
95         if (mState != STATE_INIT) {
96             Log.w(TAG, "Invalid start state " + mState);
97             return;
98         }
99         mPrintJob.start();
100 
101         // Acquire a lock so that WiFi isn't put to sleep while we send the job
102         mPrintService.lockWifi();
103 
104         mState = STATE_DISCOVERY;
105         mCompleteConsumer = callback;
106         mDiscoveryTimeout = mPrintService.delay(DISCOVERY_TIMEOUT, () -> {
107             if (DEBUG) Log.d(TAG, "Discovery timeout");
108             if (mState == STATE_DISCOVERY) {
109                 finish(false, mPrintService.getString(R.string.printer_offline));
110             }
111         });
112 
113         mPrintService.getDiscovery().start(this);
114     }
115 
116     /**
117      * Restart the job if possible.
118      */
restart()119     void restart() {
120         if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState);
121         if (mState == STATE_SECURITY) {
122             mCapabilities.certificate = mCertificateStore.get(mCapabilities.uuid);
123             deliver();
124         }
125     }
126 
cancel()127     void cancel() {
128         if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState);
129 
130         switch (mState) {
131             case STATE_DISCOVERY:
132             case STATE_CAPABILITIES:
133             case STATE_SECURITY:
134                 // Cancel immediately
135                 mState = STATE_CANCEL;
136                 finish(false, null);
137                 break;
138 
139             case STATE_DELIVERING:
140                 // Request cancel and wait for completion
141                 mState = STATE_CANCEL;
142                 mBackend.cancel();
143                 break;
144         }
145     }
146 
getPrintJobId()147     PrintJobId getPrintJobId() {
148         return mPrintJob.getId();
149     }
150 
151     @Override
onPrinterFound(DiscoveredPrinter printer)152     public void onPrinterFound(DiscoveredPrinter printer) {
153         if (mState != STATE_DISCOVERY) {
154             return;
155         }
156         if (!printer.getId(mPrintService).equals(mPrintJob.getInfo().getPrinterId())) {
157             return;
158         }
159 
160         if (DEBUG) Log.d(TAG, "onPrinterFound() " + printer.name + " state=" + mState);
161 
162         if (P2pUtils.isP2p(printer)) {
163             // Launch a P2P connection attempt
164             mConnection = new P2pPrinterConnection(mPrintService, printer, this);
165             return;
166         }
167 
168         if (P2pUtils.isOnConnectedInterface(mPrintService, printer) && mConnection == null) {
169             // Hold the P2P connection up during printing
170             mConnection = new P2pPrinterConnection(mPrintService, printer, this);
171         }
172 
173         // We have a good path so stop discovering and get capabilities
174         mPrintService.getDiscovery().stop(this);
175         mState = STATE_CAPABILITIES;
176         mPath = printer.path;
177         // Upgrade to IPPS path if present
178         for (Uri path : printer.paths) {
179             if (IPPS_SCHEME.equals(path.getScheme())) {
180                 mPath = path;
181                 break;
182             }
183         }
184 
185         mPrintService.getCapabilitiesCache().request(printer, true, this);
186     }
187 
188     @Override
onPrinterLost(DiscoveredPrinter printer)189     public void onPrinterLost(DiscoveredPrinter printer) {
190         // Ignore (the capability request, if any, will fail)
191     }
192 
193     @Override
onConnectionComplete(DiscoveredPrinter printer)194     public void onConnectionComplete(DiscoveredPrinter printer) {
195         // Ignore late connection events
196         if (mState != STATE_DISCOVERY) {
197             return;
198         }
199 
200         if (printer == null) {
201             finish(false, mPrintService.getString(R.string.failed_printer_connection));
202         } else if (mPrintJob.isBlocked()) {
203             mPrintJob.start();
204         }
205     }
206 
207     @Override
onConnectionDelayed(boolean delayed)208     public void onConnectionDelayed(boolean delayed) {
209         if (DEBUG) Log.d(TAG, "onConnectionDelayed " + delayed);
210 
211         // Ignore late events
212         if (mState != STATE_DISCOVERY) {
213             return;
214         }
215 
216         if (delayed) {
217             mPrintJob.block(mPrintService.getString(R.string.connect_hint_text));
218         } else {
219             // Remove block message
220             mPrintJob.start();
221         }
222     }
223 
getPrintJob()224     PrintJob getPrintJob() {
225         return mPrintJob;
226     }
227 
228     @Override
onCapabilities(LocalPrinterCapabilities capabilities)229     public void onCapabilities(LocalPrinterCapabilities capabilities) {
230         if (DEBUG) Log.d(TAG, "Capabilities for " + mPath + " are " + capabilities);
231         if (mState != STATE_CAPABILITIES) {
232             return;
233         }
234 
235         if (capabilities == null) {
236             finish(false, mPrintService.getString(R.string.printer_offline));
237         } else {
238             if (DEBUG) Log.d(TAG, "Starting backend print of " + mPrintJob);
239             if (mDiscoveryTimeout != null) {
240                 mDiscoveryTimeout.cancel();
241             }
242             mCapabilities = capabilities;
243             deliver();
244         }
245     }
246 
deliver()247     private void deliver() {
248         if (mCapabilities.certificate != null && !IPPS_SCHEME.equals(mPath.getScheme())) {
249             mState = STATE_SECURITY;
250             mPrintJob.block(mPrintService.getString(R.string.printer_not_encrypted));
251             mPrintService.notifyCertificateChange(mCapabilities.name,
252                     mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, null);
253         } else {
254             mState = STATE_DELIVERING;
255             mPrintJob.start();
256             mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus);
257         }
258     }
259 
handleJobStatus(JobStatus jobStatus)260     private void handleJobStatus(JobStatus jobStatus) {
261         if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus);
262 
263         byte[] certificate = jobStatus.getCertificate();
264         if (certificate != null && mCapabilities != null) {
265             // If there is no certificate, record this one
266             if (mCertificateStore.get(mCapabilities.uuid) == null) {
267                 if (DEBUG) Log.d(TAG, "Recording new certificate");
268                 mCertificateStore.put(mCapabilities.uuid, certificate);
269             }
270         }
271 
272         switch (jobStatus.getJobState()) {
273             case BackendConstants.JOB_STATE_DONE:
274                 switch (jobStatus.getJobResult()) {
275                     case BackendConstants.JOB_DONE_OK:
276                         finish(true, null);
277                         break;
278                     case BackendConstants.JOB_DONE_CANCELLED:
279                         mState = STATE_CANCEL;
280                         finish(false, null);
281                         break;
282                     case BackendConstants.JOB_DONE_CORRUPT:
283                         finish(false, mPrintService.getString(R.string.unreadable_input));
284                         break;
285                     default:
286                         // Job failed
287                         if (jobStatus.getBlockedReasonId() == R.string.printer_bad_certificate) {
288                             handleBadCertificate(jobStatus);
289                         } else {
290                             finish(false, null);
291                         }
292                         break;
293                 }
294                 break;
295 
296             case BackendConstants.JOB_STATE_BLOCKED:
297                 if (mState == STATE_CANCEL) {
298                     return;
299                 }
300                 int blockedId = jobStatus.getBlockedReasonId();
301                 blockedId = (blockedId == 0) ? R.string.printer_check : blockedId;
302                 String blockedReason = mPrintService.getString(blockedId);
303                 mPrintJob.block(blockedReason);
304                 break;
305 
306             case BackendConstants.JOB_STATE_RUNNING:
307                 if (mState == STATE_CANCEL) {
308                     return;
309                 }
310                 mPrintJob.start();
311                 break;
312         }
313     }
314 
handleBadCertificate(JobStatus jobStatus)315     private void handleBadCertificate(JobStatus jobStatus) {
316         byte[] certificate = jobStatus.getCertificate();
317 
318         if (certificate == null) {
319             mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate));
320         } else {
321             if (DEBUG) Log.d(TAG, "Certificate change detected.");
322             mState = STATE_SECURITY;
323             mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate));
324             mPrintService.notifyCertificateChange(mCapabilities.name,
325                     mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate);
326         }
327     }
328 
329     /**
330      * Terminate the job, issuing appropriate notifications.
331      *
332      * @param success true if the printer reported successful job completion
333      * @param error   reason for job failure if known
334      */
finish(boolean success, String error)335     private void finish(boolean success, String error) {
336         if (DEBUG) Log.d(TAG, "finish() success=" + success + ", error=" + error);
337         mPrintService.getDiscovery().stop(this);
338         if (mDiscoveryTimeout != null) {
339             mDiscoveryTimeout.cancel();
340         }
341         if (mConnection != null) {
342             mConnection.close();
343         }
344         mPrintService.unlockWifi();
345         mBackend.closeDocument();
346         if (success) {
347             // Job must not be blocked before completion
348             mPrintJob.start();
349             mPrintJob.complete();
350         } else if (mState == STATE_CANCEL) {
351             mPrintJob.cancel();
352         } else {
353             mPrintJob.fail(error);
354         }
355         mState = STATE_DONE;
356         mCompleteConsumer.accept(LocalPrintJob.this);
357     }
358 }
359