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