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.ipp; 19 20 import android.content.Context; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.net.Uri; 24 import android.os.AsyncTask; 25 import android.os.Build; 26 import android.os.Handler; 27 import android.printservice.PrintJob; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import com.android.bips.R; 32 import com.android.bips.jni.BackendConstants; 33 import com.android.bips.jni.JobCallback; 34 import com.android.bips.jni.JobCallbackParams; 35 import com.android.bips.jni.LocalJobParams; 36 import com.android.bips.jni.LocalPrinterCapabilities; 37 import com.android.bips.jni.PdfRender; 38 import com.android.bips.jni.PrinterStatusMonitor; 39 import com.android.bips.util.FileUtils; 40 41 import java.io.File; 42 import java.util.Locale; 43 import java.util.function.Consumer; 44 45 public class Backend implements JobCallback { 46 private static final String TAG = Backend.class.getSimpleName(); 47 private static final boolean DEBUG = false; 48 49 static final String TEMP_JOB_FOLDER = "jobs"; 50 51 // Error codes strictly to be in negative number 52 static final int ERROR_FILE = -1; 53 static final int ERROR_CANCEL = -2; 54 static final int ERROR_UNKNOWN = -3; 55 56 private static final String VERSION_UNKNOWN = "(unknown)"; 57 58 private final Handler mMainHandler; 59 private final Context mContext; 60 private JobStatus mCurrentJobStatus; 61 private Consumer<JobStatus> mJobStatusListener; 62 private AsyncTask<Void, Void, Integer> mStartTask; 63 Backend(Context context)64 public Backend(Context context) { 65 if (DEBUG) Log.d(TAG, "Backend()"); 66 67 mContext = context; 68 mMainHandler = new Handler(context.getMainLooper()); 69 PdfRender.getInstance(mContext); 70 71 // Load required JNI libraries 72 System.loadLibrary(BackendConstants.WPRINT_LIBRARY_PREFIX); 73 74 // Create and initialize JNI layer 75 nativeInit(this, context.getApplicationInfo().dataDir, Build.VERSION.SDK_INT); 76 nativeSetSourceInfo(context.getString(R.string.app_name).toLowerCase(Locale.US), 77 getApplicationVersion(context).toLowerCase(Locale.US), 78 BackendConstants.WPRINT_APPLICATION_ID.toLowerCase(Locale.US)); 79 } 80 81 /** Return the current application version or VERSION_UNKNOWN */ getApplicationVersion(Context context)82 private String getApplicationVersion(Context context) { 83 try { 84 PackageInfo packageInfo = context.getPackageManager() 85 .getPackageInfo(context.getPackageName(), 0); 86 return packageInfo.versionName; 87 } catch (PackageManager.NameNotFoundException e) { 88 return VERSION_UNKNOWN; 89 } 90 } 91 92 /** Asynchronously get printer capabilities, returning results or null to a callback */ getCapabilities(Uri uri, long timeout, boolean highPriority, final Consumer<LocalPrinterCapabilities> capabilitiesConsumer)93 public GetCapabilitiesTask getCapabilities(Uri uri, long timeout, boolean highPriority, 94 final Consumer<LocalPrinterCapabilities> capabilitiesConsumer) { 95 if (DEBUG) Log.d(TAG, "getCapabilities()"); 96 97 GetCapabilitiesTask task = new GetCapabilitiesTask(this, uri, timeout, highPriority) { 98 @Override 99 protected void onPostExecute(LocalPrinterCapabilities result) { 100 capabilitiesConsumer.accept(result); 101 } 102 }; 103 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 104 return task; 105 } 106 107 /** 108 * Start a print job. Results will be notified to the listener. Do not start more than 109 * one job at a time. 110 */ print(Uri uri, PrintJob printJob, LocalPrinterCapabilities capabilities, Consumer<JobStatus> listener)111 public void print(Uri uri, PrintJob printJob, LocalPrinterCapabilities capabilities, 112 Consumer<JobStatus> listener) { 113 if (DEBUG) Log.d(TAG, "print()"); 114 115 mJobStatusListener = listener; 116 mCurrentJobStatus = new JobStatus(); 117 118 mStartTask = new StartJobTask(mContext, this, uri, printJob, capabilities) { 119 @Override 120 public void onCancelled(Integer result) { 121 if (DEBUG) Log.d(TAG, "StartJobTask onCancelled " + result); 122 onPostExecute(ERROR_CANCEL); 123 } 124 125 @Override 126 protected void onPostExecute(Integer result) { 127 if (DEBUG) Log.d(TAG, "StartJobTask onPostExecute " + result); 128 mStartTask = null; 129 if (result > 0) { 130 mCurrentJobStatus = new JobStatus.Builder(mCurrentJobStatus).setId(result) 131 .build(); 132 } else if (mJobStatusListener != null) { 133 String jobResult = BackendConstants.JOB_DONE_ERROR; 134 if (result == ERROR_CANCEL) { 135 jobResult = BackendConstants.JOB_DONE_CANCELLED; 136 } else if (result == ERROR_FILE) { 137 jobResult = BackendConstants.JOB_DONE_CORRUPT; 138 } 139 140 // If the start attempt failed and we are still listening, notify and be done 141 mCurrentJobStatus = new JobStatus.Builder() 142 .setJobState(BackendConstants.JOB_STATE_DONE) 143 .setJobResult(jobResult).build(); 144 mJobStatusListener.accept(mCurrentJobStatus); 145 mJobStatusListener = null; 146 } 147 } 148 }; 149 mStartTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 150 } 151 152 /** Attempt to cancel the current job */ cancel()153 public void cancel() { 154 if (DEBUG) Log.d(TAG, "cancel()"); 155 156 if (mStartTask != null) { 157 if (DEBUG) Log.d(TAG, "cancelling start task"); 158 mStartTask.cancel(true); 159 } else if (mCurrentJobStatus != null && mCurrentJobStatus.getId() != JobStatus.ID_UNKNOWN) { 160 if (DEBUG) Log.d(TAG, "cancelling job via new task"); 161 new CancelJobTask(this, mCurrentJobStatus.getId()) 162 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 163 } else { 164 if (DEBUG) Log.d(TAG, "Nothing to cancel in backend, ignoring"); 165 } 166 } 167 168 /** 169 * Call when it is safe to release document-centric resources related to a print job 170 */ closeDocument()171 public void closeDocument() { 172 // Tell the renderer it may release resources for the document 173 PdfRender.getInstance(mContext).closeDocument(); 174 } 175 176 /** 177 * Call when service is shutting down, nothing else is happening, and this object 178 * is no longer required. After closing this object it should be discarded. 179 */ close()180 public void close() { 181 nativeExit(); 182 PdfRender.getInstance(mContext).close(); 183 } 184 185 /** Called by JNI */ 186 @Override jobCallback(final int jobId, final JobCallbackParams params)187 public void jobCallback(final int jobId, final JobCallbackParams params) { 188 mMainHandler.post(() -> { 189 if (DEBUG) Log.d(TAG, "jobCallback() jobId=" + jobId + ", params=" + params); 190 191 JobStatus.Builder builder = new JobStatus.Builder(mCurrentJobStatus); 192 193 builder.setId(params.jobId); 194 195 if (params.certificate != null) { 196 builder.setCertificate(params.certificate); 197 } 198 199 if (!TextUtils.isEmpty(params.printerState)) { 200 updateBlockedReasons(builder, params); 201 } else if (!TextUtils.isEmpty(params.jobState)) { 202 builder.setJobState(params.jobState); 203 if (!TextUtils.isEmpty(params.jobDoneResult)) { 204 builder.setJobResult(params.jobDoneResult); 205 } 206 updateBlockedReasons(builder, params); 207 } 208 mCurrentJobStatus = builder.build(); 209 210 if (mJobStatusListener != null) { 211 mJobStatusListener.accept(mCurrentJobStatus); 212 } 213 214 if (mCurrentJobStatus.isJobDone()) { 215 nativeEndJob(jobId); 216 // Reset status for next job. 217 mCurrentJobStatus = new JobStatus(); 218 mJobStatusListener = null; 219 220 FileUtils.deleteAll(new File(mContext.getFilesDir(), Backend.TEMP_JOB_FOLDER)); 221 } 222 }); 223 } 224 225 /** Update the blocked reason list with non-empty strings */ updateBlockedReasons(JobStatus.Builder builder, JobCallbackParams params)226 private void updateBlockedReasons(JobStatus.Builder builder, JobCallbackParams params) { 227 if ((params.blockedReasons != null) && (params.blockedReasons.length > 0)) { 228 builder.clearBlockedReasons(); 229 for (String reason : params.blockedReasons) { 230 if (!TextUtils.isEmpty(reason)) { 231 builder.addBlockedReason(reason); 232 } 233 } 234 } 235 } 236 237 /** 238 * Extracts the ip portion of x.x.x.x/y/z 239 * 240 * @param address any string in the format xxx/yyy/zzz 241 * @return the part before the "/" or "xxx" in this case 242 */ getIp(String address)243 public static String getIp(String address) { 244 int i = address.indexOf('/'); 245 return i == -1 ? address : address.substring(0, i); 246 } 247 248 /** 249 * Initialize the lower layer. 250 * 251 * @param jobCallback job callback to use whenever job updates arrive 252 * @param dataDir directory to use for temporary files 253 * @param apiVersion local system API version to be supplied to printers 254 * @return {@link BackendConstants#STATUS_OK} or an error code. 255 */ nativeInit(JobCallback jobCallback, String dataDir, int apiVersion)256 native int nativeInit(JobCallback jobCallback, String dataDir, int apiVersion); 257 258 /** 259 * Supply additional information about the source of jobs. 260 * 261 * @param appName human-readable name of application providing data to the printer 262 * @param version version of delivering application 263 * @param appId identifier for the delivering application 264 */ nativeSetSourceInfo(String appName, String version, String appId)265 native void nativeSetSourceInfo(String appName, String version, String appId); 266 267 /** 268 * Request capabilities from a printer. 269 * 270 * @param address IP address or hostname (e.g. "192.168.1.2") 271 * @param port port to use (e.g. 631) 272 * @param httpResource path of print resource on host (e.g. "/ipp/print") 273 * @param uriScheme scheme (e.g. "ipp") 274 * @param timeout milliseconds to wait before giving up on request 275 * @param capabilities target object to be filled with printer capabilities, if successful 276 * @return {@link BackendConstants#STATUS_OK} or an error code. 277 */ nativeGetCapabilities(String address, int port, String httpResource, String uriScheme, long timeout, LocalPrinterCapabilities capabilities)278 native int nativeGetCapabilities(String address, int port, String httpResource, 279 String uriScheme, long timeout, LocalPrinterCapabilities capabilities); 280 281 /** 282 * Java native interface to setup to monitor status 283 * 284 * @param address printer address 285 * @param port printer port 286 * @param httpResource printer http resource 287 * @param uriScheme printer URI scheme 288 * @return status handle 289 */ nativeMonitorStatusSetup( String address, int port, String httpResource, String uriScheme )290 public native long nativeMonitorStatusSetup( 291 String address, int port, String httpResource, String uriScheme 292 ); 293 294 /** 295 * Java native interface to start to monitor status 296 * 297 * @param statusId status ID 298 * @param monitor print status monitor 299 */ nativeMonitorStatusStart(long statusId, PrinterStatusMonitor monitor)300 public native void nativeMonitorStatusStart(long statusId, PrinterStatusMonitor monitor); 301 302 /** 303 * Java native interface to stop to monitor status 304 * 305 * @param statusId status ID 306 */ nativeMonitorStatusStop(long statusId)307 public native void nativeMonitorStatusStop(long statusId); 308 309 /** 310 * Determine initial parameters to be used for jobs 311 * 312 * @param jobParams object to be filled with default parameters 313 * @return {@link BackendConstants#STATUS_OK} or an error code. 314 */ nativeGetDefaultJobParameters(LocalJobParams jobParams)315 native int nativeGetDefaultJobParameters(LocalJobParams jobParams); 316 317 /** 318 * Update job parameters to align with known printer capabilities 319 * 320 * @param jobParams on input, contains requested job parameters; on output contains final 321 * job parameter selections. 322 * @param capabilities printer capabilities to be used when finalizing job parameters 323 * @return {@link BackendConstants#STATUS_OK} or an error code. 324 */ nativeGetFinalJobParameters(LocalJobParams jobParams, LocalPrinterCapabilities capabilities)325 native int nativeGetFinalJobParameters(LocalJobParams jobParams, 326 LocalPrinterCapabilities capabilities); 327 328 /** 329 * Begin job delivery to a target printer. Updates on the job will be sent to the registered 330 * {@link JobCallback}. 331 * 332 * @param address IP address or hostname (e.g. "192.168.1.2") 333 * @param port port to use (e.g. 631) 334 * @param mimeType MIME type of data being sent 335 * @param jobParams job parameters to use when providing the job to the printer 336 * @param capabilities printer capabilities for the printer being used 337 * @param fileList list of files to be provided of the given MIME type 338 * @param debugDir directory to receive debugging information, if any 339 * @param scheme URI scheme (e.g. ipp/ipps) 340 * @return {@link BackendConstants#STATUS_OK} or an error code. 341 */ nativeStartJob(String address, int port, String mimeType, LocalJobParams jobParams, LocalPrinterCapabilities capabilities, String[] fileList, String debugDir, String scheme)342 native int nativeStartJob(String address, int port, String mimeType, LocalJobParams jobParams, 343 LocalPrinterCapabilities capabilities, String[] fileList, String debugDir, 344 String scheme); 345 346 /** 347 * Request cancellation of the identified job. 348 * 349 * @param jobId identifier of the job to cancel 350 * @return {@link BackendConstants#STATUS_OK} or an error code. 351 */ nativeCancelJob(int jobId)352 native int nativeCancelJob(int jobId); 353 354 /** 355 * Finalizes a job after it is ends for any reason 356 * 357 * @param jobId identifier of the job to end 358 * @return {@link BackendConstants#STATUS_OK} or an error code. 359 */ nativeEndJob(int jobId)360 native int nativeEndJob(int jobId); 361 362 /** 363 * Shut down and clean up resources in the JNI layer on system exit 364 * 365 * @return {@link BackendConstants#STATUS_OK} or an error code. 366 */ nativeExit()367 native int nativeExit(); 368 } 369