DO NOT MERGE - Merge pi-platform-release (PPRL.190205.001) into stage-aosp-master

Bug: 124234733
Change-Id: I91108660492aff52d7e9df127256b2e9a4b62c0b
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..32d8056
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2016 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.
+
+android_app {
+    name: "BuiltInPrintService",
+    srcs: [
+        "src/**/*.java",
+        "src/**/I*.aidl",
+    ],
+    resource_dirs: ["res"],
+    sdk_version: "system_current",
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+    },
+    jni_libs: [
+        "libwfds",
+        "libcups",
+    ],
+}
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index beb246d..0000000
--- a/Android.mk
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (C) 2016 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.
-
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src)
-
-LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res
-
-LOCAL_PACKAGE_NAME := BuiltInPrintService
-
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-
-LOCAL_JNI_SHARED_LIBRARIES := libwfds libcups
-
-include $(BUILD_PACKAGE)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/jni/Android.bp b/jni/Android.bp
new file mode 100644
index 0000000..d1c9ef9
--- /dev/null
+++ b/jni/Android.bp
@@ -0,0 +1,71 @@
+// 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.
+
+cc_library_shared {
+    name: "libwfds",
+
+    sdk_version: "current",
+
+    cflags: [
+        "-DINCLUDE_PDF=1",
+        "-Werror",
+        "-Wextra",
+        "-Wno-unused-parameter",
+        "-Wno-sign-compare",
+        "-Wno-missing-field-initializers",
+        "-Wno-implicit-function-declaration",
+        "-Wno-format",
+        "-Wno-missing-braces",
+    ],
+
+    srcs: [
+        "lib/lib_wprint.c",
+        "lib/plugin_db.c",
+        "lib/printable_area.c",
+        "lib/printer.c",
+        "lib/wprint_msgq.c",
+        "lib/wprintJNI.c",
+
+        "ipphelper/ipp_print.c",
+        "ipphelper/ipphelper.c",
+        "ipphelper/ippstatus_capabilities.c",
+        "ipphelper/ippstatus_monitor.c",
+
+        "plugins/lib_pclm.c",
+        "plugins/lib_pwg.c",
+        "plugins/genPCLm/src/genPCLm.cpp",
+        "plugins/genPCLm/src/genJPEGStrips.cpp",
+        "plugins/pdf_render.c",
+        "plugins/plugin_pcl.c",
+        "plugins/plugin_pdf.c",
+        "plugins/pclm_wrapper_api.cpp",
+        "plugins/wprint_image.c",
+        "plugins/wprint_image_platform.c",
+        "plugins/wprint_mupdf.c",
+        "plugins/wprint_scaler.c",
+    ],
+
+    local_include_dirs: [
+        "include",
+        "plugins/genPCLm/inc",
+        "ipphelper",
+    ],
+    static_libs: ["libjpeg_static_ndk"],
+    shared_libs: [
+        "libcups",
+        "liblog",
+        "libz",
+    ],
+}
diff --git a/jni/Android.mk b/jni/Android.mk
deleted file mode 100644
index d57c115..0000000
--- a/jni/Android.mk
+++ /dev/null
@@ -1,60 +0,0 @@
-# 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.
-
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-INCLUDE_DIR    := include
-LIB_DIR        := lib
-PLUGINS_DIR    := plugins
-IPP_HELPER_DIR := ipphelper
-
-LOCAL_SDK_VERSION := current
-
-LOCAL_CFLAGS += \
-      -DINCLUDE_PDF=1 -Werror -Wextra -Wno-unused-parameter \
-      -Wno-sign-compare -Wno-missing-field-initializers \
-      -Wno-implicit-function-declaration -Wno-format -Wno-missing-braces
-
-PLUGINS_SRCS := \
-      $(PLUGINS_DIR)/lib_pclm.c $(PLUGINS_DIR)/lib_pwg.c \
-      $(PLUGINS_DIR)/genPCLm/src/genPCLm.cpp \
-      $(PLUGINS_DIR)/genPCLm/src/genJPEGStrips.cpp \
-      $(PLUGINS_DIR)/pdf_render.c $(PLUGINS_DIR)/plugin_pcl.c \
-      $(PLUGINS_DIR)/plugin_pdf.c $(PLUGINS_DIR)/pclm_wrapper_api.cpp \
-      $(PLUGINS_DIR)/wprint_image.c $(PLUGINS_DIR)/wprint_image_platform.c \
-      $(PLUGINS_DIR)/wprint_mupdf.c $(PLUGINS_DIR)/wprint_scaler.c
-
-LIB_SRCS := \
-      $(LIB_DIR)/lib_wprint.c $(LIB_DIR)/plugin_db.c \
-      $(LIB_DIR)/printable_area.c $(LIB_DIR)/printer.c \
-      $(LIB_DIR)/wprint_msgq.c $(LIB_DIR)/wprintJNI.c
-
-IPP_HELPER_SRCS := \
-      $(IPP_HELPER_DIR)/ipp_print.c $(IPP_HELPER_DIR)/ipphelper.c \
-      $(IPP_HELPER_DIR)/ippstatus_capabilities.c \
-      $(IPP_HELPER_DIR)/ippstatus_monitor.c
-
-LOCAL_SRC_FILES:= \
-      $(LIB_SRCS) $(IPP_HELPER_SRCS) $(PLUGINS_SRCS)
-
-LOCAL_C_INCLUDES += \
-      $(LOCAL_PATH)/$(INCLUDE_DIR) $(LOCAL_PATH)/$(PLUGINS_DIR)/genPCLm/inc \
-      $(LOCAL_PATH)/$(IPP_HELPER_DIR)
-LOCAL_STATIC_LIBRARIES := libjpeg_static_ndk
-LOCAL_SHARED_LIBRARIES := libcups liblog libz
-LOCAL_MODULE := libwfds
-LOCAL_MODULE_TAGS := optional
-include $(BUILD_SHARED_LIBRARY)
diff --git a/jni/include/lib_wprint.h b/jni/include/lib_wprint.h
index 006f6c1..0d2fd12 100644
--- a/jni/include/lib_wprint.h
+++ b/jni/include/lib_wprint.h
@@ -184,10 +184,15 @@
     bool accepts_pclm;
     bool accepts_pdf;
     bool copies_supported;
