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