1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * Copyright (C) 2016 Mopria Alliance, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.bips; 19 20 import android.app.Notification; 21 import android.app.NotificationChannel; 22 import android.app.NotificationManager; 23 import android.app.PendingIntent; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.graphics.drawable.Icon; 30 import android.net.nsd.NsdManager; 31 import android.net.wifi.WifiManager; 32 import android.os.Handler; 33 import android.print.PrinterId; 34 import android.printservice.PrintJob; 35 import android.printservice.PrintService; 36 import android.printservice.PrinterDiscoverySession; 37 import android.text.TextUtils; 38 import android.util.Log; 39 40 import com.android.bips.discovery.DelayedDiscovery; 41 import com.android.bips.discovery.DiscoveredPrinter; 42 import com.android.bips.discovery.Discovery; 43 import com.android.bips.discovery.ManualDiscovery; 44 import com.android.bips.discovery.MdnsDiscovery; 45 import com.android.bips.discovery.MultiDiscovery; 46 import com.android.bips.discovery.NsdResolveQueue; 47 import com.android.bips.discovery.P2pDiscovery; 48 import com.android.bips.ipp.Backend; 49 import com.android.bips.ipp.CapabilitiesCache; 50 import com.android.bips.ipp.CertificateStore; 51 import com.android.bips.p2p.P2pMonitor; 52 import com.android.bips.p2p.P2pUtils; 53 import com.android.bips.util.BroadcastMonitor; 54 55 import java.lang.ref.WeakReference; 56 57 public class BuiltInPrintService extends PrintService { 58 private static final String TAG = BuiltInPrintService.class.getSimpleName(); 59 private static final boolean DEBUG = false; 60 private static final int IPPS_PRINTER_DELAY = 150; 61 private static final int P2P_DISCOVERY_DELAY = 1000; 62 private static final String CHANNEL_ID_SECURITY = "security"; 63 private static final String TAG_CERTIFICATE_REQUEST = 64 BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_REQUEST"; 65 private static final String ACTION_CERTIFICATE_ACCEPT = 66 BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_ACCEPT"; 67 private static final String ACTION_CERTIFICATE_REJECT = 68 BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_REJECT"; 69 public static final String ACTION_P2P_PERMISSION_CANCEL = 70 BuiltInPrintService.class.getCanonicalName() + ".P2P_PERMISSION_CANCEL"; 71 public static final String ACTION_P2P_DISABLE = 72 BuiltInPrintService.class.getCanonicalName() + ".ACTION_P2P_DISABLE"; 73 private static final String EXTRA_CERTIFICATE = "certificate"; 74 private static final String EXTRA_PRINTER_ID = "printer-id"; 75 private static final String EXTRA_PRINTER_UUID = "printer-uuid"; 76 private static final int CERTIFICATE_REQUEST_ID = 1000; 77 public static final int P2P_PERMISSION_REQUEST_ID = 1001; 78 79 // Present because local activities can bind, but cannot access this object directly 80 private static WeakReference<BuiltInPrintService> sInstance; 81 82 private MultiDiscovery mAllDiscovery; 83 private P2pDiscovery mP2pDiscovery; 84 private Discovery mMdnsDiscovery; 85 private ManualDiscovery mManualDiscovery; 86 private CapabilitiesCache mCapabilitiesCache; 87 private CertificateStore mCertificateStore; 88 private JobQueue mJobQueue; 89 private Handler mMainHandler; 90 private Backend mBackend; 91 private WifiManager.WifiLock mWifiLock; 92 private P2pMonitor mP2pMonitor; 93 private NsdResolveQueue mNsdResolveQueue; 94 private P2pPermissionManager mP2pPermissionManager; 95 96 /** 97 * Return the current print service instance, if running 98 */ getInstance()99 public static BuiltInPrintService getInstance() { 100 return sInstance == null ? null : sInstance.get(); 101 } 102 103 @Override onCreate()104 public void onCreate() { 105 if (DEBUG) { 106 try { 107 PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); 108 String version = pInfo.versionName; 109 Log.d(TAG, "onCreate() " + version); 110 } catch (PackageManager.NameNotFoundException ignored) { 111 } 112 } 113 super.onCreate(); 114 createNotificationChannel(); 115 mP2pPermissionManager = new P2pPermissionManager(this); 116 mP2pPermissionManager.reset(); 117 118 sInstance = new WeakReference<>(this); 119 mBackend = new Backend(this); 120 mCertificateStore = new CertificateStore(this); 121 mCapabilitiesCache = new CapabilitiesCache(this, mBackend, 122 CapabilitiesCache.DEFAULT_MAX_CONCURRENT); 123 mP2pMonitor = new P2pMonitor(this); 124 125 NsdManager nsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE); 126 mNsdResolveQueue = new NsdResolveQueue(this, nsdManager); 127 128 // Delay IPP results so that IPP is preferred 129 Discovery ippDiscovery = new MdnsDiscovery(this, MdnsDiscovery.SCHEME_IPP); 130 Discovery ippsDiscovery = new MdnsDiscovery(this, MdnsDiscovery.SCHEME_IPPS); 131 mMdnsDiscovery = new MultiDiscovery(ippDiscovery, new DelayedDiscovery(ippsDiscovery, 0, 132 IPPS_PRINTER_DELAY)); 133 mP2pDiscovery = new P2pDiscovery(this); 134 mManualDiscovery = new ManualDiscovery(this); 135 136 // Delay P2P discovery so that all others are found first 137 mAllDiscovery = new MultiDiscovery(mMdnsDiscovery, mManualDiscovery, new DelayedDiscovery( 138 mP2pDiscovery, P2P_DISCOVERY_DELAY, 0)); 139 140 mJobQueue = new JobQueue(); 141 mMainHandler = new Handler(getMainLooper()); 142 WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); 143 mWifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); 144 } 145 146 @Override onDestroy()147 public void onDestroy() { 148 if (DEBUG) Log.d(TAG, "onDestroy()"); 149 mP2pPermissionManager.closeNotification(); 150 mCapabilitiesCache.close(); 151 mP2pMonitor.stopAll(); 152 mBackend.close(); 153 unlockWifi(); 154 sInstance = null; 155 mMainHandler.removeCallbacksAndMessages(null); 156 super.onDestroy(); 157 } 158 159 @Override onCreatePrinterDiscoverySession()160 protected PrinterDiscoverySession onCreatePrinterDiscoverySession() { 161 if (DEBUG) Log.d(TAG, "onCreatePrinterDiscoverySession"); 162 return new LocalDiscoverySession(this); 163 } 164 165 @Override onPrintJobQueued(PrintJob printJob)166 protected void onPrintJobQueued(PrintJob printJob) { 167 if (DEBUG) Log.d(TAG, "onPrintJobQueued"); 168 mJobQueue.print(new LocalPrintJob(this, mBackend, printJob)); 169 } 170 171 @Override onRequestCancelPrintJob(PrintJob printJob)172 protected void onRequestCancelPrintJob(PrintJob printJob) { 173 if (DEBUG) Log.d(TAG, "onRequestCancelPrintJob"); 174 mJobQueue.cancel(printJob.getId()); 175 } 176 177 /** 178 * Return the global discovery object 179 */ getDiscovery()180 public Discovery getDiscovery() { 181 return mAllDiscovery; 182 } 183 184 /** 185 * Return the global object for MDNS discoveries 186 */ getMdnsDiscovery()187 public Discovery getMdnsDiscovery() { 188 return mMdnsDiscovery; 189 } 190 191 /** 192 * Return the global object for manual discoveries 193 */ getManualDiscovery()194 public ManualDiscovery getManualDiscovery() { 195 return mManualDiscovery; 196 } 197 198 /** 199 * Return the global object for Wi-Fi Direct printer discoveries 200 */ getP2pDiscovery()201 public P2pDiscovery getP2pDiscovery() { 202 return mP2pDiscovery; 203 } 204 205 /** 206 * Return the global object for general Wi-Fi Direct management 207 */ getP2pMonitor()208 public P2pMonitor getP2pMonitor() { 209 return mP2pMonitor; 210 } 211 212 /** 213 * Return the global {@link NsdResolveQueue} 214 */ getNsdResolveQueue()215 public NsdResolveQueue getNsdResolveQueue() { 216 return mNsdResolveQueue; 217 } 218 219 /** 220 * Return a general {@link P2pPermissionManager} 221 */ getP2pPermissionManager()222 public P2pPermissionManager getP2pPermissionManager() { 223 return mP2pPermissionManager; 224 } 225 226 /** 227 * Listen for a set of broadcast messages until stopped 228 */ receiveBroadcasts(BroadcastReceiver receiver, String... actions)229 public BroadcastMonitor receiveBroadcasts(BroadcastReceiver receiver, String... actions) { 230 return new BroadcastMonitor(this, receiver, actions); 231 } 232 233 /** 234 * Return the global Printer Capabilities cache 235 */ getCapabilitiesCache()236 public CapabilitiesCache getCapabilitiesCache() { 237 return mCapabilitiesCache; 238 } 239 240 /** 241 * Return a store of certificate public keys for supporting trust-on-first-use. 242 */ getCertificateStore()243 public CertificateStore getCertificateStore() { 244 return mCertificateStore; 245 } 246 247 /** 248 * Return the main handler for posting {@link Runnable} objects to the main UI 249 */ getMainHandler()250 public Handler getMainHandler() { 251 return mMainHandler; 252 } 253 254 /** Run something on the main thread, returning an object that can cancel the request */ delay(int delay, Runnable toRun)255 public DelayedAction delay(int delay, Runnable toRun) { 256 mMainHandler.postDelayed(toRun, delay); 257 return () -> mMainHandler.removeCallbacks(toRun); 258 } 259 260 /** 261 * Return a friendly description string including host and (if present) location 262 */ getDescription(DiscoveredPrinter printer)263 public String getDescription(DiscoveredPrinter printer) { 264 if (P2pUtils.isP2p(printer) || P2pUtils.isOnConnectedInterface(this, printer)) { 265 return getString(R.string.wifi_direct); 266 } 267 268 String host = printer.getHost(); 269 if (!TextUtils.isEmpty(printer.location)) { 270 return getString(R.string.printer_description, host, printer.location); 271 } else { 272 return host; 273 } 274 } 275 276 /** Prevent Wi-Fi from going to sleep until {@link #unlockWifi} is called */ lockWifi()277 public void lockWifi() { 278 if (!mWifiLock.isHeld()) { 279 mWifiLock.acquire(); 280 } 281 } 282 283 /** Allow Wi-Fi to be disabled during sleep modes. */ unlockWifi()284 public void unlockWifi() { 285 if (mWifiLock.isHeld()) { 286 mWifiLock.release(); 287 } 288 } 289 290 /** 291 * Set up a channel for notifications. 292 */ createNotificationChannel()293 private void createNotificationChannel() { 294 NotificationChannel channel = new NotificationChannel(CHANNEL_ID_SECURITY, 295 getString(R.string.security), NotificationManager.IMPORTANCE_HIGH); 296 297 NotificationManager manager = (NotificationManager) getSystemService( 298 Context.NOTIFICATION_SERVICE); 299 manager.createNotificationChannel(channel); 300 } 301 302 /** 303 * Notify the user of a certificate change (could be a MITM attack) and allow response. 304 * 305 * When certificate is null, the printer is being downgraded to no-encryption. 306 */ notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid, byte[] certificate)307 void notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid, 308 byte[] certificate) { 309 String message; 310 if (certificate == null) { 311 message = getString(R.string.not_encrypted_request); 312 } else { 313 message = getString(R.string.certificate_update_request); 314 } 315 316 Intent rejectIntent = new Intent(this, BuiltInPrintService.class) 317 .setAction(ACTION_CERTIFICATE_REJECT) 318 .putExtra(EXTRA_PRINTER_ID, printerId); 319 PendingIntent pendingRejectIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID, 320 rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 321 Notification.Action rejectAction = new Notification.Action.Builder( 322 Icon.createWithResource(this, R.drawable.ic_printservice), 323 getString(R.string.reject), pendingRejectIntent).build(); 324 325 PendingIntent deleteIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID, 326 rejectIntent, PendingIntent.FLAG_IMMUTABLE); 327 328 Intent acceptIntent = new Intent(this, BuiltInPrintService.class) 329 .setAction(ACTION_CERTIFICATE_ACCEPT) 330 .putExtra(EXTRA_PRINTER_UUID, printerUuid) 331 .putExtra(EXTRA_PRINTER_ID, printerId); 332 if (certificate != null) { 333 acceptIntent.putExtra(EXTRA_CERTIFICATE, certificate); 334 } 335 PendingIntent pendingAcceptIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID, 336 acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 337 Notification.Action acceptAction = new Notification.Action.Builder( 338 Icon.createWithResource(this, R.drawable.ic_printservice), 339 getString(R.string.accept), pendingAcceptIntent).build(); 340 341 Notification notification = new Notification.Builder(this, CHANNEL_ID_SECURITY) 342 .setContentTitle(printerName) 343 .setSmallIcon(R.drawable.ic_printservice) 344 .setStyle(new Notification.BigTextStyle().bigText(message)) 345 .setContentText(message) 346 .setAutoCancel(true) 347 .addAction(rejectAction) 348 .addAction(acceptAction) 349 .setDeleteIntent(deleteIntent) 350 .build(); 351 352 NotificationManager manager = (NotificationManager) getSystemService( 353 Context.NOTIFICATION_SERVICE); 354 manager.notify(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID, notification); 355 } 356 357 @Override onStartCommand(Intent intent, int flags, int startId)358 public int onStartCommand(Intent intent, int flags, int startId) { 359 if (DEBUG) Log.d(TAG, "Received action=" + intent.getAction()); 360 NotificationManager manager = (NotificationManager) getSystemService( 361 Context.NOTIFICATION_SERVICE); 362 if (ACTION_CERTIFICATE_ACCEPT.equals(intent.getAction())) { 363 byte[] certificate = intent.getByteArrayExtra(EXTRA_CERTIFICATE); 364 PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID); 365 String printerUuid = intent.getStringExtra(EXTRA_PRINTER_UUID); 366 if (certificate != null) { 367 mCertificateStore.put(printerUuid, certificate); 368 } else { 369 mCertificateStore.remove(printerUuid); 370 } 371 // Restart the job with the updated certificate in place 372 mJobQueue.restart(printerId); 373 manager.cancel(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID); 374 } else if (ACTION_CERTIFICATE_REJECT.equals(intent.getAction())) { 375 // Cancel any job in certificate state for this uuid 376 PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID); 377 mJobQueue.cancel(printerId); 378 manager.cancel(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID); 379 } else if (ACTION_P2P_PERMISSION_CANCEL.equals(intent.getAction())) { 380 // Inform p2pPermissionManager the user canceled the notification (non-permanent) 381 mP2pPermissionManager.applyPermissionChange(false); 382 } else if (ACTION_P2P_DISABLE.equals(intent.getAction())) { 383 mP2pPermissionManager.applyPermissionChange(true); 384 } 385 return START_NOT_STICKY; 386 } 387 } 388