1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.printservice.recommendation.plugin.google; 18 19 import static com.android.printservice.recommendation.util.MDNSUtils.ATTRIBUTE_TY; 20 21 import android.content.Context; 22 import android.util.ArrayMap; 23 import android.util.Log; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.StringRes; 27 28 import com.android.printservice.recommendation.PrintServicePlugin; 29 import com.android.printservice.recommendation.R; 30 import com.android.printservice.recommendation.util.MDNSFilteredDiscovery; 31 32 import java.net.Inet4Address; 33 import java.net.InetAddress; 34 import java.nio.charset.StandardCharsets; 35 import java.util.HashSet; 36 import java.util.Map; 37 import java.util.Set; 38 39 /** 40 * Plugin detecting <a href="https://developers.google.com/cloud-print/docs/privet">Google Cloud 41 * Print</a> printers. 42 */ 43 public class CloudPrintPlugin implements PrintServicePlugin { 44 private static final String LOG_TAG = CloudPrintPlugin.class.getSimpleName(); 45 private static final boolean DEBUG = false; 46 47 private static final String ATTRIBUTE_TXTVERS = "txtvers"; 48 private static final String ATTRIBUTE_URL = "url"; 49 private static final String ATTRIBUTE_TYPE = "type"; 50 private static final String ATTRIBUTE_ID = "id"; 51 private static final String ATTRIBUTE_CS = "cs"; 52 53 private static final String TYPE = "printer"; 54 55 private static final String PRIVET_SERVICE = "_privet._tcp"; 56 57 /** The required mDNS service types */ 58 private static final Set<String> PRINTER_SERVICE_TYPE = new HashSet<String>() {{ 59 // Not checking _printer_._sub 60 add(PRIVET_SERVICE); 61 }}; 62 63 /** All possible connection states */ 64 private static final Set<String> POSSIBLE_CONNECTION_STATES = new HashSet<String>() {{ 65 add("online"); 66 add("offline"); 67 add("connecting"); 68 add("not-configured"); 69 }}; 70 71 private static final byte SUPPORTED_TXTVERS = '1'; 72 73 /** The mDNS filtered discovery */ 74 private final MDNSFilteredDiscovery mMDNSFilteredDiscovery; 75 76 /** 77 * Create a plugin detecting Google Cloud Print printers. 78 * 79 * @param context The context the plugin runs in 80 */ 81 public CloudPrintPlugin(@NonNull Context context) { 82 mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, PRINTER_SERVICE_TYPE, 83 nsdServiceInfo -> { 84 // The attributes are case insensitive. For faster searching create a clone of 85 // the map with the attribute-keys all in lower case. 86 ArrayMap<String, byte[]> caseInsensitiveAttributes = 87 new ArrayMap<>(nsdServiceInfo.getAttributes().size()); 88 for (Map.Entry<String, byte[]> entry : nsdServiceInfo.getAttributes() 89 .entrySet()) { 90 caseInsensitiveAttributes.put(entry.getKey().toLowerCase(), 91 entry.getValue()); 92 } 93 94 if (DEBUG) { 95 Log.i(LOG_TAG, nsdServiceInfo.getServiceName() + ":"); 96 Log.i(LOG_TAG, "type: " + nsdServiceInfo.getServiceType()); 97 Log.i(LOG_TAG, "host: " + nsdServiceInfo.getHost()); 98 for (Map.Entry<String, byte[]> entry : caseInsensitiveAttributes.entrySet()) { 99 if (entry.getValue() == null) { 100 Log.i(LOG_TAG, entry.getKey() + "= null"); 101 } else { 102 Log.i(LOG_TAG, entry.getKey() + "=" + new String(entry.getValue(), 103 StandardCharsets.UTF_8)); 104 } 105 } 106 } 107 108 byte[] txtvers = caseInsensitiveAttributes.get(ATTRIBUTE_TXTVERS); 109 if (txtvers == null || txtvers.length != 1 || txtvers[0] != SUPPORTED_TXTVERS) { 110 // The spec requires this to be the first attribute, but at this time we 111 // lost the order of the attributes 112 return false; 113 } 114 115 if (caseInsensitiveAttributes.get(ATTRIBUTE_TY) == null) { 116 return false; 117 } 118 119 byte[] url = caseInsensitiveAttributes.get(ATTRIBUTE_URL); 120 if (url == null || url.length == 0) { 121 return false; 122 } 123 124 byte[] type = caseInsensitiveAttributes.get(ATTRIBUTE_TYPE); 125 if (type == null || !TYPE.equals( 126 new String(type, StandardCharsets.UTF_8).toLowerCase())) { 127 return false; 128 } 129 130 if (caseInsensitiveAttributes.get(ATTRIBUTE_ID) == null) { 131 return false; 132 } 133 134 byte[] cs = caseInsensitiveAttributes.get(ATTRIBUTE_CS); 135 if (cs == null || !POSSIBLE_CONNECTION_STATES.contains( 136 new String(cs, StandardCharsets.UTF_8).toLowerCase())) { 137 return false; 138 } 139 140 InetAddress address = nsdServiceInfo.getHost(); 141 if (!(address instanceof Inet4Address)) { 142 // Not checking for link local address 143 return false; 144 } 145 146 return true; 147 }); 148 } 149 150 @Override 151 @NonNull public CharSequence getPackageName() { 152 return "com.google.android.apps.cloudprint"; 153 } 154 155 @Override 156 public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception { 157 mMDNSFilteredDiscovery.start(callback); 158 } 159 160 @Override 161 @StringRes public int getName() { 162 return R.string.plugin_vendor_google_cloud_print; 163 } 164 165 @Override 166 public void stop() throws Exception { 167 mMDNSFilteredDiscovery.stop(); 168 } 169 } 170