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 */ 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 */ 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 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 125 PrintJobId getPrintJobId() { 126 return mPrintJob.getId(); 127 } 128 129 @Override 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 142 public void onPrinterLost(DiscoveredPrinter printer) { 143 // Ignore (the capability request, if any, will fail) 144 } 145 146 PrintJob getPrintJob() { 147 return mPrintJob; 148 } 149 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 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 */ 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 }