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.Bundle;
22 import android.print.PrintJobId;
23 import android.printservice.PrintJob;
24 import android.util.Log;
25 
26 import com.android.bips.discovery.ConnectionListener;
27 import com.android.bips.discovery.DiscoveredPrinter;
28 import com.android.bips.discovery.MdnsDiscovery;
29 import com.android.bips.ipp.Backend;
30 import com.android.bips.ipp.CapabilitiesCache;
31 import com.android.bips.ipp.CertificateStore;
32 import com.android.bips.ipp.JobStatus;
33 import com.android.bips.jni.BackendConstants;
34 import com.android.bips.jni.LocalPrinterCapabilities;
35 import com.android.bips.p2p.P2pPrinterConnection;
36 import com.android.bips.p2p.P2pUtils;
37 
38 import java.util.ArrayList;
39 import java.util.StringJoiner;
40 import java.util.function.Consumer;
41 
42 /**
43  * Manage the process of delivering a print job
44  */
45 class LocalPrintJob implements MdnsDiscovery.Listener, ConnectionListener,
46         CapabilitiesCache.OnLocalPrinterCapabilities {
47     private static final String TAG = LocalPrintJob.class.getSimpleName();
48     private static final boolean DEBUG = false;
49     private static final String IPP_SCHEME = "ipp";
50     private static final String IPPS_SCHEME = "ipps";
51 
52     /** Maximum time to wait to find a printer before failing the job */
53     private static final int DISCOVERY_TIMEOUT = 2 * 60 * 1000;
54 
55     // Internal job states
56     private static final int STATE_INIT = 0;
57     private static final int STATE_DISCOVERY = 1;
58     private static final int STATE_CAPABILITIES = 2;
59     private static final int STATE_DELIVERING = 3;
60     private static final int STATE_SECURITY = 4;
61     private static final int STATE_CANCEL = 5;
62     private static final int STATE_DONE = 6;
63 
64     private final BuiltInPrintService mPrintService;
65     private final PrintJob mPrintJob;
66     private final Backend mBackend;
67 
68     private int mState;
69     private Consumer<LocalPrintJob> mCompleteConsumer;
70     private Uri mPath;
71     private DelayedAction mDiscoveryTimeout;
72     private P2pPrinterConnection mConnection;
73     private LocalPrinterCapabilities mCapabilities;
74     private CertificateStore mCertificateStore;
75     private long mStartTime;
76     private ArrayList<String> mBlockedReasons = new ArrayList<>();
77 
78     /**
79      * Construct the object; use {@link #start(Consumer)} to begin job processing.
80      */
LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob)81     LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) {
82         mPrintService = printService;
83         mBackend = backend;
84         mPrintJob = printJob;
85         mCertificateStore = mPrintService.getCertificateStore();
86         mState = STATE_INIT;
87 
88         // Tell the job it is blocked (until start())
89         mPrintJob.start();
90         mPrintJob.block(printService.getString(R.string.waiting_to_send));
91     }
92 
93     /**
94      * Begin the process of delivering the job. Internally, discovers the target printer,
95      * obtains its capabilities, delivers the job to the printer, and waits for job completion.
96      *
97      * @param callback Callback to be issued when job processing is complete
98      */
start(Consumer<LocalPrintJob> callback)99     void start(Consumer<LocalPrintJob> callback) {
100         mStartTime = System.currentTimeMillis();
101         // TODO: Log job attempted event here using getJobAttemptedBundle()
102         if (DEBUG) Log.d(TAG, "start() " + mPrintJob);
103         if (mState != STATE_INIT) {
104             Log.w(TAG, "Invalid start state " + mState);
105             return;
106         }
107         mPrintJob.start();
108 
109         // Acquire a lock so that WiFi isn't put to sleep while we send the job
110         mPrintService.lockWifi();
111 
112         mState = STATE_DISCOVERY;
113         mCompleteConsumer = callback;
114         mDiscoveryTimeout = mPrintService.delay(DISCOVERY_TIMEOUT, () -> {
115             if (DEBUG) Log.d(TAG, "Discovery timeout");
116             if (mState == STATE_DISCOVERY) {
117                 finish(false, mPrintService.getString(R.string.printer_offline));
118             }
119         });
120 
121         mPrintService.getDiscovery().start(this);
122     }
123 
124     /**
125      * Restart the job if possible.
126      */
restart()127     void restart() {
128         if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState);
129         if (mState == STATE_SECURITY) {
130             mCapabilities.certificate = mCertificateStore.get(mCapabilities.uuid);
131             deliver();
132         }
133     }
134 
cancel()135     void cancel() {
136         if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState);
137 
138         switch (mState) {
139             case STATE_DISCOVERY:
140             case STATE_CAPABILITIES:
141             case STATE_SECURITY:
142                 // Cancel immediately
143                 mState = STATE_CANCEL;
144                 finish(false, null);
145                 break;
146 
147             case STATE_DELIVERING:
148                 // Request cancel and wait for completion
149                 mState = STATE_CANCEL;
150                 mBackend.cancel();
151                 break;
152         }
153         Bundle bundle = getJobCompletedAnalyticsBundle(BackendConstants.JOB_DONE_CANCELLED);
154         bundle.putString(BackendConstants.PARAM_ERROR_MESSAGES, getStringifiedBlockedReasons());
155         // TODO: Log job completed event here with the above bundle
156     }
157 
getPrintJobId()158     PrintJobId getPrintJobId() {
159         return mPrintJob.getId();
160     }
161 
162     @Override
onPrinterFound(DiscoveredPrinter printer)163     public void onPrinterFound(DiscoveredPrinter printer) {
164         if (mState != STATE_DISCOVERY) {
165             return;
166         }
167         if (!printer.getId(mPrintService).equals(mPrintJob.getInfo().getPrinterId())) {
168             return;
169         }
170 
171         if (DEBUG) Log.d(TAG, "onPrinterFound() " + printer.name + " state=" + mState);
172 
173         if (P2pUtils.isP2p(printer)) {
174             // Launch a P2P connection attempt
175             mConnection = new P2pPrinterConnection(mPrintService, printer, this);
176             return;
177         }
178 
179         if (P2pUtils.isOnConnectedInterface(mPrintService, printer) && mConnection == null) {
180             // Hold the P2P connection up during printing
181             mConnection = new P2pPrinterConnection(mPrintService, printer, this);
182         }
183 
184         // We have a good path so stop discovering and get capabilities
185         mPrintService.getDiscovery().stop(this);
186         mState = STATE_CAPABILITIES;
187         mPath = printer.path;
188         // Upgrade to IPPS path if present
189         for (Uri path : printer.paths) {
190             if (IPPS_SCHEME.equals(path.getScheme())) {
191                 mPath = path;
192                 break;
193             }
194         }
195 
196         mPrintService.getCapabilitiesCache().request(printer, true, this);
197     }
198 
199     @Override
onPrinterLost(DiscoveredPrinter printer)200     public void onPrinterLost(DiscoveredPrinter printer) {
201         // Ignore (the capability request, if any, will fail)
202     }
203 
204     @Override
onConnectionComplete(DiscoveredPrinter printer)205     public void onConnectionComplete(DiscoveredPrinter printer) {
206         // Ignore late connection events
207         if (mState != STATE_DISCOVERY) {
208             return;
209         }
210 
211         if (printer == null) {
212             finish(false, mPrintService.getString(R.string.failed_printer_connection));
213         } else if (mPrintJob.isBlocked()) {
214             mPrintJob.start();
215         }
216     }
217 
218     @Override
onConnectionDelayed(boolean delayed)219     public void onConnectionDelayed(boolean delayed) {
220         if (DEBUG) Log.d(TAG, "onConnectionDelayed " + delayed);
221 
222         // Ignore late events
223         if (mState != STATE_DISCOVERY) {
224             return;
225         }
226 
227         if (delayed) {
228             mPrintJob.block(mPrintService.getString(R.string.connect_hint_text));
229         } else {
230             // Remove block message
231             mPrintJob.start();
232         }
233     }
234 
getPrintJob()235     PrintJob getPrintJob() {
236         return mPrintJob;
237     }
238 
239     @Override
onCapabilities(LocalPrinterCapabilities capabilities)240     public void onCapabilities(LocalPrinterCapabilities capabilities) {
241         if (DEBUG) Log.d(TAG, "Capabilities for " + mPath + " are " + capabilities);
242         if (mState != STATE_CAPABILITIES) {
243             return;
244         }
245 
246         if (capabilities == null) {
247             finish(false, mPrintService.getString(R.string.printer_offline));
248         } else {
249             if (DEBUG) Log.d(TAG, "Starting backend print of " + mPrintJob);
250             if (mDiscoveryTimeout != null) {
251                 mDiscoveryTimeout.cancel();
252             }
253             mCapabilities = capabilities;
254             deliver();
255         }
256     }
257 
deliver()258     private void deliver() {
259         // Upgrade to IPPS if necessary
260         Uri newUri = Uri.parse(mCapabilities.path);
261         if (IPPS_SCHEME.equals(newUri.getScheme()) && newUri.getPort() > 0 &&
262             IPP_SCHEME.equals(mPath.getScheme())) {
263             mPath = mPath.buildUpon().scheme(IPPS_SCHEME).encodedAuthority(mPath.getHost() +
264                 ":" + newUri.getPort()).build();
265         }
266 
267         if (DEBUG) Log.d(TAG, "deliver() to " + mPath);
268         if (mCapabilities.certificate != null && !IPPS_SCHEME.equals(mPath.getScheme())) {
269             mState = STATE_SECURITY;
270             mPrintJob.block(mPrintService.getString(R.string.printer_not_encrypted));
271             mPrintService.notifyCertificateChange(mCapabilities.name,
272                     mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, null);
273         } else {
274             mState = STATE_DELIVERING;
275             mPrintJob.start();
276             mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus);
277         }
278     }
279 
handleJobStatus(JobStatus jobStatus)280     private void handleJobStatus(JobStatus jobStatus) {
281         if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus);
282 
283         byte[] certificate = jobStatus.getCertificate();
284         if (certificate != null && mCapabilities != null) {
285             // If there is no certificate, record this one
286             if (mCertificateStore.get(mCapabilities.uuid) == null) {
287                 if (DEBUG) Log.d(TAG, "Recording new certificate");
288                 mCertificateStore.put(mCapabilities.uuid, certificate);
289             }
290         }
291 
292         mBlockedReasons.addAll(jobStatus.getBlockedReasons());
293 
294         switch (jobStatus.getJobState()) {
295             case BackendConstants.JOB_STATE_DONE:
296                 Bundle bundle = getJobCompletedAnalyticsBundle(jobStatus.getJobResult());
297 
298                 switch (jobStatus.getJobResult()) {
299                     case BackendConstants.JOB_DONE_OK:
300                         finish(true, null);
301                         break;
302                     case BackendConstants.JOB_DONE_CANCELLED:
303                         mState = STATE_CANCEL;
304                         finish(false, null);
305                         bundle.putString(
306                                 BackendConstants.PARAM_ERROR_MESSAGES,
307                                 getStringifiedBlockedReasons());
308                         break;
309                     case BackendConstants.JOB_DONE_CORRUPT:
310                         finish(false, mPrintService.getString(R.string.unreadable_input));
311                         bundle.putString(
312                                 BackendConstants.PARAM_ERROR_MESSAGES,
313                                 getStringifiedBlockedReasons());
314                         break;
315                     case BackendConstants.JOB_DONE_BAD_CERTIFICATE:
316                         handleBadCertificate(jobStatus);
317                         break;
318                     default:
319                         // Job failed
320                         finish(false, null);
321                         bundle.putString(
322                                 BackendConstants.PARAM_ERROR_MESSAGES,
323                                 getStringifiedBlockedReasons());
324                         break;
325                 }
326                 // TODO: Log JobCompleted analytic with the bundle here
327                 break;
328 
329             case BackendConstants.JOB_STATE_BLOCKED:
330                 if (mState == STATE_CANCEL) {
331                     return;
332                 }
333                 int blockedId = jobStatus.getBlockedReasonId();
334                 blockedId = (blockedId == 0) ? R.string.printer_check : blockedId;
335                 String blockedReason = mPrintService.getString(blockedId);
336                 mPrintJob.block(blockedReason);
337                 break;
338 
339             case BackendConstants.JOB_STATE_RUNNING:
340                 if (mState == STATE_CANCEL) {
341                     return;
342                 }
343                 mPrintJob.start();
344                 break;
345         }
346     }
347 
handleBadCertificate(JobStatus jobStatus)348     private void handleBadCertificate(JobStatus jobStatus) {
349         byte[] certificate = jobStatus.getCertificate();
350 
351         if (certificate == null) {
352             mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate));
353         } else {
354             if (DEBUG) Log.d(TAG, "Certificate change detected.");
355             mState = STATE_SECURITY;
356             mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate));
357             mPrintService.notifyCertificateChange(mCapabilities.name,
358                     mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate);
359         }
360     }
361 
362     /**
363      * Terminate the job, issuing appropriate notifications.
364      *
365      * @param success true if the printer reported successful job completion
366      * @param error   reason for job failure if known
367      */
finish(boolean success, String error)368     private void finish(boolean success, String error) {
369         if (DEBUG) Log.d(TAG, "finish() success=" + success + ", error=" + error);
370         mPrintService.getDiscovery().stop(this);
371         if (mDiscoveryTimeout != null) {
372             mDiscoveryTimeout.cancel();
373         }
374         if (mConnection != null) {
375             mConnection.close();
376         }
377         mPrintService.unlockWifi();
378         mBackend.closeDocument();
379         if (success) {
380             // Job must not be blocked before completion
381             mPrintJob.start();
382             mPrintJob.complete();
383         } else if (mState == STATE_CANCEL) {
384             mPrintJob.cancel();
385         } else {
386             mPrintJob.fail(error);
387         }
388         mState = STATE_DONE;
389         mCompleteConsumer.accept(LocalPrintJob.this);
390     }
391 
392     /**
393      * Get stringified blocked reasons delimited by '|'
394      * @return delimited string of blocked reasons
395      */
getStringifiedBlockedReasons()396     private String getStringifiedBlockedReasons() {
397         StringJoiner reasons = new StringJoiner("|");
398         for (String reason: mBlockedReasons) {
399             reasons.add(reason);
400         }
401         return reasons.toString();
402     }
403 
404     /**
405      * Get the job completed analytics bundle
406      *
407      * @param result result of the job
408      * @return analytics bundle
409      */
getJobCompletedAnalyticsBundle(String result)410     private Bundle getJobCompletedAnalyticsBundle(String result) {
411         Bundle bundle = new Bundle();
412         bundle.putString(BackendConstants.PARAM_JOB_ID, mPrintJob.getId().toString());
413         bundle.putLong(BackendConstants.PARAM_DATE_TIME, System.currentTimeMillis());
414         // TODO: Add real location
415         bundle.putString(BackendConstants.PARAM_LOCATION, "United States");
416         // TODO: Add real user id
417         bundle.putString(BackendConstants.PARAM_USER_ID, "userid");
418         bundle.putString(BackendConstants.PARAM_RESULT, result);
419         bundle.putLong(
420                 BackendConstants.PARAM_ELAPSED_TIME_ALL, System.currentTimeMillis() - mStartTime);
421         return bundle;
422     }
423 
424     /**
425      * Get the job started analytics bundle
426      *
427      * @return analytics bundle
428      */
getJobAttemptedAnalyticsBundle()429     private Bundle getJobAttemptedAnalyticsBundle() {
430         Bundle bundle = new Bundle();
431         bundle.putString(BackendConstants.PARAM_JOB_ID, mPrintJob.getId().toString());
432         bundle.putLong(BackendConstants.PARAM_DATE_TIME, System.currentTimeMillis());
433         // TODO: Add real location
434         bundle.putString(BackendConstants.PARAM_LOCATION, "United States");
435         bundle.putInt(
436                 BackendConstants.PARAM_JOB_PAGES,
437                 mPrintJob.getInfo().getCopies() * mPrintJob.getDocument().getInfo().getPageCount());
438         // TODO: Add real user id
439         bundle.putString(BackendConstants.PARAM_USER_ID, "userid");
440         // TODO: Determine whether the print job came from share to BIPS or from print system
441         bundle.putString(BackendConstants.PARAM_SOURCE_PATH, "ShareToBips || PrintSystem");
442         return bundle;
443     }
444 }
445