Merge "Fix long-edge vs short-edge duplex in PWG-Raster"
diff --git a/jni/include/lib_wprint.h b/jni/include/lib_wprint.h
index 006f6c1..147e557 100644
--- a/jni/include/lib_wprint.h
+++ b/jni/include/lib_wprint.h
@@ -188,6 +188,10 @@
char docCategory[10];
const char *media_default;
+ // Expected certificate if any
+ uint8 *certificate;
+ int certificate_len;
+
// IPP max job-name is 2**31 - 1, we set a shorter limit
char job_name[MAX_ID_STRING_LENGTH + 1];
char job_originating_user_name[MAX_NAME_LENGTH + 1];
@@ -198,17 +202,23 @@
bool accepts_os_version;
} wprint_job_params_t;
+typedef struct wprint_connect_info_st wprint_connect_info_t;
+
/*
* Parameters defining how to reach a remote printing service
*/
-typedef struct {
+struct wprint_connect_info_st {
const char *printer_addr;
const char *uri_path;
const char *uri_scheme;
int port_num;
/* Timeout per retry in milliseconds */
long timeout;
-} wprint_connect_info_t;
+ /* Return non-0 if the received certificate is not acceptable. */
+ int (*validate_certificate)(struct wprint_connect_info_st *connect_info, uint8 *data, int data_len);
+ /* User-supplied data. */
+ void *user;
+};
/*
* Current state of a queued job
@@ -224,6 +234,9 @@
wprint_job_state_t state;
unsigned int blocked_reasons;
int job_done_result;
+ // Certificate received from printer, if any
+ uint8 *certificate;
+ int certificate_len;
} wprint_job_callback_params_t;
typedef enum {
diff --git a/jni/include/wprint_status_types.h b/jni/include/wprint_status_types.h
index 9fe1b1d..80b3c74 100644
--- a/jni/include/wprint_status_types.h
+++ b/jni/include/wprint_status_types.h
@@ -38,7 +38,8 @@
#define BLOCKED_REASON_IDLE (1 << PRINT_STATUS_IDLE)
#define BLOCKED_REASON_CANCELLED (1 << PRINT_STATUS_CANCELLED)
#define BLOCKED_REASON_PRINT_STATUS_VERY_LOW_ON_INK (1 << PRINT_STATUS_VERY_LOW_ON_INK)
-#define BLOCKED_REASON_PARTIAL_CANCEL (1 << PRINT_STATUS_PARTIAL_CANCEL)
+#define BLOCKED_REASON_PARTIAL_CANCEL (1 << PRINT_STATUS_PARTIAL_CANCEL)
+#define BLOCKED_REASON_BAD_CERTIFICATE (1 << PRINT_STATUS_BAD_CERTIFICATE)
/*
* Enumeration for printer statuses
@@ -68,6 +69,7 @@
PRINT_STATUS_VERY_LOW_ON_INK,
PRINT_STATUS_PARTIAL_CANCEL,
+ PRINT_STATUS_BAD_CERTIFICATE,
PRINT_STATUS_MAX_STATE // Add new entries above this line.
} print_status_t;
diff --git a/jni/ipphelper/ipphelper.c b/jni/ipphelper/ipphelper.c
index 60758ac..ec01564 100644
--- a/jni/ipphelper/ipphelper.c
+++ b/jni/ipphelper/ipphelper.c
@@ -643,8 +643,6 @@
LOGD("media-supported found; number of values %d", ippGetCount(attrptr));
for (i = 0; i < ippGetCount(attrptr); i++) {
idx = ipp_find_media_size(ippGetString(attrptr, i, NULL), &media_sizeTemp);
- LOGD(" Temp - i: %d idx %d keyword: %s PT_size %d", i, idx, ippGetString(
- attrptr, i, NULL), media_sizeTemp);
// Modified since anytime the find media size returned 0 it could either mean
// NOT found or na_letter.
@@ -1171,11 +1169,29 @@
}
}
+/*
+ * Handle server certificate information.
+ */
+static int ipp_server_cert_cb(http_t *http, void *tls, cups_array_t *certs, void *user_data) {
+ wprint_connect_info_t *connect_info = (wprint_connect_info_t *)user_data;
+ int error = 0;
+ if (connect_info->validate_certificate) {
+ http_credential_t *credential = cupsArrayFirst(certs);
+ if (credential) {
+ LOGD("ipp_server_cert_cb: validate_certificate (len=%d)", credential->datalen);
+ error = connect_info->validate_certificate(connect_info, credential->data, credential->datalen);
+ }
+ }
+ return error;
+}
+
http_t *ipp_cups_connect(const wprint_connect_info_t *connect_info, char *printer_uri,
unsigned int uriLength) {
const char *uri_path;
http_t *curl_http = NULL;
+ cupsSetServerCertCB(ipp_server_cert_cb, (void *)connect_info);
+
if ((connect_info->uri_path == NULL) || (strlen(connect_info->uri_path) == 0)) {
uri_path = DEFAULT_IPP_URI_RESOURCE;
} else {
@@ -1202,6 +1218,8 @@
if (curl_http == NULL) {
LOGD("ipp_cups_connect failed addr=%s port=%d", connect_info->printer_addr, ippPortNumber);
}
+
+ cupsSetServerCertCB(NULL, NULL);
return curl_http;
}
diff --git a/jni/lib/lib_wprint.c b/jni/lib/lib_wprint.c
index e7aebfe..8af47d6 100644
--- a/jni/lib/lib_wprint.c
+++ b/jni/lib/lib_wprint.c
@@ -154,6 +154,10 @@
char printer_uri[1024];
int job_debug_fd;
int page_debug_fd;
+
+ /* A buffer of bytes containing the certificate received while setting up this job, if any. */
+ uint8 *certificate;
+ int certificate_len;
} _job_queue_t;
/*
@@ -480,7 +484,10 @@
}
jq->page_debug_fd = -1;
jq->debug_path[0] = 0;
-
+ if (jq->certificate) {
+ free(jq->certificate);
+ jq->certificate = NULL;
+ }
return OK;
} else {
return ERROR;
@@ -516,6 +523,8 @@
statusnew = new_status->printer_status & ~PRINTER_IDLE_BIT;
statusold = old_status->printer_status & ~PRINTER_IDLE_BIT;
+ cb_param.certificate = jq->certificate;
+ cb_param.certificate_len = jq->certificate_len;
LOGD("_job_status_callback(): current printer state: %d", statusnew);
blocked_reasons = 0;
@@ -689,10 +698,86 @@
}
/*
+ * Return true unless the server gave an unexpected certificate
+ */
+static bool _is_certificate_allowed(_job_queue_t *jq) {
+ int result = true;
+
+ // Compare certificates if both are known
+ if (jq->job_params.certificate && jq->certificate) {
+ if (jq->job_params.certificate_len != jq->certificate_len) {
+ LOGD("_is_certificate_allowed: certificate length mismatch allowed=%d, received=%d",
+ jq->job_params.certificate_len, jq->certificate_len);
+ result = false;
+ } else if (0 != memcmp(jq->job_params.certificate, jq->certificate, jq->certificate_len)) {
+ LOGD("_is_certificate_allowed: certificate content mismatch");
+ result = false;
+ } else {
+ LOGD("_is_certificate_allowed: certificate match, len=%d",
+ jq->job_params.certificate_len);
+ }
+ }
+
+ return result;
+}
+
+/*
+ * Callback from lower layers containing certificate data, if any.
+ */
+static int _validate_certificate(wprint_connect_info_t *connect_info, uint8 *data, int data_len) {
+ _job_queue_t *jq = connect_info->user;
+ LOGD("_validate_certificate: %s://%s:%d%s handling server cert len=%d for job %ld",
+ connect_info->uri_scheme, connect_info->printer_addr, connect_info->port_num,
+ connect_info->uri_path, data_len, jq->job_handle);
+
+ // Free any old certificate we have and save new certificate data
+ if (jq->certificate) {
+ free(jq->certificate);
+ jq->certificate = NULL;
+ }
+ jq->certificate = (uint8 *)malloc(data_len);
+ int error = 0;
+ if (jq->certificate == NULL) {
+ LOGD("_validate_certificate: malloc failed");
+ error = -1;
+ } else {
+ memcpy(jq->certificate, data, data_len);
+ jq->certificate_len = data_len;
+ if (!_is_certificate_allowed(jq)) {
+ LOGD("_validate_certificate: received certificate disallowed.");
+ error = -1;
+ }
+ }
+ return error;
+}
+
+/*
+ * Initialize the status interface (so we can use it to query for printer status.
+ */
+static void _initialize_status_ifc(_job_queue_t *jq) {
+ wprint_connect_info_t connect_info;
+ connect_info.printer_addr = jq->printer_addr;
+ connect_info.uri_path = jq->printer_uri;
+ connect_info.port_num = jq->port_num;
+ if (jq->use_secure_uri) {
+ connect_info.uri_scheme = IPPS_PREFIX;
+ connect_info.user = jq;
+ connect_info.validate_certificate = _validate_certificate;
+ } else {
+ connect_info.uri_scheme = IPP_PREFIX;
+ connect_info.validate_certificate = NULL;
+ }
+ connect_info.timeout = DEFAULT_IPP_TIMEOUT;
+
+ // Initialize the status interface with this connection info
+ jq->status_ifc->init(jq->status_ifc, &connect_info);
+}
+
+/*
* Runs a print job. Contains logic for what to do given different printer statuses.
*/
static void *_job_thread(void *param) {
- wprint_job_callback_params_t cb_param;
+ wprint_job_callback_params_t cb_param = { 0 };
_msg_t msg;
wJob_t job_handle;
_job_queue_t *jq;
@@ -737,17 +822,7 @@
// initialize the status ifc
if (jq->status_ifc != NULL) {
- wprint_connect_info_t connect_info;
- connect_info.printer_addr = jq->printer_addr;
- connect_info.uri_path = jq->printer_uri;
- connect_info.port_num = jq->port_num;
- if (jq->use_secure_uri) {
- connect_info.uri_scheme = IPPS_PREFIX;
- } else {
- connect_info.uri_scheme = IPP_PREFIX;
- }
- connect_info.timeout = DEFAULT_IPP_TIMEOUT;
- jq->status_ifc->init(jq->status_ifc, &connect_info);
+ _initialize_status_ifc(jq);
}
// wait for the printer to be idle
if ((jq->status_ifc != NULL) && (jq->status_ifc->get_status != NULL)) {
@@ -759,6 +834,10 @@
jq->status_ifc->get_status(jq->status_ifc, &printer_state);
status = printer_state.printer_status & ~PRINTER_IDLE_BIT;
+ // Pass along any certificate received in future callbacks
+ cb_param.certificate = jq->certificate;
+ cb_param.certificate_len = jq->certificate_len;
+
switch (status) {
case PRINT_STATUS_IDLE:
printer_state.printer_status = PRINT_STATUS_IDLE;
@@ -776,8 +855,13 @@
case PRINT_STATUS_SVC_REQUEST:
if ((printer_state.printer_reasons[0] == PRINT_STATUS_UNABLE_TO_CONNECT)
|| (printer_state.printer_reasons[0] == PRINT_STATUS_OFFLINE)) {
- LOGD("_job_thread: Received an Unable to Connect message");
- jq->blocked_reasons = BLOCKED_REASON_UNABLE_TO_CONNECT;
+ if (_is_certificate_allowed(jq)) {
+ LOGD("_job_thread: Received an Unable to Connect message");
+ jq->blocked_reasons = BLOCKED_REASON_UNABLE_TO_CONNECT;
+ } else {
+ LOGD("_job_thread: Bad certificate");
+ jq->blocked_reasons = BLOCKED_REASON_BAD_CERTIFICATE;
+ }
loop = 0;
break;
}
@@ -2047,6 +2131,8 @@
cb_param.state = JOB_DONE;
cb_param.blocked_reasons = BLOCKED_REASONS_CANCELLED;
cb_param.job_done_result = CANCELLED;
+ cb_param.certificate = jq->certificate;
+ cb_param.certificate_len = jq->certificate_len;
jq->cb_fn(job_handle, (void *) &cb_param);
}
diff --git a/jni/lib/wprintJNI.c b/jni/lib/wprintJNI.c
index 913198a..42c27ca 100644
--- a/jni/lib/wprintJNI.c
+++ b/jni/lib/wprintJNI.c
@@ -75,6 +75,7 @@
static jfieldID _LocalPrinterCapabilitiesField__supportedMediaTypes;
static jfieldID _LocalPrinterCapabilitiesField__supportedMediaSizes;
static jfieldID _LocalPrinterCapabilitiesField__nativeData;
+static jfieldID _LocalPrinterCapabilitiesField__certificate;
static jclass _JobCallbackClass;
static jobject _callbackReceiver;
@@ -86,6 +87,7 @@
static jfieldID _JobCallbackParamsField__jobState;
static jfieldID _JobCallbackParamsField__jobDoneResult;
static jfieldID _JobCallbackParamsField__blockedReasons;
+static jfieldID _JobCallbackParamsField__certificate;
static jclass _PrintServiceStringsClass;
static jfieldID _PrintServiceStringsField__JOB_STATE_QUEUED;
@@ -110,6 +112,7 @@
static jfieldID _PrintServiceStringsField__BLOCKED_REASON__LOW_ON_INK;
static jfieldID _PrintServiceStringsField__BLOCKED_REASON__LOW_ON_TONER;
static jfieldID _PrintServiceStringsField__BLOCKED_REASON__REALLY_LOW_ON_INK;
+static jfieldID _PrintServiceStringsField__BLOCKED_REASON__BAD_CERTIFICATE;
static jfieldID _PrintServiceStringsField__BLOCKED_REASON__UNKNOWN;
static jfieldID _PrintServiceStringsField__ALIGNMENT__CENTER;
static jfieldID _PrintServiceStringsField__ALIGNMENT__CENTER_HORIZONTAL;
@@ -482,6 +485,8 @@
env, _LocalPrinterCapabilitiesClass, "supportedMediaSizes", "[I");
_LocalPrinterCapabilitiesField__nativeData = (*env)->GetFieldID(
env, _LocalPrinterCapabilitiesClass, "nativeData", "[B");
+ _LocalPrinterCapabilitiesField__certificate = (*env)->GetFieldID(
+ env, _LocalPrinterCapabilitiesClass, "certificate", "[B");
_JobCallbackParamsClass = (jclass) (*env)->NewGlobalRef(env, (*env)->FindClass(
env, "com/android/bips/jni/JobCallbackParams"));
@@ -495,6 +500,8 @@
env, _JobCallbackParamsClass, "jobDoneResult", "Ljava/lang/String;");
_JobCallbackParamsField__blockedReasons = (*env)->GetFieldID(
env, _JobCallbackParamsClass, "blockedReasons", "[Ljava/lang/String;");
+ _JobCallbackParamsField__certificate = (*env)->GetFieldID(
+ env, _JobCallbackParamsClass, "certificate", "[B");
if (callbackReceiver) {
_callbackReceiver = (jobject) (*env)->NewGlobalRef(env, callbackReceiver);
@@ -557,6 +564,9 @@
_PrintServiceStringsField__BLOCKED_REASON__REALLY_LOW_ON_INK = (*env)->GetStaticFieldID(
env, _PrintServiceStringsClass, "BLOCKED_REASON__REALLY_LOW_ON_INK",
"Ljava/lang/String;");
+ _PrintServiceStringsField__BLOCKED_REASON__BAD_CERTIFICATE = (*env)->GetStaticFieldID(
+ env, _PrintServiceStringsClass, "BLOCKED_REASON__BAD_CERTIFICATE",
+ "Ljava/lang/String;");
_PrintServiceStringsField__BLOCKED_REASON__UNKNOWN = (*env)->GetStaticFieldID(
env, _PrintServiceStringsClass, "BLOCKED_REASON__UNKNOWN", "Ljava/lang/String;");
@@ -589,7 +599,7 @@
}
jbyte *nativeDataPtr = (*env)->GetByteArrayElements(env, nativeDataObject, NULL);
memcpy(wprintPrinterCaps, (const void *) nativeDataPtr, sizeof(printer_capabilities_t));
- (*env)->ReleaseByteArrayElements(env, nativeDataObject, nativeDataPtr, JNI_ABORT);
+ (*env)->ReleaseByteArrayElements(env, nativeDataObject, nativeDataPtr, 0);
return OK;
}
@@ -1076,6 +1086,10 @@
jStr = (jstring) (*env)->GetStaticObjectField(
env, _PrintServiceStringsClass,
_PrintServiceStringsField__BLOCKED_REASON__REALLY_LOW_ON_INK);
+ } else if (blocked_reasons & BLOCKED_REASON_BAD_CERTIFICATE) {
+ jStr = (jstring) (*env)->GetStaticObjectField(
+ env, _PrintServiceStringsClass,
+ _PrintServiceStringsField__BLOCKED_REASON__BAD_CERTIFICATE);
} else if (blocked_reasons & BLOCKED_REASON_UNKNOWN) {
jStr = (jstring) (*env)->GetStaticObjectField(
env, _PrintServiceStringsClass,
@@ -1094,6 +1108,24 @@
(*env)->SetIntField(env, callbackParams, _JobCallbackParamsField__jobId,
(jint) job_handle);
+
+ if (cb_param->certificate) {
+ LOGI("_wprint_callback_fn: copying certificate len=%d", cb_param->certificate_len);
+ jbyteArray certificate = (*env)->NewByteArray(env, cb_param->certificate_len);
+ jbyte *certificateBytes = (*env)->GetByteArrayElements(env, certificate, 0);
+ memcpy(certificateBytes, (const void *) cb_param->certificate,
+ cb_param->certificate_len);
+ (*env)->ReleaseByteArrayElements(env, certificate, certificateBytes, 0);
+ (*env)->SetObjectField(env, callbackParams, _JobCallbackParamsField__certificate,
+ certificate);
+ (*env)->DeleteLocalRef(env, certificate);
+ } else {
+ LOGI("_wprint_callback_fn: there is no certificate");
+ // No cert, set NULL
+ (*env)->SetObjectField(env, callbackParams, _JobCallbackParamsField__certificate,
+ NULL);
+ }
+
(*env)->CallVoidMethod(env, _callbackReceiver, _JobCallbackMethod__jobCallback,
(jint) job_handle, callbackParams);
(*env)->DeleteLocalRef(env, callbackParams);
@@ -1160,6 +1192,7 @@
connect_info.uri_scheme = copyToNewString(env, uriScheme);
connect_info.port_num = port;
connect_info.timeout = timeout;
+ connect_info.validate_certificate = NULL;
LOGI("nativeGetCapabilities for %s JNIenv is %p", connect_info.printer_addr, env);
@@ -1222,6 +1255,24 @@
}
/*
+ * Convert certificate (if present) from printer capabilities into job_params.
+ */
+static void _convertCertificate(JNIEnv *env, jobject printerCaps, wprint_job_params_t *params) {
+ params->certificate = NULL;
+ jbyteArray certificate = (jbyteArray) (*env)->GetObjectField(env, printerCaps,
+ _LocalPrinterCapabilitiesField__certificate);
+ if (certificate) {
+ params->certificate_len = (*env)->GetArrayLength(env, certificate);
+ params->certificate = malloc(params->certificate_len);
+ if (params->certificate) {
+ jbyte *certificateBytes = (*env)->GetByteArrayElements(env, certificate, NULL);
+ memcpy(params->certificate, certificateBytes, params->certificate_len);
+ (*env)->ReleaseByteArrayElements(env, certificate, certificateBytes, JNI_ABORT);
+ }
+ }
+}
+
+/*
* JNI call to wprint to start a print job. Takes connection params, job params, caps, and file
* array to complete the job
*/
@@ -1238,6 +1289,7 @@
_convertJobParams_to_C(env, jobParams, ¶ms);
_convertPrinterCaps_to_C(env, printerCaps, &caps);
+ _convertCertificate(env, printerCaps, ¶ms);
LOGD("nativeStartJob: After _convertJobParams_to_C: res=%d, name=%s",
params.pdf_render_resolution, params.job_name);
@@ -1360,6 +1412,9 @@
}
}
+ if (params.certificate) {
+ free(params.certificate);
+ }
(*env)->ReleaseStringUTFChars(env, mimeType, mimeTypeStr);
(*env)->ReleaseStringUTFChars(env, address, addressStr);
(*env)->ReleaseStringUTFChars(env, _fakeDir, dataDirStr);
diff --git a/res/drawable/ic_printer_locked.xml b/res/drawable/ic_printer_locked.xml
new file mode 100644
index 0000000..fbb57bd
--- /dev/null
+++ b/res/drawable/ic_printer_locked.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 The Android Open Source Project
+
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="36dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0"
+ android:tint="@color/iconColor">
+ <path
+ android:pathData="M 16.96 6 L 16.96 14 L 36 14 L 36 6 L 16.96 6 Z M 9.408 6.028 C 7.427 6.028 5.819 7.635 5.819 9.616 L 5.819 11.769 C 5.03 11.769 4.384 12.414 4.384 13.204 L 4.384 18.945 C 4.384 19.734 5.03 20.38 5.819 20.38 L 12.996 20.38 C 13.785 20.38 14.431 19.734 14.431 18.945 L 14.431 13.204 C 14.431 12.414 13.785 11.769 12.996 11.769 L 12.996 9.616 C 12.996 7.635 11.388 6.028 9.408 6.028 Z M 9.408 7.463 C 10.599 7.463 11.561 8.425 11.561 9.616 L 11.561 11.769 L 7.254 11.769 L 7.254 9.616 C 7.254 8.425 8.216 7.463 9.408 7.463 Z M 16.96 16 L 16.96 19.155 C 16.976 20.614 16.078 22.989 13.362 22.973 L 4 22.984 L 4 34 L 12 34 L 12 42 L 36 42 L 36 34 L 44 34 L 44 22 C 44 18.69 41.31 16 38 16 L 16.96 16 Z M 38 20 C 39.11 20 40 20.89 40 22 C 40 23.11 39.11 24 38 24 C 36.89 24 36 23.11 36 22 C 36 20.89 36.89 20 38 20 Z M 16 28 L 32 28 L 32 38 L 16 38 L 16 28 Z"
+ android:fillColor="@android:color/black" />
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eac63ce..015e6b4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -29,6 +29,7 @@
<string name="printer_door_open">Door open</string>
<string name="printer_jammed">Jammed</string>
<string name="printer_offline">Offline</string>
+ <string name="printer_bad_certificate">Bad certificate</string>
<string name="printer_check">Check printer</string>
<string name="waiting_to_send">Waiting to send</string>
@@ -81,5 +82,14 @@
<string name="connects_via_network">Connects via current network at
<xliff:g example="192.168.0.101" id="ip_address">%1$s</xliff:g>
</string>
+ <!-- Channel name for security-related notifications [CHAR LIMIT=40] -->
+ <string name="security">Security</string>
+ <!-- Message shown in notification if a printer presented a changes security certificate [CHAR LIMIT=UNLIMITED] -->
+ <string name="certificate_update_request">This printer provided a new security certificate, or another
+ device is impersonating it. Accept the new certificate?</string>
+ <!-- Button label in a notification. This button a accepts a changed security certificate presented by a printer [CHAR LIMIT=20] -->
+ <string name="accept">Accept</string>
+ <!-- Button label in a notification. This button a rejects a changed security certificate presented by a printer [CHAR LIMIT=20] -->
+ <string name="reject">Reject</string>
</resources>
diff --git a/src/com/android/bips/BuiltInPrintService.java b/src/com/android/bips/BuiltInPrintService.java
index 2bb73ef..9e5d156 100644
--- a/src/com/android/bips/BuiltInPrintService.java
+++ b/src/com/android/bips/BuiltInPrintService.java
@@ -17,13 +17,20 @@
package com.android.bips;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
import android.net.nsd.NsdManager;
import android.net.wifi.WifiManager;
import android.os.Handler;
+import android.print.PrinterId;
import android.printservice.PrintJob;
import android.printservice.PrintService;
import android.printservice.PrinterDiscoverySession;
@@ -40,6 +47,7 @@
import com.android.bips.discovery.P2pDiscovery;
import com.android.bips.ipp.Backend;
import com.android.bips.ipp.CapabilitiesCache;
+import com.android.bips.ipp.CertificateStore;
import com.android.bips.p2p.P2pMonitor;
import com.android.bips.p2p.P2pUtils;
import com.android.bips.util.BroadcastMonitor;
@@ -51,6 +59,17 @@
private static final boolean DEBUG = false;
private static final int IPPS_PRINTER_DELAY = 150;
private static final int P2P_DISCOVERY_DELAY = 1000;
+ private static final String CHANNEL_ID_SECURITY = "security";
+ private static final String TAG_CERTIFICATE_REQUEST =
+ BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_REQUEST";
+ private static final String ACTION_CERTIFICATE_ACCEPT =
+ BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_ACCEPT";
+ private static final String ACTION_CERTIFICATE_REJECT =
+ BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_REJECT";
+ private static final String EXTRA_CERTIFICATE = "certificate";
+ private static final String EXTRA_PRINTER_ID = "printer-id";
+ private static final String EXTRA_PRINTER_UUID = "printer-uuid";
+ private static final int CERTIFICATE_REQUEST_ID = 1000;
// Present because local activities can bind, but cannot access this object directly
private static WeakReference<BuiltInPrintService> sInstance;
@@ -60,6 +79,7 @@
private Discovery mMdnsDiscovery;
private ManualDiscovery mManualDiscovery;
private CapabilitiesCache mCapabilitiesCache;
+ private CertificateStore mCertificateStore;
private JobQueue mJobQueue;
private Handler mMainHandler;
private Backend mBackend;
@@ -85,9 +105,11 @@
}
}
super.onCreate();
+ createNotificationChannel();
sInstance = new WeakReference<>(this);
mBackend = new Backend(this);
+ mCertificateStore = new CertificateStore(this);
mCapabilitiesCache = new CapabilitiesCache(this, mBackend,
CapabilitiesCache.DEFAULT_MAX_CONCURRENT);
mP2pMonitor = new P2pMonitor(this);
@@ -200,6 +222,13 @@
}
/**
+ * Return a store of certificate public keys for supporting trust-on-first-use.
+ */
+ public CertificateStore getCertificateStore() {
+ return mCertificateStore;
+ }
+
+ /**
* Return the main handler for posting {@link Runnable} objects to the main UI
*/
public Handler getMainHandler() {
@@ -228,6 +257,13 @@
}
}
+ /**
+ * Return an icon ID appropriate for displaying a printer.
+ */
+ public int getIconId(DiscoveredPrinter printer) {
+ return printer.isSecure() ? R.drawable.ic_printer_locked : R.drawable.ic_printer;
+ }
+
/** Prevent Wi-Fi from going to sleep until {@link #unlockWifi} is called */
public void lockWifi() {
if (!mWifiLock.isHeld()) {
@@ -241,4 +277,83 @@
mWifiLock.release();
}
}
+
+ /**
+ * Set up a channel for notifications.
+ */
+ private void createNotificationChannel() {
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID_SECURITY,
+ getString(R.string.security), NotificationManager.IMPORTANCE_HIGH);
+
+ NotificationManager manager = (NotificationManager) getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ manager.createNotificationChannel(channel);
+ }
+
+ /**
+ * Notify the user of a certificate change (could be a MITM attack) and allow response.
+ */
+ void notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid,
+ byte[] certificate) {
+ String message = getString(R.string.certificate_update_request);
+
+ Intent rejectIntent = new Intent(this, BuiltInPrintService.class)
+ .setAction(ACTION_CERTIFICATE_REJECT)
+ .putExtra(EXTRA_PRINTER_ID, printerId);
+ PendingIntent pendingRejectIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
+ rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification.Action rejectAction = new Notification.Action.Builder(
+ Icon.createWithResource(this, R.drawable.ic_printservice),
+ getString(R.string.reject), pendingRejectIntent).build();
+
+ PendingIntent deleteIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
+ rejectIntent, 0);
+
+ Intent acceptIntent = new Intent(this, BuiltInPrintService.class)
+ .setAction(ACTION_CERTIFICATE_ACCEPT)
+ .putExtra(EXTRA_CERTIFICATE, certificate)
+ .putExtra(EXTRA_PRINTER_UUID, printerUuid)
+ .putExtra(EXTRA_PRINTER_ID, printerId);
+ PendingIntent pendingAcceptIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
+ acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification.Action acceptAction = new Notification.Action.Builder(
+ Icon.createWithResource(this, R.drawable.ic_printservice),
+ getString(R.string.accept), pendingAcceptIntent).build();
+
+ Notification notification = new Notification.Builder(this, CHANNEL_ID_SECURITY)
+ .setContentTitle(printerName)
+ .setSmallIcon(R.drawable.ic_printservice)
+ .setStyle(new Notification.BigTextStyle().bigText(message))
+ .setContentText(message)
+ .setAutoCancel(true)
+ .addAction(rejectAction)
+ .addAction(acceptAction)
+ .setDeleteIntent(deleteIntent)
+ .build();
+
+ NotificationManager manager = (NotificationManager) getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ manager.notify(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID, notification);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (DEBUG) Log.d(TAG, "Received action=" + intent.getAction());
+ if (ACTION_CERTIFICATE_ACCEPT.equals(intent.getAction())) {
+ byte[] certificate = intent.getByteArrayExtra(EXTRA_CERTIFICATE);
+ PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID);
+ String printerUuid = intent.getStringExtra(EXTRA_PRINTER_UUID);
+ mCertificateStore.put(printerUuid, certificate);
+ // Restart the job with the new certificate in place
+ mJobQueue.restart(printerId);
+ } else if (ACTION_CERTIFICATE_REJECT.equals(intent.getAction())) {
+ // Cancel any job in certificate state for this uuid
+ PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID);
+ mJobQueue.cancel(printerId);
+ }
+ NotificationManager manager = (NotificationManager) getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ manager.cancel(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID);
+ return START_NOT_STICKY;
+ }
}
diff --git a/src/com/android/bips/JobQueue.java b/src/com/android/bips/JobQueue.java
index 6b64b41..f34dcdf 100644
--- a/src/com/android/bips/JobQueue.java
+++ b/src/com/android/bips/JobQueue.java
@@ -18,13 +18,16 @@
package com.android.bips;
import android.print.PrintJobId;
+import android.print.PrinterId;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
/** Manages a job queue, ensuring only one job is printed at a time */
class JobQueue {
- private final List<LocalPrintJob> mJobs = new ArrayList<>();
+ private final List<LocalPrintJob> mJobs = new CopyOnWriteArrayList<>();
private LocalPrintJob mCurrent;
/** Queue a print job for printing at the next available opportunity */
@@ -33,6 +36,26 @@
startNextJob();
}
+ /** Cancel any previously queued job for a printer with the supplied ID. */
+ void cancel(PrinterId printerId) {
+ for (LocalPrintJob job : mJobs) {
+ if (printerId.equals(job.getPrintJob().getInfo().getPrinterId())) {
+ cancel(job.getPrintJobId());
+ }
+ }
+
+ if (mCurrent != null && printerId.equals(mCurrent.getPrintJob().getInfo().getPrinterId())) {
+ cancel(mCurrent.getPrintJobId());
+ }
+ }
+
+ /** Restart any blocked job for a printer with this ID. */
+ void restart(PrinterId printerId) {
+ if (mCurrent != null && printerId.equals(mCurrent.getPrintJob().getInfo().getPrinterId())) {
+ mCurrent.restart();
+ }
+ }
+
/** Cancel a previously queued job */
void cancel(PrintJobId id) {
// If a job hasn't started, kill it instantly.
@@ -44,7 +67,7 @@
}
}
- if (mCurrent.getPrintJobId().equals(id)) {
+ if (mCurrent != null && mCurrent.getPrintJobId().equals(id)) {
mCurrent.cancel();
}
}
diff --git a/src/com/android/bips/LocalPrintJob.java b/src/com/android/bips/LocalPrintJob.java
index ba0776c..4a93f18 100644
--- a/src/com/android/bips/LocalPrintJob.java
+++ b/src/com/android/bips/LocalPrintJob.java
@@ -27,6 +27,7 @@
import com.android.bips.discovery.MdnsDiscovery;
import com.android.bips.ipp.Backend;
import com.android.bips.ipp.CapabilitiesCache;
+import com.android.bips.ipp.CertificateStore;
import com.android.bips.ipp.JobStatus;
import com.android.bips.jni.BackendConstants;
import com.android.bips.jni.LocalPrinterCapabilities;
@@ -51,8 +52,9 @@
private static final int STATE_DISCOVERY = 1;
private static final int STATE_CAPABILITIES = 2;
private static final int STATE_DELIVERING = 3;
- private static final int STATE_CANCEL = 4;
- private static final int STATE_DONE = 5;
+ private static final int STATE_CERTIFICATE = 4;
+ private static final int STATE_CANCEL = 5;
+ private static final int STATE_DONE = 6;
private final BuiltInPrintService mPrintService;
private final PrintJob mPrintJob;
@@ -63,6 +65,7 @@
private Uri mPath;
private DelayedAction mDiscoveryTimeout;
private P2pPrinterConnection mConnection;
+ private LocalPrinterCapabilities mCapabilities;
/**
* Construct the object; use {@link #start(Consumer)} to begin job processing.
@@ -107,12 +110,24 @@
mPrintService.getDiscovery().start(this);
}
+ /**
+ * Restart the job if possible.
+ */
+ void restart() {
+ if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState);
+ if (mState == STATE_CERTIFICATE) {
+ mCapabilities.certificate = mPrintService.getCertificateStore().get(mCapabilities.uuid);
+ deliver();
+ }
+ }
+
void cancel() {
if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState);
switch (mState) {
case STATE_DISCOVERY:
case STATE_CAPABILITIES:
+ case STATE_CERTIFICATE:
// Cancel immediately
mState = STATE_CANCEL;
finish(false, null);
@@ -156,6 +171,14 @@
mPrintService.getDiscovery().stop(this);
mState = STATE_CAPABILITIES;
mPath = printer.path;
+ // Upgrade to IPPS path if present
+ for (Uri path : printer.paths) {
+ if (path.getScheme().equals("ipps")) {
+ mPath = path;
+ break;
+ }
+ }
+
mPrintService.getCapabilitiesCache().request(printer, true, this);
}
@@ -213,14 +236,30 @@
if (mDiscoveryTimeout != null) {
mDiscoveryTimeout.cancel();
}
- mState = STATE_DELIVERING;
- mPrintJob.start();
- mBackend.print(mPath, mPrintJob, capabilities, this::handleJobStatus);
+ mCapabilities = capabilities;
+ deliver();
}
}
+ private void deliver() {
+ mState = STATE_DELIVERING;
+ mPrintJob.start();
+ mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus);
+ }
+
private void handleJobStatus(JobStatus jobStatus) {
if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus);
+
+ byte[] certificate = jobStatus.getCertificate();
+ if (certificate != null && mCapabilities != null) {
+ CertificateStore store = mPrintService.getCertificateStore();
+ // If there is no certificate, record this one
+ if (store.get(mCapabilities.uuid) == null) {
+ if (DEBUG) Log.d(TAG, "Recording new certificate");
+ store.put(mCapabilities.uuid, certificate);
+ }
+ }
+
switch (jobStatus.getJobState()) {
case BackendConstants.JOB_STATE_DONE:
switch (jobStatus.getJobResult()) {
@@ -236,7 +275,11 @@
break;
default:
// Job failed
- finish(false, null);
+ if (jobStatus.getBlockedReasonId() == R.string.printer_bad_certificate) {
+ handleBadCertificate(jobStatus);
+ } else {
+ finish(false, null);
+ }
break;
}
break;
@@ -260,6 +303,20 @@
}
}
+ private void handleBadCertificate(JobStatus jobStatus) {
+ byte[] certificate = jobStatus.getCertificate();
+
+ if (certificate == null) {
+ mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate));
+ } else {
+ if (DEBUG) Log.d(TAG, "Certificate change detected.");
+ mState = STATE_CERTIFICATE;
+ mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate));
+ mPrintService.notifyCertificateChange(mCapabilities.name,
+ mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate);
+ }
+ }
+
/**
* Terminate the job, issuing appropriate notifications.
*
diff --git a/src/com/android/bips/LocalPrinter.java b/src/com/android/bips/LocalPrinter.java
index b8f3da6..f13f3de 100644
--- a/src/com/android/bips/LocalPrinter.java
+++ b/src/com/android/bips/LocalPrinter.java
@@ -109,7 +109,7 @@
PrinterInfo.Builder builder = new PrinterInfo.Builder(
mPrinterId, printer.name,
idle ? PrinterInfo.STATUS_IDLE : PrinterInfo.STATUS_UNAVAILABLE)
- .setIconResourceId(R.drawable.ic_printer)
+ .setIconResourceId(mPrintService.getIconId(printer))
.setDescription(mPrintService.getDescription(mDiscoveredPrinter));
if (mCapabilities != null) {
diff --git a/src/com/android/bips/discovery/DiscoveredPrinter.java b/src/com/android/bips/discovery/DiscoveredPrinter.java
index 5d452af..6a135b8 100644
--- a/src/com/android/bips/discovery/DiscoveredPrinter.java
+++ b/src/com/android/bips/discovery/DiscoveredPrinter.java
@@ -26,6 +26,8 @@
import java.io.IOException;
import java.io.StringWriter;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
/** Represents a network-visible printer */
@@ -42,6 +44,9 @@
/** Resource path at which the print service can be reached */
public final Uri path;
+ /** All paths at which this this printer can be reached. Includes "path". */
+ public final List<Uri> paths;
+
/** Lazily-created printer id. */
private PrinterId mPrinterId;
@@ -50,14 +55,27 @@
*
* @param uuid Unique identification of the network printer, if known
* @param name Self-identified printer or service name
+ * @param paths One or more network paths at which the printer is currently available
+ * @param location Self-advertised location of the printer, if known
+ */
+ public DiscoveredPrinter(Uri uuid, String name, List<Uri> paths, String location) {
+ this.uuid = uuid;
+ this.name = name;
+ this.path = paths.get(0);
+ this.paths = Collections.unmodifiableList(paths);
+ this.location = location;
+ }
+
+ /**
+ * Construct minimal information about a network printer
+ *
+ * @param uuid Unique identification of the network printer, if known
+ * @param name Self-identified printer or service name
* @param path Network path at which the printer is currently available
* @param location Self-advertised location of the printer, if known
*/
public DiscoveredPrinter(Uri uuid, String name, Uri path, String location) {
- this.uuid = uuid;
- this.name = name;
- this.path = path;
- this.location = location;
+ this(uuid, name, Collections.singletonList(path), location);
}
/** Construct an object based on field values of an JSON object found next in the JsonReader */
@@ -91,6 +109,7 @@
this.uuid = uuid;
this.name = printerName;
this.path = path;
+ this.paths = Collections.singletonList(path);
this.location = location;
}
@@ -103,6 +122,18 @@
}
/**
+ * Return true if this printer has a secure (encrypted) path.
+ */
+ public boolean isSecure() {
+ for (Uri path : paths) {
+ if (path.getScheme().equals("ipps")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Return a host string for the user to see (an IP address or hostname without port number)
*/
public String getHost() {
@@ -139,7 +170,7 @@
DiscoveredPrinter other = (DiscoveredPrinter) obj;
return Objects.equals(uuid, other.uuid)
&& Objects.equals(name, other.name)
- && Objects.equals(path, other.path)
+ && Objects.equals(paths, other.paths)
&& Objects.equals(location, other.location);
}
@@ -148,7 +179,7 @@
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + (uuid != null ? uuid.hashCode() : 0);
- result = 31 * result + path.hashCode();
+ result = 31 * result + paths.hashCode();
result = 31 * result + (location != null ? location.hashCode() : 0);
return result;
}
diff --git a/src/com/android/bips/discovery/MultiDiscovery.java b/src/com/android/bips/discovery/MultiDiscovery.java
index d564db9..93f68b4 100644
--- a/src/com/android/bips/discovery/MultiDiscovery.java
+++ b/src/com/android/bips/discovery/MultiDiscovery.java
@@ -44,31 +44,50 @@
mChildListener = new Listener() {
@Override
public void onPrinterFound(DiscoveredPrinter printer) {
- printerFound(first(printer.getUri()));
+ printerFound(merged(printer.getUri()));
}
@Override
public void onPrinterLost(DiscoveredPrinter printer) {
// Merge remaining printer records, if any
- DiscoveredPrinter first = first(printer.getUri());
- if (first == null) {
+ DiscoveredPrinter remaining = merged(printer.getUri());
+ if (remaining == null) {
printerLost(printer.getUri());
} else {
- printerFound(first);
+ printerFound(remaining);
}
}
};
}
- /** For a given URI return the first matching record, based on discovery mechanism order */
- private DiscoveredPrinter first(Uri printerUri) {
+ /**
+ * For a given URI combine and return records with the same printerUri, based on discovery
+ * mechanism order.
+ */
+ private DiscoveredPrinter merged(Uri printerUri) {
+ DiscoveredPrinter combined = null;
+
for (Discovery discovery : getChildren()) {
- DiscoveredPrinter found = discovery.getPrinter(printerUri);
- if (found != null) {
- return found;
+ DiscoveredPrinter discovered = discovery.getPrinter(printerUri);
+ if (discovered != null) {
+ if (combined == null) {
+ combined = discovered;
+ } else {
+ // Merge all paths found, in order, without duplicates
+ List<Uri> allPaths = new ArrayList<>(combined.paths);
+ for (Uri path : discovered.paths) {
+ if (!allPaths.contains(path)) {
+ allPaths.add(path);
+ }
+ }
+ // Assemble a new printer containing paths.
+ combined = new DiscoveredPrinter(discovered.uuid, discovered.name, allPaths,
+ discovered.location);
+ }
}
}
- return null;
+
+ return combined;
}
@Override
diff --git a/src/com/android/bips/ipp/Backend.java b/src/com/android/bips/ipp/Backend.java
index 94d6a11..0e4affd 100644
--- a/src/com/android/bips/ipp/Backend.java
+++ b/src/com/android/bips/ipp/Backend.java
@@ -191,6 +191,10 @@
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)) {
diff --git a/src/com/android/bips/ipp/CapabilitiesCache.java b/src/com/android/bips/ipp/CapabilitiesCache.java
index dd4d258..7c361f1 100644
--- a/src/com/android/bips/ipp/CapabilitiesCache.java
+++ b/src/com/android/bips/ipp/CapabilitiesCache.java
@@ -1,6 +1,5 @@
/*
* 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.
@@ -23,7 +22,6 @@
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.p2p.WifiP2pManager;
-import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.util.LruCache;
@@ -48,8 +46,7 @@
* with the ability to fetch them on cache misses. {@link #close} must be called when use
* is complete.
*/
-public class CapabilitiesCache extends LruCache<Uri, LocalPrinterCapabilities> implements
- AutoCloseable {
+public class CapabilitiesCache implements AutoCloseable {
private static final String TAG = CapabilitiesCache.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -66,6 +63,9 @@
// Maximum time per retry before giving up on second pass. Must differ from FIRST_PASS_TIMEOUT.
private static final int SECOND_PASS_TIMEOUT = 8000;
+ // Underlying cache
+ private final LruCache<Uri, LocalPrinterCapabilities> mCache = new LruCache<>(CACHE_SIZE);
+
// Outstanding requests based on printer path
private final Map<Uri, Request> mRequests = new HashMap<>();
private final Set<Uri> mToEvict = new HashSet<>();
@@ -81,7 +81,6 @@
* @param maxConcurrent Maximum number of capabilities requests to make at any one time
*/
public CapabilitiesCache(BuiltInPrintService service, Backend backend, int maxConcurrent) {
- super(CACHE_SIZE);
if (DEBUG) Log.d(TAG, "CapabilitiesCache()");
mService = service;
@@ -96,7 +95,7 @@
// Evict specified device capabilities when P2P network is lost.
if (DEBUG) Log.d(TAG, "Evicting P2P " + mToEvictP2p);
for (Uri uri : mToEvictP2p) {
- remove(uri);
+ mCache.remove(uri);
}
mToEvictP2p.clear();
}
@@ -108,7 +107,7 @@
// Evict specified device capabilities when network is lost.
if (DEBUG) Log.d(TAG, "Evicting Wi-Fi " + mToEvict);
for (Uri uri : mToEvict) {
- remove(uri);
+ mCache.remove(uri);
}
mToEvict.clear();
}
@@ -173,7 +172,20 @@
* Returns capabilities for the specified printer, if known
*/
public LocalPrinterCapabilities get(DiscoveredPrinter printer) {
- return get(printer.path);
+ LocalPrinterCapabilities capabilities = mCache.get(printer.path);
+ // Populate certificate from store if possible
+ if (capabilities != null) {
+ capabilities.certificate = mService.getCertificateStore().get(capabilities.uuid);
+ }
+ return capabilities;
+ }
+
+ /**
+ * Remove capabilities corresponding to a Printer URI
+ * @return The removed capabilities, if any
+ */
+ public LocalPrinterCapabilities remove(Uri printerUri) {
+ return mCache.remove(printerUri);
}
/**
@@ -281,10 +293,11 @@
startNextRequest();
return;
} else {
- remove(printer.getUri());
+ mCache.remove(printer.getUri());
}
} else {
- put(printer.path, capabilities);
+ capabilities.certificate = mService.getCertificateStore().get(capabilities.uuid);
+ mCache.put(printer.path, capabilities);
}
LocalPrinterCapabilities result = capabilities;
diff --git a/src/com/android/bips/ipp/CertificateStore.java b/src/com/android/bips/ipp/CertificateStore.java
new file mode 100644
index 0000000..605f711
--- /dev/null
+++ b/src/com/android/bips/ipp/CertificateStore.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * 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.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.bips.BuiltInPrintService;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A persistent cache of certificate public keys known to be associated with certain printer
+ * UUIDs.
+ */
+public class CertificateStore {
+ private static final String TAG = CertificateStore.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ /** File location of the on-disk certificate store. */
+ private final File mStoreFile;
+
+ /** RAM-based store of certificates (UUID to certificate) */
+ private final Map<String, byte[]> mCertificates = new HashMap<>();
+
+ public CertificateStore(BuiltInPrintService service) {
+ mStoreFile = new File(service.getCacheDir(), getClass().getSimpleName() + ".json");
+ load();
+ }
+
+ /** Write a new, non-null certificate to the store. */
+ public void put(String uuid, byte[] certificate) {
+ byte[] oldCertificate = mCertificates.put(uuid, certificate);
+ if (oldCertificate == null || !Arrays.equals(oldCertificate, certificate)) {
+ // Cache the certificate for later
+ if (DEBUG) Log.d(TAG, "New certificate uuid=" + uuid + " len=" + certificate.length);
+ save();
+ }
+ }
+
+ /** Return the known certificate public key for a printer having the specified UUID, or null. */
+ public byte[] get(String uuid) {
+ return mCertificates.get(uuid);
+ }
+
+ /** Write to storage immediately. */
+ private void save() {
+ if (mStoreFile.exists()) {
+ mStoreFile.delete();
+ }
+
+ try (JsonWriter writer = new JsonWriter(new BufferedWriter(new FileWriter(mStoreFile)))) {
+ writer.beginObject();
+ writer.name("certificates");
+ writer.beginArray();
+ for (Map.Entry<String, byte[]> entry : mCertificates.entrySet()) {
+ writer.beginObject();
+ writer.name("uuid").value(entry.getKey());
+ writer.name("pubkey").value(bytesToHex(entry.getValue()));
+ writer.endObject();
+ }
+ writer.endArray();
+ writer.endObject();
+ if (DEBUG) Log.d(TAG, "Wrote " + mCertificates.size() + " certificates to store");
+ } catch (NullPointerException | IOException e) {
+ Log.w(TAG, "Error while storing to " + mStoreFile, e);
+ }
+ }
+
+ /** Load known certificates from storage into RAM. */
+ private void load() {
+ if (!mStoreFile.exists()) {
+ return;
+ }
+
+ try (JsonReader reader = new JsonReader(new BufferedReader(new FileReader(mStoreFile)))) {
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String itemName = reader.nextName();
+ if (itemName.equals("certificates")) {
+ reader.beginArray();
+ while (reader.hasNext()) {
+ loadItem(reader);
+ }
+ reader.endArray();
+ } else {
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ } catch (IllegalStateException | IOException error) {
+ Log.w(TAG, "Error while loading from " + mStoreFile, error);
+ }
+ if (DEBUG) Log.d(TAG, "Loaded size=" + mCertificates.size() + " from " + mStoreFile);
+ }
+
+ /** Load a single certificate entry into RAM. */
+ private void loadItem(JsonReader reader) throws IOException {
+ String uuid = null;
+ byte[] pubkey = null;
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String itemName = reader.nextName();
+ switch(itemName) {
+ case "uuid":
+ uuid = reader.nextString();
+ break;
+ case "pubkey":
+ try {
+ pubkey = hexToBytes(reader.nextString());
+ } catch (IllegalArgumentException ignored) {
+ }
+ break;
+ default:
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ if (uuid != null && pubkey != null) {
+ mCertificates.put(uuid, pubkey);
+ }
+ }
+
+ private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
+
+ /** Converts a byte array to a hexadecimal string, or null if bytes are null. */
+ private static String bytesToHex(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; i++) {
+ int b = bytes[i] & 0xFF;
+ hexChars[i * 2] = HEX_CHARS[b >>> 4];
+ hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ /** Converts a hexadecimal string to a byte array, or null if hexString is null. */
+ private static byte[] hexToBytes(String hexString) {
+ if (hexString == null) {
+ return null;
+ }
+
+ char[] source = hexString.toCharArray();
+ byte[] dest = new byte[source.length / 2];
+ for (int sourcePos = 0, destPos = 0; sourcePos < source.length; ) {
+ int hi = Character.digit(source[sourcePos++], 16);
+ int lo = Character.digit(source[sourcePos++], 16);
+ if ((hi < 0) || (lo < 0)) {
+ throw new IllegalArgumentException();
+ }
+ dest[destPos++] = (byte) (hi << 4 | lo);
+ }
+ return dest;
+ }
+}
diff --git a/src/com/android/bips/ipp/JobStatus.java b/src/com/android/bips/ipp/JobStatus.java
index 72be347..b2d07e2 100644
--- a/src/com/android/bips/ipp/JobStatus.java
+++ b/src/com/android/bips/ipp/JobStatus.java
@@ -53,12 +53,15 @@
R.string.printer_low_on_toner);
sBlockReasonsMap.put(BackendConstants.BLOCKED_REASON__BUSY, R.string.printer_busy);
sBlockReasonsMap.put(BackendConstants.BLOCKED_REASON__OFFLINE, R.string.printer_offline);
+ sBlockReasonsMap.put(BackendConstants.BLOCKED_REASON__BAD_CERTIFICATE,
+ R.string.printer_bad_certificate);
}
private int mId;
private String mJobState;
private String mJobResult;
private final Set<String> mBlockedReasons;
+ private byte[] mCertificate;
/** Create a new, blank job status */
public JobStatus() {
@@ -72,6 +75,7 @@
mJobState = other.mJobState;
mJobResult = other.mJobResult;
mBlockedReasons = other.mBlockedReasons;
+ mCertificate = other.mCertificate;
}
/** Returns a string resource ID corresponding to a blocked reason, or 0 if none found */
@@ -104,12 +108,18 @@
return !TextUtils.isEmpty(mJobResult);
}
+ /** Return certificate if supplied as part of this status. */
+ public byte[] getCertificate() {
+ return mCertificate;
+ }
+
@Override
public String toString() {
return "JobStatus{id=" + mId
+ ", jobState=" + mJobState
+ ", jobResult=" + mJobResult
+ ", blockedReasons=" + mBlockedReasons
+ + ", certificate=" + (mCertificate != null)
+ "}";
}
@@ -139,6 +149,11 @@
return this;
}
+ Builder setCertificate(byte[] certificate) {
+ mPrototype.mCertificate = certificate;
+ return this;
+ }
+
Builder clearBlockedReasons() {
mPrototype.mBlockedReasons.clear();
return this;
diff --git a/src/com/android/bips/jni/BackendConstants.java b/src/com/android/bips/jni/BackendConstants.java
index a734a46..9960a47 100644
--- a/src/com/android/bips/jni/BackendConstants.java
+++ b/src/com/android/bips/jni/BackendConstants.java
@@ -67,5 +67,6 @@
public static final String BLOCKED_REASON__LOW_ON_INK = "marker-ink-almost-empty";
public static final String BLOCKED_REASON__LOW_ON_TONER = "marker-toner-almost-empty";
public static final String BLOCKED_REASON__REALLY_LOW_ON_INK = "marker-ink-really-low";
+ public static final String BLOCKED_REASON__BAD_CERTIFICATE = "bad-certificate";
public static final String BLOCKED_REASON__UNKNOWN = "unknown";
}
diff --git a/src/com/android/bips/jni/JobCallbackParams.java b/src/com/android/bips/jni/JobCallbackParams.java
index 97badec..f0b9aee 100644
--- a/src/com/android/bips/jni/JobCallbackParams.java
+++ b/src/com/android/bips/jni/JobCallbackParams.java
@@ -24,4 +24,5 @@
public String jobState;
public String jobDoneResult;
public String[] blockedReasons;
+ public byte[] certificate;
}
diff --git a/src/com/android/bips/jni/LocalPrinterCapabilities.java b/src/com/android/bips/jni/LocalPrinterCapabilities.java
index f30b24a..332ce5d 100644
--- a/src/com/android/bips/jni/LocalPrinterCapabilities.java
+++ b/src/com/android/bips/jni/LocalPrinterCapabilities.java
@@ -54,6 +54,9 @@
/** Bears the underlying native C structure (printer_capabilities_t) or null if not present */
public byte[] nativeData;
+ /** Public key of certificate for this printer, if known */
+ public byte[] certificate;
+
public void buildCapabilities(BuiltInPrintService service,
PrinterCapabilitiesInfo.Builder builder) {
builder.setColorModes(
@@ -122,6 +125,7 @@
+ " supportedMediaTypes=" + Arrays.toString(supportedMediaTypes)
+ " supportedMediaSizes=" + Arrays.toString(supportedMediaSizes)
+ " inetAddress=" + inetAddress
+ + " certificate=" + (certificate != null)
+ "}";
}
}
diff --git a/src/com/android/bips/ui/PrinterPreference.java b/src/com/android/bips/ui/PrinterPreference.java
index 36cce3c..9b048b4 100644
--- a/src/com/android/bips/ui/PrinterPreference.java
+++ b/src/com/android/bips/ui/PrinterPreference.java
@@ -47,7 +47,7 @@
}
setSummary(mPrintService.getDescription(printer));
- setIcon(R.drawable.ic_printer);
+ setIcon(mPrintService.getIconId(printer));
}
DiscoveredPrinter getPrinter() {