/* * Copyright (C) 2016 The Android Open Source Project * Copyright (C) 2016 Mopria Alliance, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bips.ipp; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.printservice.PrintJob; import android.text.TextUtils; import android.util.Log; import com.android.bips.R; import com.android.bips.jni.BackendConstants; import com.android.bips.jni.JobCallback; import com.android.bips.jni.JobCallbackParams; import com.android.bips.jni.LocalJobParams; import com.android.bips.jni.LocalPrinterCapabilities; import com.android.bips.jni.PdfRender; import com.android.bips.util.FileUtils; import java.io.File; import java.util.Locale; import java.util.function.Consumer; public class Backend implements JobCallback { private static final String TAG = Backend.class.getSimpleName(); private static final boolean DEBUG = false; static final String TEMP_JOB_FOLDER = "jobs"; // Error codes strictly to be in negative number static final int ERROR_FILE = -1; static final int ERROR_CANCEL = -2; static final int ERROR_UNKNOWN = -3; private static final String VERSION_UNKNOWN = "(unknown)"; private final Handler mMainHandler; private final Context mContext; private JobStatus mCurrentJobStatus; private Consumer mJobStatusListener; private AsyncTask mStartTask; public Backend(Context context) { if (DEBUG) Log.d(TAG, "Backend()"); mContext = context; mMainHandler = new Handler(context.getMainLooper()); PdfRender.getInstance(mContext); // Load required JNI libraries System.loadLibrary(BackendConstants.WPRINT_LIBRARY_PREFIX); // Create and initialize JNI layer nativeInit(this, context.getApplicationInfo().dataDir, Build.VERSION.SDK_INT); nativeSetSourceInfo(context.getString(R.string.app_name).toLowerCase(Locale.US), getApplicationVersion(context).toLowerCase(Locale.US), BackendConstants.WPRINT_APPLICATION_ID.toLowerCase(Locale.US)); } /** Return the current application version or VERSION_UNKNOWN */ private String getApplicationVersion(Context context) { try { PackageInfo packageInfo = context.getPackageManager() .getPackageInfo(context.getPackageName(), 0); return packageInfo.versionName; } catch (PackageManager.NameNotFoundException e) { return VERSION_UNKNOWN; } } /** Asynchronously get printer capabilities, returning results or null to a callback */ public GetCapabilitiesTask getCapabilities(Uri uri, long timeout, boolean highPriority, final Consumer capabilitiesConsumer) { if (DEBUG) Log.d(TAG, "getCapabilities()"); GetCapabilitiesTask task = new GetCapabilitiesTask(this, uri, timeout, highPriority) { @Override protected void onPostExecute(LocalPrinterCapabilities result) { capabilitiesConsumer.accept(result); } }; task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); return task; } /** * Start a print job. Results will be notified to the listener. Do not start more than * one job at a time. */ public void print(Uri uri, PrintJob printJob, LocalPrinterCapabilities capabilities, Consumer listener) { if (DEBUG) Log.d(TAG, "print()"); mJobStatusListener = listener; mCurrentJobStatus = new JobStatus(); mStartTask = new StartJobTask(mContext, this, uri, printJob, capabilities) { @Override public void onCancelled(Integer result) { if (DEBUG) Log.d(TAG, "StartJobTask onCancelled " + result); onPostExecute(ERROR_CANCEL); } @Override protected void onPostExecute(Integer result) { if (DEBUG) Log.d(TAG, "StartJobTask onPostExecute " + result); mStartTask = null; if (result > 0) { mCurrentJobStatus = new JobStatus.Builder(mCurrentJobStatus).setId(result) .build(); } else if (mJobStatusListener != null) { String jobResult = BackendConstants.JOB_DONE_ERROR; if (result == ERROR_CANCEL) { jobResult = BackendConstants.JOB_DONE_CANCELLED; } else if (result == ERROR_FILE) { jobResult = BackendConstants.JOB_DONE_CORRUPT; } // If the start attempt failed and we are still listening, notify and be done mCurrentJobStatus = new JobStatus.Builder() .setJobState(BackendConstants.JOB_STATE_DONE) .setJobResult(jobResult).build(); mJobStatusListener.accept(mCurrentJobStatus); mJobStatusListener = null; } } }; mStartTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } /** Attempt to cancel the current job */ public void cancel() { if (DEBUG) Log.d(TAG, "cancel()"); if (mStartTask != null) { if (DEBUG) Log.d(TAG, "cancelling start task"); mStartTask.cancel(true); } else if (mCurrentJobStatus != null && mCurrentJobStatus.getId() != JobStatus.ID_UNKNOWN) { if (DEBUG) Log.d(TAG, "cancelling job via new task"); new CancelJobTask(this, mCurrentJobStatus.getId()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { if (DEBUG) Log.d(TAG, "Nothing to cancel in backend, ignoring"); } } /** * Call when it is safe to release document-centric resources related to a print job */ public void closeDocument() { // Tell the renderer it may release resources for the document PdfRender.getInstance(mContext).closeDocument(); } /** * Call when service is shutting down, nothing else is happening, and this object * is no longer required. After closing this object it should be discarded. */ public void close() { new Thread(this::nativeExit).start(); PdfRender.getInstance(mContext).close(); } /** Called by JNI */ @Override public void jobCallback(final int jobId, final JobCallbackParams params) { mMainHandler.post(() -> { if (DEBUG) Log.d(TAG, "jobCallback() jobId=" + jobId + ", params=" + params); JobStatus.Builder builder = new JobStatus.Builder(mCurrentJobStatus); builder.setId(params.jobId); if (params.certificate != null) { builder.setCertificate(params.certificate); } if (!TextUtils.isEmpty(params.printerState)) { updateBlockedReasons(builder, params); } else if (!TextUtils.isEmpty(params.jobState)) { builder.setJobState(params.jobState); if (!TextUtils.isEmpty(params.jobDoneResult)) { builder.setJobResult(params.jobDoneResult); } updateBlockedReasons(builder, params); } mCurrentJobStatus = builder.build(); if (mJobStatusListener != null) { mJobStatusListener.accept(mCurrentJobStatus); } if (mCurrentJobStatus.isJobDone()) { nativeEndJob(jobId); // Reset status for next job. mCurrentJobStatus = new JobStatus(); mJobStatusListener = null; FileUtils.deleteAll(new File(mContext.getFilesDir(), Backend.TEMP_JOB_FOLDER)); } }); } /** Update the blocked reason list with non-empty strings */ private void updateBlockedReasons(JobStatus.Builder builder, JobCallbackParams params) { if ((params.blockedReasons != null) && (params.blockedReasons.length > 0)) { builder.clearBlockedReasons(); for (String reason : params.blockedReasons) { if (!TextUtils.isEmpty(reason)) { builder.addBlockedReason(reason); } } } } /** * Extracts the ip portion of x.x.x.x/y/z * * @param address any string in the format xxx/yyy/zzz * @return the part before the "/" or "xxx" in this case */ static String getIp(String address) { int i = address.indexOf('/'); return i == -1 ? address : address.substring(0, i); } /** * Initialize the lower layer. * * @param jobCallback job callback to use whenever job updates arrive * @param dataDir directory to use for temporary files * @param apiVersion local system API version to be supplied to printers * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeInit(JobCallback jobCallback, String dataDir, int apiVersion); /** * Supply additional information about the source of jobs. * * @param appName human-readable name of application providing data to the printer * @param version version of delivering application * @param appId identifier for the delivering application */ native void nativeSetSourceInfo(String appName, String version, String appId); /** * Request capabilities from a printer. * * @param address IP address or hostname (e.g. "192.168.1.2") * @param port port to use (e.g. 631) * @param httpResource path of print resource on host (e.g. "/ipp/print") * @param uriScheme scheme (e.g. "ipp") * @param timeout milliseconds to wait before giving up on request * @param capabilities target object to be filled with printer capabilities, if successful * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeGetCapabilities(String address, int port, String httpResource, String uriScheme, long timeout, LocalPrinterCapabilities capabilities); /** * Determine initial parameters to be used for jobs * * @param jobParams object to be filled with default parameters * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeGetDefaultJobParameters(LocalJobParams jobParams); /** * Update job parameters to align with known printer capabilities * * @param jobParams on input, contains requested job parameters; on output contains final * job parameter selections. * @param capabilities printer capabilities to be used when finalizing job parameters * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeGetFinalJobParameters(LocalJobParams jobParams, LocalPrinterCapabilities capabilities); /** * Begin job delivery to a target printer. Updates on the job will be sent to the registered * {@link JobCallback}. * * @param address IP address or hostname (e.g. "192.168.1.2") * @param port port to use (e.g. 631) * @param mimeType MIME type of data being sent * @param jobParams job parameters to use when providing the job to the printer * @param capabilities printer capabilities for the printer being used * @param fileList list of files to be provided of the given MIME type * @param debugDir directory to receive debugging information, if any * @param scheme URI scheme (e.g. ipp/ipps) * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeStartJob(String address, int port, String mimeType, LocalJobParams jobParams, LocalPrinterCapabilities capabilities, String[] fileList, String debugDir, String scheme); /** * Request cancellation of the identified job. * * @param jobId identifier of the job to cancel * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeCancelJob(int jobId); /** * Finalizes a job after it is ends for any reason * * @param jobId identifier of the job to end * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeEndJob(int jobId); /** * Shut down and clean up resources in the JNI layer on system exit * * @return {@link BackendConstants#STATUS_OK} or an error code. */ native int nativeExit(); }