Home | History | Annotate | Download | only in ipp
      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 VERSION_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 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      */
    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 */
    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      */
    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      */
    179     public void close() {
    180         nativeExit();
    181         PdfRender.getInstance(mContext).close();
    182     }
    183 
    184     /** Called by JNI */
    185     @Override
    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 (!TextUtils.isEmpty(params.printerState)) {
    195                 updateBlockedReasons(builder, params);
    196             } else if (!TextUtils.isEmpty(params.jobState)) {
    197                 builder.setJobState(params.jobState);
    198                 if (!TextUtils.isEmpty(params.jobDoneResult)) {
    199                     builder.setJobResult(params.jobDoneResult);
    200                 }
    201                 updateBlockedReasons(builder, params);
    202             }
    203             mCurrentJobStatus = builder.build();
    204 
    205             if (mJobStatusListener != null) {
    206                 mJobStatusListener.accept(mCurrentJobStatus);
    207             }
    208 
    209             if (mCurrentJobStatus.isJobDone()) {
    210                 nativeEndJob(jobId);
    211                 // Reset status for next job.
    212                 mCurrentJobStatus = new JobStatus();
    213                 mJobStatusListener = null;
    214 
    215                 FileUtils.deleteAll(new File(mContext.getFilesDir(), Backend.TEMP_JOB_FOLDER));
    216             }
    217         });
    218     }
    219 
    220     /** Update the blocked reason list with non-empty strings */
    221     private void updateBlockedReasons(JobStatus.Builder builder, JobCallbackParams params) {
    222         if ((params.blockedReasons != null) && (params.blockedReasons.length > 0)) {
    223             builder.clearBlockedReasons();
    224             for (String reason : params.blockedReasons) {
    225                 if (!TextUtils.isEmpty(reason)) {
    226                     builder.addBlockedReason(reason);
    227                 }
    228             }
    229         }
    230     }
    231 
    232     /**
    233      * Extracts the ip portion of x.x.x.x/y/z
    234      *
    235      * @param address any string in the format xxx/yyy/zzz
    236      * @return the part before the "/" or "xxx" in this case
    237      */
    238     static String getIp(String address) {
    239         int i = address.indexOf('/');
    240         return i == -1 ? address : address.substring(0, i);
    241     }
    242 
    243     /**
    244      * Initialize the lower layer.
    245      *
    246      * @param jobCallback job callback to use whenever job updates arrive
    247      * @param dataDir directory to use for temporary files
    248      * @param apiVersion local system API version to be supplied to printers
    249      * @return {@link BackendConstants#STATUS_OK} or an error code.
    250      */
    251     native int nativeInit(JobCallback jobCallback, String dataDir, int apiVersion);
    252 
    253     /**
    254      * Supply additional information about the source of jobs.
    255      *
    256      * @param appName human-readable name of application providing data to the printer
    257      * @param version version of delivering application
    258      * @param appId identifier for the delivering application
    259      */
    260     native void nativeSetSourceInfo(String appName, String version, String appId);
    261 
    262     /**
    263      * Request capabilities from a printer.
    264      *
    265      * @param address IP address or hostname (e.g. "192.168.1.2")
    266      * @param port port to use (e.g. 631)
    267      * @param httpResource path of print resource on host (e.g. "/ipp/print")
    268      * @param uriScheme scheme (e.g. "ipp")
    269      * @param timeout milliseconds to wait before giving up on request
    270      * @param capabilities target object to be filled with printer capabilities, if successful
    271      * @return {@link BackendConstants#STATUS_OK} or an error code.
    272      */
    273     native int nativeGetCapabilities(String address, int port, String httpResource,
    274             String uriScheme, long timeout, LocalPrinterCapabilities capabilities);
    275 
    276     /**
    277      * Determine initial parameters to be used for jobs
    278      *
    279      * @param jobParams object to be filled with default parameters
    280      * @return {@link BackendConstants#STATUS_OK} or an error code.
    281      */
    282     native int nativeGetDefaultJobParameters(LocalJobParams jobParams);
    283 
    284     /**
    285      * Update job parameters to align with known printer capabilities
    286      *
    287      * @param jobParams on input, contains requested job parameters; on output contains final
    288      *                  job parameter selections.
    289      * @param capabilities printer capabilities to be used when finalizing job parameters
    290      * @return {@link BackendConstants#STATUS_OK} or an error code.
    291      */
    292     native int nativeGetFinalJobParameters(LocalJobParams jobParams,
    293             LocalPrinterCapabilities capabilities);
    294 
    295     /**
    296      * Begin job delivery to a target printer. Updates on the job will be sent to the registered
    297      * {@link JobCallback}.
    298      *
    299      * @param address IP address or hostname (e.g. "192.168.1.2")
    300      * @param port port to use (e.g. 631)
    301      * @param mimeType MIME type of data being sent
    302      * @param jobParams job parameters to use when providing the job to the printer
    303      * @param capabilities printer capabilities for the printer being used
    304      * @param fileList list of files to be provided of the given MIME type
    305      * @param debugDir directory to receive debugging information, if any
    306      * @param scheme URI scheme (e.g. ipp/ipps)
    307      * @return {@link BackendConstants#STATUS_OK} or an error code.
    308      */
    309     native int nativeStartJob(String address, int port, String mimeType, LocalJobParams jobParams,
    310             LocalPrinterCapabilities capabilities, String[] fileList, String debugDir,
    311             String scheme);
    312 
    313     /**
    314      * Request cancellation of the identified job.
    315      *
    316      * @param jobId identifier of the job to cancel
    317      * @return {@link BackendConstants#STATUS_OK} or an error code.
    318      */
    319     native int nativeCancelJob(int jobId);
    320 
    321     /**
    322      * Finalizes a job after it is ends for any reason
    323      *
    324      * @param jobId identifier of the job to end
    325      * @return {@link BackendConstants#STATUS_OK} or an error code.
    326      */
    327     native int nativeEndJob(int jobId);
    328 
    329     /**
    330      * Shut down and clean up resources in the JNI layer on system exit
    331      *
    332      * @return {@link BackendConstants#STATUS_OK} or an error code.
    333      */
    334     native int nativeExit();
    335 }
    336