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