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