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