+    int print_quality;
     const char *useragent;
     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 +203,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 +235,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/printer_capabilities_types.h b/jni/include/printer_capabilities_types.h
index 0f05b35..677f19c 100644
--- a/jni/include/printer_capabilities_types.h
+++ b/jni/include/printer_capabilities_types.h
@@ -22,6 +22,7 @@
 #define MAX_MEDIA_TRAYS_SUPPORTED 10
 #define MAX_MEDIA_TYPES_SUPPORTED 20
 #define MAX_RESOLUTIONS_SUPPORTED 10
+#define MAX_QUALITY_SUPPORTED 3
 #define MAX_URI_LENGTH 1024
 #define MAX_STRING 256
 #define MAX_UUID 46
@@ -43,6 +44,8 @@
     char location[MAX_STRING];
     unsigned char canRotateDuplexBackPage;
     unsigned char color;
+    int supportedQuality[MAX_QUALITY_SUPPORTED];
+    unsigned int numSupportedQuality;
     unsigned char faceDownTray;
     media_size_t supportedMediaSizes[MAX_SIZES_SUPPORTED];
     unsigned int numSupportedMediaSizes;
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/ipp_print.c b/jni/ipphelper/ipp_print.c
index 57914b3..36b7015 100644
--- a/jni/ipphelper/ipp_print.c
+++ b/jni/ipphelper/ipp_print.c
@@ -293,6 +293,12 @@
         ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_INTEGER, "copies", job_params->num_copies);
     }
 
+    // Add print quality if requested
+    if (job_params->print_quality) {
+        ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_ENUM, "print-quality",
+                job_params->print_quality);
+    }
+
     ippAddResolution(request, IPP_TAG_JOB, "printer-resolution", IPP_RES_PER_INCH,
             job_params->pixel_units, job_params->pixel_units);
     if (job_params->duplex == DUPLEX_MODE_BOOK) {
diff --git a/jni/ipphelper/ipphelper.c b/jni/ipphelper/ipphelper.c
index 60758ac..f45060e 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.
@@ -786,6 +784,15 @@
             }
         }
     }
+    if ((attrptr = ippFindAttribute(response, "print-quality-supported", IPP_TAG_ENUM)) !=
+            NULL) {
+        for (i = 0; i < ippGetCount(attrptr) && capabilities->numSupportedQuality
+                < MAX_QUALITY_SUPPORTED; i++) {
+            LOGD("print-quality-supported: %d", ippGetInteger(attrptr, i));
+            capabilities->supportedQuality[capabilities->numSupportedQuality++] =
+                ippGetInteger(attrptr, i);
+        }
+    }
 
     char imagePCLm[] = "application/PCLm";
     char imagePWG[] = "image/pwg-raster";
@@ -1171,11 +1178,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 +1227,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/ipphelper/ippstatus_capabilities.c b/jni/ipphelper/ippstatus_capabilities.c
index 2c34861..ad784d6 100644
--- a/jni/ipphelper/ippstatus_capabilities.c
+++ b/jni/ipphelper/ippstatus_capabilities.c
@@ -55,6 +55,7 @@
         "media-type-supported",
         "output-bin-supported",
         "print-color-mode-supported",
+        "print-quality-supported",
         "printer-resolution-supported",
         "sides-supported",
         "printer-device-id",
diff --git a/jni/lib/lib_wprint.c b/jni/lib/lib_wprint.c
index e7aebfe..b8bfbee 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;
                             }
@@ -1371,6 +1455,16 @@
 }
 
 /*
+ * Return true if the specified int array of the supplied length contains a value.
+ */
+static bool int_array_contains(const int *array, int length, int value) {
+    for (int i = 0; i < length; i++) {
+        if (array[i] == value) return true;
+    }
+    return false;
+}
+
+/*
  * Checks printers reported media sizes and validates that wprint supports them
  */
 static void _validate_supported_media_sizes(printer_capabilities_t *printer_cap) {
@@ -1639,6 +1733,12 @@
         job_params->num_copies = 1;
     }
 
+    // If printing photo and HIGH quality is supported, specify it.
+    if (strcasecmp(job_params->docCategory, "photo") == 0 && int_array_contains(
+            printer_cap->supportedQuality, printer_cap->numSupportedQuality, IPP_QUALITY_HIGH)) {
+        job_params->print_quality = IPP_QUALITY_HIGH;
+    }
+
     // confirm that the media size is supported
     for (i = 0; i < printer_cap->numSupportedMediaSizes; i++) {
         if (job_params->media_size == printer_cap->supportedMediaSizes[i]) {
@@ -2047,6 +2147,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, &params);
     _convertPrinterCaps_to_C(env, printerCaps, &caps);
+    _convertCertificate(env, printerCaps, &params);
 
     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/jni/plugins/lib_pwg.c b/jni/plugins/lib_pwg.c
index 5f60a10..8e3e525 100644
--- a/jni/plugins/lib_pwg.c
+++ b/jni/plugins/lib_pwg.c
@@ -70,7 +70,6 @@
         h->PageSize[1] = (int) h->cupsPageSize[1];
         h->Separations = CUPS_TRUE;
         h->TraySwitch = CUPS_TRUE;
-        h->Tumble = CUPS_TRUE;
         h->cupsWidth = pixel_width;
         h->cupsHeight = pixel_height;
         h->cupsBitsPerPixel = (monochrome ? 8 : 24);
@@ -219,12 +218,15 @@
     if (duplex == DUPLEX_MODE_BOOK) {
         job_info->pclm_page_info.duplexDisposition = duplex_longEdge;
         header_pwg.Duplex = CUPS_TRUE;
+        header_pwg.Tumble = CUPS_FALSE;
     } else if (duplex == DUPLEX_MODE_TABLET) {
         job_info->pclm_page_info.duplexDisposition = duplex_shortEdge;
         header_pwg.Duplex = CUPS_TRUE;
+        header_pwg.Tumble = CUPS_TRUE;
     } else {
         job_info->pclm_page_info.duplexDisposition = simplex;
         header_pwg.Duplex = CUPS_FALSE;
+        header_pwg.Tumble = CUPS_FALSE;
     }
 
     job_info->pclm_page_info.mirrorBackside = false;
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
deleted file mode 100644
index 343cf1f..0000000
--- a/res/values-as/strings.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-     Copyright (C) 2016 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.
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_name" msgid="3551052199033657984">"ডিফ\'ল্ট প্ৰিণ্ট সেৱা"</string>
-    <string name="printer_busy" msgid="8604311528104955859">"ব্যস্ত"</string>
-    <string name="printer_out_of_paper" msgid="4882186432807703877">"কাগজ শেষ হৈছে"</string>
-    <string name="printer_out_of_ink" msgid="7361897651097675464">"চিয়াহী শেষ হৈছে"</string>
-    <string name="printer_out_of_toner" msgid="2077516357225364154">"ট\'নাৰ শেষ হৈছে"</string>
-    <string name="printer_low_on_ink" msgid="3515015872393897705">"চিয়াহী কম আছে"</string>
-    <string name="printer_low_on_toner" msgid="8807858294038587130">"ট\'নাৰ কম আছে"</string>
-    <string name="printer_door_open" msgid="2446302931916940874">"প্ৰিণ্টাৰৰ দুৱাৰ খোল খাই আছে"</string>
-    <string name="printer_jammed" msgid="5104099859384749499">"প্ৰিণ্টাৰত জাম লাগিছে"</string>
-    <string name="printer_offline" msgid="9196864753298645066">"প্ৰিণ্টাৰ অফলাইন হৈ আছে"</string>
-    <string name="printer_check" msgid="6428369671197132828">"প্ৰিণ্টাৰ পৰীক্ষা কৰক"</string>
-    <string name="waiting_to_send" msgid="2022115199902822180">"পঠাবলৈ ৰৈ থকা হৈছে"</string>
-    <string name="unreadable_input" msgid="2199948329556527912">"দস্তাবেজ পঢ়িবপৰা নগ\'ল"</string>
-    <string name="media_size_l" msgid="164416542021598181">"L"</string>
-    <string name="media_size_5x7in" msgid="1275128379533195285">"৫x৭ ইঞ্চি"</string>
-    <string name="media_size_89x119mm" msgid="6828167243395807385">"৮৯x১১৯ মি. মি."</string>
-    <string name="media_size_54x86mm" msgid="1301991206183343895">"৫৪x৮৬ মি. মি."</string>
-    <string name="media_size_8x10in" msgid="1872576638522812402">"৮x১০ ইঞ্চি"</string>
-    <string name="media_size_4x6in" msgid="3093276425529958253">"৪x৬ ইঞ্চি"</string>
-    <string name="printer_description" msgid="8580767673213837142">"%1$s: %2$s"</string>
-    <string name="title_activity_add_printer" msgid="9119216095769228566">"প্ৰিণ্টাৰ যোগ কৰক"</string>
-    <string name="add_printer_by_ip" msgid="562864787592910327">"আইপি ঠিকনাৰ জৰিয়তে প্ৰিণ্টাৰ যোগ কৰক"</string>
-    <string name="hostname_or_ip" msgid="3460546103553992915">"হ\'ষ্টৰ নাম বা আইপি ঠিকনা"</string>
-    <string name="ip_hint" msgid="7939777481941979799">"192.168.0.4"</string>
-    <string name="add" msgid="1950342261671100906">"যোগ দিয়ক"</string>
-    <string name="add_named" msgid="9074106244018070583">"প্ৰিণ্টাৰ যোগ কৰক <xliff:g id="PRINTER">%1$s</xliff:g>"</string>
-    <string name="no_printer_found" msgid="4777867380924351173">"এই ঠিকনাত কোনো প্ৰিণ্টাৰ পোৱা নগ\'ল"</string>
-    <string name="printer_not_supported" msgid="281955849350938408">"এই প্ৰিণ্টাৰটো নচলে"</string>
-    <string name="wifi_direct" msgid="4629404342852294985">"ৱাই-ফাই ডাইৰেক্ট"</string>
-    <string name="find_wifi_direct" msgid="5270504288829123954">"ৱাই-ফাই ডাইৰেক্ট প্ৰিণ্টাৰ বিচাৰক"</string>
-    <string name="wifi_direct_printers" msgid="541168032444693191">"ৱাই-ফাই ডাইৰেক্ট প্ৰিণ্টাৰ"</string>
-    <string name="searching" msgid="2114018057619514587">"সন্ধান কৰি থকা হৈছে…"</string>
-    <string name="connect_hint_text" msgid="587112503851044234">"আপোনাৰ প্ৰিণ্টাৰৰ সন্মুখৰ পেনেলত আপুনি এই সংযোগটো অনুমোদন কৰিবলগীয়া হ\'ব পাৰে"</string>
-    <string name="connecting_to" msgid="2665161014972086194">"<xliff:g id="PRINTER">%1$s</xliff:g>ৰ লগত সংযোগ কৰি থকা হৈছে"</string>
-    <string name="failed_printer_connection" msgid="4196305972749960362">"প্ৰিণ্টাৰৰ লগত সংযোগ স্থাপন কৰিব পৰা নগ\'ল"</string>
-    <string name="failed_connection" msgid="8068661997318286575">"<xliff:g id="PRINTER">%1$s</xliff:g>ৰ লগত সংযোগ স্থাপন কৰিব পৰা নগ\'ল"</string>
-    <string name="saved_printers" msgid="4567534965213125526">"ছেভ কৰি ৰখা প্ৰিণ্টাৰ"</string>
-    <string name="forget" msgid="892068061425802502">"পাহৰক"</string>
-    <string name="connects_via_wifi_direct" msgid="652300632780158437">"ৱাই-ফাই ডাইৰেক্টৰ জৰিয়তে সংযোগ কৰে"</string>
-    <string name="connects_via_network" msgid="5990041581556733898">"<xliff:g id="IP_ADDRESS">%1$s</xliff:g> ঠিকনাত চলিত নেটৱৰ্কৰ জৰিয়তে সংযোগ কৰে"</string>
-</resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
deleted file mode 100644
index f927213..0000000
--- a/res/values-or/strings.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-     Copyright (C) 2016 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.
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_name" msgid="3551052199033657984">"ଡିଫଲ୍ଟ ପ୍ରିଣ୍ଟ ସେବା"</string>
-    <string name="printer_busy" msgid="8604311528104955859">"ବ୍ୟସ୍ତ"</string>
-    <string name="printer_out_of_paper" msgid="4882186432807703877">"ପେପର୍‍ ଶେଷ ହୋଇଯାଇଛି"</string>
-    <string name="printer_out_of_ink" msgid="7361897651097675464">"ଇଙ୍କ ଶେଷ ହୋଇଯାଇଛି"</string>
-    <string name="printer_out_of_toner" msgid="2077516357225364154">"ଟୋନର୍‍ ନାହିଁ"</string>
-    <string name="printer_low_on_ink" msgid="3515015872393897705">"ଇଙ୍କ କମ୍‍ ଅଛି"</string>
-    <string name="printer_low_on_toner" msgid="8807858294038587130">"ଟୋନର୍‍ କମ୍‍ ଅଛି"</string>
-    <string name="printer_door_open" msgid="2446302931916940874">"ଡୋର୍‍ ଖୋଲା ଅଛି"</string>
-    <string name="printer_jammed" msgid="5104099859384749499">"ଜାମ୍‍ ହୋଇଯାଇଛି"</string>
-    <string name="printer_offline" msgid="9196864753298645066">"ଅଫଲାଇନ୍"</string>
-    <string name="printer_check" msgid="6428369671197132828">"ପ୍ରିଣ୍ଟର୍‍ ଖୋଜନ୍ତୁ"</string>
-    <string name="waiting_to_send" msgid="2022115199902822180">"ପଠାଇବାକୁ ଅପେକ୍ଷା କରାଯାଉଛି"</string>
-    <string name="unreadable_input" msgid="2199948329556527912">"ଡକ୍ୟୁମେଣ୍ଟ ପଢ଼ିପାରିଲା ନାହିଁ"</string>
-    <string name="media_size_l" msgid="164416542021598181">"L"</string>
-    <string name="media_size_5x7in" msgid="1275128379533195285">"5x7 ଇଞ୍ଚ"</string>
-    <string name="media_size_89x119mm" msgid="6828167243395807385">"89x119 ମିମି"</string>
-    <string name="media_size_54x86mm" msgid="1301991206183343895">"54x86 ମିମି"</string>
-    <string name="media_size_8x10in" msgid="1872576638522812402">"8x10 ଇଞ୍ଚ"</string>
-    <string name="media_size_4x6in" msgid="3093276425529958253">"4x6 ଇଞ୍ଚ"</string>
-    <string name="printer_description" msgid="8580767673213837142">"%1$s – %2$s"</string>
-    <string name="title_activity_add_printer" msgid="9119216095769228566">"ପ୍ରିଣ୍ଟର୍‌ ଯୋଡ଼ନ୍ତୁ"</string>
-    <string name="add_printer_by_ip" msgid="562864787592910327">"IP ଠିକଣା ମାଧ୍ୟମରେ ପ୍ରିଣ୍ଟର୍‌କୁ ଯୋଡ଼ନ୍ତୁ"</string>
-    <string name="hostname_or_ip" msgid="3460546103553992915">"ହୋଷ୍ଟନେମ୍‍ କିମ୍ବା IP ଠିକଣା"</string>
-    <string name="ip_hint" msgid="7939777481941979799">"192.168.0.4"</string>
-    <string name="add" msgid="1950342261671100906">"ଯୋଡ଼ନ୍ତୁ"</string>
-    <string name="add_named" msgid="9074106244018070583">"<xliff:g id="PRINTER">%1$s</xliff:g>କୁ ଯୋଡ଼ନ୍ତୁ"</string>
-    <string name="no_printer_found" msgid="4777867380924351173">"ଏହି ଠିକଣାରେ କୌଣସି ପ୍ରିଣ୍ଟର ମିଳିଲା ନାହିଁ"</string>
-    <string name="printer_not_supported" msgid="281955849350938408">"ପ୍ରିଣ୍ଟର୍‍ ସପୋର୍ଟ କରୁନାହିଁ"</string>
-    <string name="wifi_direct" msgid="4629404342852294985">"ୱାଇ-ଫାଇ ଡାଇରେକ୍ଟ"</string>
-    <string name="find_wifi_direct" msgid="5270504288829123954">"ୱାଇ-ଫାଇ ଡାଇରେକ୍ଟ ପ୍ରିଣ୍ଟର୍‌ ଖୋଜନ୍ତୁ"</string>
-    <string name="wifi_direct_printers" msgid="541168032444693191">"ୱାଇ-ଫାଇ ଡାଇରେକ୍ଟ ପ୍ରିଣ୍ଟର୍"</string>
-    <string name="searching" msgid="2114018057619514587">"ଖୋଜୁଛି…"</string>
-    <string name="connect_hint_text" msgid="587112503851044234">"ପ୍ରିଣ୍ଟର୍‌ର ସାମ୍ନା ପ୍ୟାନେଲ୍‌ରୁ ଆପଣ ଏହି କନେକ୍ସନ୍‌କୁ ଅନୁମୋଦନ କରିପାରିବେ"</string>
-    <string name="connecting_to" msgid="2665161014972086194">"<xliff:g id="PRINTER">%1$s</xliff:g> ସହିତ କନେକ୍ଟ ହେଉଛି"</string>
-    <string name="failed_printer_connection" msgid="4196305972749960362">"ପ୍ରିଣ୍ଟର ସହିତ କନେକ୍ଟ ହେଲାନାହିଁ"</string>
-    <string name="failed_connection" msgid="8068661997318286575">"<xliff:g id="PRINTER">%1$s</xliff:g> ସହିତ କନେକ୍ଟ ହେଲାନାହିଁ"</string>
-    <string name="saved_printers" msgid="4567534965213125526">"ସେଭ୍ ହୋଇଥିବା ପ୍ରିଣ୍ଟର୍"</string>
-    <string name="forget" msgid="892068061425802502">"ଭୁଲିଯାଆନ୍ତୁ"</string>
-    <string name="connects_via_wifi_direct" msgid="652300632780158437">"ୱାଇ-ଫାଇ ଡାଇରେକ୍ଟ ମାଧ୍ୟମରେ କନେକ୍ଟ ହେବ"</string>
-    <string name="connects_via_network" msgid="5990041581556733898">"<xliff:g id="IP_ADDRESS">%1$s</xliff:g>ରେ ବର୍ତ୍ତମାନର ନେଟ୍‌ୱର୍କ ମାଧ୍ୟମରେ କନେକ୍ଟ କରାଯାଇଥାଏ"</string>
-</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eac63ce..2ff94c6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -29,6 +29,8 @@
     <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_not_encrypted">Not encrypted</string>
     <string name="printer_check">Check printer</string>
     <string name="waiting_to_send">Waiting to send</string>
 
@@ -81,5 +83,16 @@
     <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>
+    <!-- Message shown in notification if a printer no longer supports encryption [CHAR LIMIT=UNLIMITED] -->
+    <string name="not_encrypted_request">This printer no longer accepts encrypted jobs. Continue printing?</string>
+    <!-- Button label in a notification. This button accepts a printer security change [CHAR LIMIT=20] -->
+    <string name="accept">Accept</string>
+    <!-- Button label in a notification. This button rejects a printer security change [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..9519962 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() {
@@ -241,4 +270,96 @@
             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.
+     *
+     * When certificate is null, the printer is being downgraded to no-encryption.
+     */
+    void notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid,
+                                 byte[] certificate) {
+        String message;
+        if (certificate == null) {
+            message = getString(R.string.not_encrypted_request);
+        } else {
+            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_PRINTER_UUID, printerUuid)
+                .putExtra(EXTRA_PRINTER_ID, printerId);
+        if (certificate != null) {
+            acceptIntent.putExtra(EXTRA_CERTIFICATE, certificate);
+        }
+        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);
+            if (certificate != null) {
+                mCertificateStore.put(printerUuid, certificate);
+            } else {
+                mCertificateStore.remove(printerUuid);
+            }
+            // Restart the job with the updated 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..adadd14 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;
@@ -42,6 +43,7 @@
         CapabilitiesCache.OnLocalPrinterCapabilities {
     private static final String TAG = LocalPrintJob.class.getSimpleName();
     private static final boolean DEBUG = false;
+    private static final String IPPS_SCHEME = "ipps";
 
     /** Maximum time to wait to find a printer before failing the job */
     private static final int DISCOVERY_TIMEOUT = 2 * 60 * 1000;
@@ -51,8 +53,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_SECURITY = 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 +66,8 @@
     private Uri mPath;
     private DelayedAction mDiscoveryTimeout;
     private P2pPrinterConnection mConnection;
+    private LocalPrinterCapabilities mCapabilities;
+    private CertificateStore mCertificateStore;
 
     /**
      * Construct the object; use {@link #start(Consumer)} to begin job processing.
@@ -71,6 +76,7 @@
         mPrintService = printService;
         mBackend = backend;
         mPrintJob = printJob;
+        mCertificateStore = mPrintService.getCertificateStore();
         mState = STATE_INIT;
 
         // Tell the job it is blocked (until start())
@@ -107,12 +113,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_SECURITY) {
+            mCapabilities.certificate = mCertificateStore.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_SECURITY:
                 // Cancel immediately
                 mState = STATE_CANCEL;
                 finish(false, null);
@@ -156,6 +174,14 @@
         mPrintService.getDiscovery().stop(this);
         mState = STATE_CAPABILITIES;
         mPath = printer.path;
+        // Upgrade to IPPS path if present
+        for (Uri path : printer.paths) {
+            if (IPPS_SCHEME.equals(path.getScheme())) {
+                mPath = path;
+                break;
+            }
+        }
+
         mPrintService.getCapabilitiesCache().request(printer, true, this);
     }
 
@@ -213,14 +239,36 @@
             if (mDiscoveryTimeout != null) {
                 mDiscoveryTimeout.cancel();
             }
+            mCapabilities = capabilities;
+            deliver();
+        }
+    }
+
+    private void deliver() {
+        if (mCapabilities.certificate != null && !IPPS_SCHEME.equals(mPath.getScheme())) {
+            mState = STATE_SECURITY;
+            mPrintJob.block(mPrintService.getString(R.string.printer_not_encrypted));
+            mPrintService.notifyCertificateChange(mCapabilities.name,
+                    mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, null);
+        } else {
             mState = STATE_DELIVERING;
             mPrintJob.start();
-            mBackend.print(mPath, mPrintJob, capabilities, this::handleJobStatus);
+            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) {
+            // If there is no certificate, record this one
+            if (mCertificateStore.get(mCapabilities.uuid) == null) {
+                if (DEBUG) Log.d(TAG, "Recording new certificate");
+                mCertificateStore.put(mCapabilities.uuid, certificate);
+            }
+        }
+
         switch (jobStatus.getJobState()) {
             case BackendConstants.JOB_STATE_DONE:
                 switch (jobStatus.getJobResult()) {
@@ -236,7 +284,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 +312,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_SECURITY;
+            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/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..76e0868
--- /dev/null
+++ b/src/com/android/bips/ipp/CertificateStore.java
@@ -0,0 +1,189 @@
+/*
+ * 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();
+        }
+    }
+
+    /** Remove any certificate associated with the specified UUID. */
+    public void remove(String uuid) {
+        if (mCertificates.remove(uuid) != null) {
+            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)
                 + "}";
     }
 }