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     private static final String EXTRA_CERTIFICATE = "certificate";
72     private static final String EXTRA_PRINTER_ID = "printer-id";
73     private static final String EXTRA_PRINTER_UUID = "printer-uuid";
74     private static final int CERTIFICATE_REQUEST_ID = 1000;
75     public static final int P2P_PERMISSION_REQUEST_ID = 1001;
76 
77     // Present because local activities can bind, but cannot access this object directly
78     private static WeakReference<BuiltInPrintService> sInstance;
79 
80     private MultiDiscovery mAllDiscovery;
81     private P2pDiscovery mP2pDiscovery;
82     private Discovery mMdnsDiscovery;
83     private ManualDiscovery mManualDiscovery;
84     private CapabilitiesCache mCapabilitiesCache;
85     private CertificateStore mCertificateStore;
86     private JobQueue mJobQueue;
87     private Handler mMainHandler;
88     private Backend mBackend;
89     private WifiManager.WifiLock mWifiLock;
90     private P2pMonitor mP2pMonitor;
91     private NsdResolveQueue mNsdResolveQueue;
92     private P2pPermissionManager mP2pPermissionManager;
93 
94     /**
95      * Return the current print service instance, if running
96      */
getInstance()97     public static BuiltInPrintService getInstance() {
98         return sInstance == null ? null : sInstance.get();
99     }
100 
101     @Override
onCreate()102     public void onCreate() {
103         if (DEBUG) {
104             try {
105                 PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
106                 String version = pInfo.versionName;
107                 Log.d(TAG, "onCreate() " + version);
108             } catch (PackageManager.NameNotFoundException ignored) {
109             }
110         }
111         super.onCreate();
112         createNotificationChannel();
113         mP2pPermissionManager = new P2pPermissionManager(this);
114         mP2pPermissionManager.reset();
115 
116         sInstance = new WeakReference<>(this);
117         mBackend = new Backend(this);
118         mCertificateStore = new CertificateStore(this);
119         mCapabilitiesCache = new CapabilitiesCache(this, mBackend,
120                 CapabilitiesCache.DEFAULT_MAX_CONCURRENT);
121         mP2pMonitor = new P2pMonitor(this);
122 
123         NsdManager nsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE);
124         mNsdResolveQueue = new NsdResolveQueue(this, nsdManager);
125 
126         // Delay IPP results so that IPP is preferred
127         Discovery ippDiscovery = new MdnsDiscovery(this, MdnsDiscovery.SCHEME_IPP);
128         Discovery ippsDiscovery = new MdnsDiscovery(this, MdnsDiscovery.SCHEME_IPPS);
129         mMdnsDiscovery = new MultiDiscovery(ippDiscovery, new DelayedDiscovery(ippsDiscovery, 0,
130                 IPPS_PRINTER_DELAY));
131         mP2pDiscovery = new P2pDiscovery(this);
132         mManualDiscovery = new ManualDiscovery(this);
133 
134         // Delay P2P discovery so that all others are found first
135         mAllDiscovery = new MultiDiscovery(mMdnsDiscovery, mManualDiscovery, new DelayedDiscovery(
136                 mP2pDiscovery, P2P_DISCOVERY_DELAY, 0));
137 
138         mJobQueue = new JobQueue();
139         mMainHandler = new Handler(getMainLooper());
140         WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
141         mWifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
142     }
143 
144     @Override
onDestroy()145     public void onDestroy() {
146         if (DEBUG) Log.d(TAG, "onDestroy()");
147         mP2pPermissionManager.closeNotification();
148         mCapabilitiesCache.close();
149         mP2pMonitor.stopAll();
150         mBackend.close();
151         unlockWifi();
152         sInstance = null;
153         mMainHandler.removeCallbacksAndMessages(null);
154         super.onDestroy();
155     }
156 
157     @Override
onCreatePrinterDiscoverySession()158     protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
159         if (DEBUG) Log.d(TAG, "onCreatePrinterDiscoverySession");
160         return new LocalDiscoverySession(this);
161     }
162 
163     @Override
onPrintJobQueued(PrintJob printJob)164     protected void onPrintJobQueued(PrintJob printJob) {
165         if (DEBUG) Log.d(TAG, "onPrintJobQueued");
166         mJobQueue.print(new LocalPrintJob(this, mBackend, printJob));
167     }
168 
169     @Override
onRequestCancelPrintJob(PrintJob printJob)170     protected void onRequestCancelPrintJob(PrintJob printJob) {
171         if (DEBUG) Log.d(TAG, "onRequestCancelPrintJob");
172         mJobQueue.cancel(printJob.getId());
173     }
174 
175     /**
176      * Return the global discovery object
177      */
getDiscovery()178     public Discovery getDiscovery() {
179         return mAllDiscovery;
180     }
181 
182     /**
183      * Return the global object for MDNS discoveries
184      */
getMdnsDiscovery()185     public Discovery getMdnsDiscovery() {
186         return mMdnsDiscovery;
187     }
188 
189     /**
190      * Return the global object for manual discoveries
191      */
getManualDiscovery()192     public ManualDiscovery getManualDiscovery() {
193         return mManualDiscovery;
194     }
195 
196     /**
197      * Return the global object for Wi-Fi Direct printer discoveries
198      */
getP2pDiscovery()199     public P2pDiscovery getP2pDiscovery() {
200         return mP2pDiscovery;
201     }
202 
203     /**
204      * Return the global object for general Wi-Fi Direct management
205      */
getP2pMonitor()206     public P2pMonitor getP2pMonitor() {
207         return mP2pMonitor;
208     }
209 
210     /**
211      * Return the global {@link NsdResolveQueue}
212      */
getNsdResolveQueue()213     public NsdResolveQueue getNsdResolveQueue() {
214         return mNsdResolveQueue;
215     }
216 
217     /**
218      * Return a general {@link P2pPermissionManager}
219      */
getP2pPermissionManager()220     public P2pPermissionManager getP2pPermissionManager() {
221         return mP2pPermissionManager;
222     }
223 
224     /**
225      * Listen for a set of broadcast messages until stopped
226      */
receiveBroadcasts(BroadcastReceiver receiver, String... actions)227     public BroadcastMonitor receiveBroadcasts(BroadcastReceiver receiver, String... actions) {
228         return new BroadcastMonitor(this, receiver, actions);
229     }
230 
231     /**
232      * Return the global Printer Capabilities cache
233      */
getCapabilitiesCache()234     public CapabilitiesCache getCapabilitiesCache() {
235         return mCapabilitiesCache;
236     }
237 
238     /**
239      * Return a store of certificate public keys for supporting trust-on-first-use.
240      */
getCertificateStore()241     public CertificateStore getCertificateStore() {
242         return mCertificateStore;
243     }
244 
245     /**
246      * Return the main handler for posting {@link Runnable} objects to the main UI
247      */
getMainHandler()248     public Handler getMainHandler() {
249         return mMainHandler;
250     }
251 
252     /** Run something on the main thread, returning an object that can cancel the request */
delay(int delay, Runnable toRun)253     public DelayedAction delay(int delay, Runnable toRun) {
254         mMainHandler.postDelayed(toRun, delay);
255         return () -> mMainHandler.removeCallbacks(toRun);
256     }
257 
258     /**
259      * Return a friendly description string including host and (if present) location
260      */
getDescription(DiscoveredPrinter printer)261     public String getDescription(DiscoveredPrinter printer) {
262         if (P2pUtils.isP2p(printer) || P2pUtils.isOnConnectedInterface(this, printer)) {
263             return getString(R.string.wifi_direct);
264         }
265 
266         String host = printer.getHost();
267         if (!TextUtils.isEmpty(printer.location)) {
268             return getString(R.string.printer_description, host, printer.location);
269         } else {
270             return host;
271         }
272     }
273 
274     /** Prevent Wi-Fi from going to sleep until {@link #unlockWifi} is called */
lockWifi()275     public void lockWifi() {
276         if (!mWifiLock.isHeld()) {
277             mWifiLock.acquire();
278         }
279     }
280 
281     /** Allow Wi-Fi to be disabled during sleep modes. */
unlockWifi()282     public void unlockWifi() {
283         if (mWifiLock.isHeld()) {
284             mWifiLock.release();
285         }
286     }
287 
288     /**
289      * Set up a channel for notifications.
290      */
createNotificationChannel()291     private void createNotificationChannel() {
292         NotificationChannel channel = new NotificationChannel(CHANNEL_ID_SECURITY,
293                 getString(R.string.security), NotificationManager.IMPORTANCE_HIGH);
294 
295         NotificationManager manager = (NotificationManager) getSystemService(
296                 Context.NOTIFICATION_SERVICE);
297         manager.createNotificationChannel(channel);
298     }
299 
300     /**
301      * Notify the user of a certificate change (could be a MITM attack) and allow response.
302      *
303      * When certificate is null, the printer is being downgraded to no-encryption.
304      */
notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid, byte[] certificate)305     void notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid,
306                                  byte[] certificate) {
307         String message;
308         if (certificate == null) {
309             message = getString(R.string.not_encrypted_request);
310         } else {
311             message = getString(R.string.certificate_update_request);
312         }
313 
314         Intent rejectIntent = new Intent(this, BuiltInPrintService.class)
315                 .setAction(ACTION_CERTIFICATE_REJECT)
316                 .putExtra(EXTRA_PRINTER_ID, printerId);
317         PendingIntent pendingRejectIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
318                 rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
319         Notification.Action rejectAction = new Notification.Action.Builder(
320                 Icon.createWithResource(this, R.drawable.ic_printservice),
321                 getString(R.string.reject), pendingRejectIntent).build();
322 
323         PendingIntent deleteIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
324                 rejectIntent, 0);
325 
326         Intent acceptIntent = new Intent(this, BuiltInPrintService.class)
327                 .setAction(ACTION_CERTIFICATE_ACCEPT)
328                 .putExtra(EXTRA_PRINTER_UUID, printerUuid)
329                 .putExtra(EXTRA_PRINTER_ID, printerId);
330         if (certificate != null) {
331             acceptIntent.putExtra(EXTRA_CERTIFICATE, certificate);
332         }
333         PendingIntent pendingAcceptIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
334                 acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);
335         Notification.Action acceptAction = new Notification.Action.Builder(
336                 Icon.createWithResource(this, R.drawable.ic_printservice),
337                 getString(R.string.accept), pendingAcceptIntent).build();
338 
339         Notification notification = new Notification.Builder(this, CHANNEL_ID_SECURITY)
340                 .setContentTitle(printerName)
341                 .setSmallIcon(R.drawable.ic_printservice)
342                 .setStyle(new Notification.BigTextStyle().bigText(message))
343                 .setContentText(message)
344                 .setAutoCancel(true)
345                 .addAction(rejectAction)
346                 .addAction(acceptAction)
347                 .setDeleteIntent(deleteIntent)
348                 .build();
349 
350         NotificationManager manager = (NotificationManager) getSystemService(
351                 Context.NOTIFICATION_SERVICE);
352         manager.notify(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID, notification);
353     }
354 
355     @Override
onStartCommand(Intent intent, int flags, int startId)356     public int onStartCommand(Intent intent, int flags, int startId) {
357         if (DEBUG) Log.d(TAG, "Received action=" + intent.getAction());
358         NotificationManager manager = (NotificationManager) getSystemService(
359                 Context.NOTIFICATION_SERVICE);
360         if (ACTION_CERTIFICATE_ACCEPT.equals(intent.getAction())) {
361             byte[] certificate = intent.getByteArrayExtra(EXTRA_CERTIFICATE);
362             PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID);
363             String printerUuid = intent.getStringExtra(EXTRA_PRINTER_UUID);
364             if (certificate != null) {
365                 mCertificateStore.put(printerUuid, certificate);
366             } else {
367                 mCertificateStore.remove(printerUuid);
368             }
369             // Restart the job with the updated certificate in place
370             mJobQueue.restart(printerId);
371             manager.cancel(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID);
372         } else if (ACTION_CERTIFICATE_REJECT.equals(intent.getAction())) {
373             // Cancel any job in certificate state for this uuid
374             PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID);
375             mJobQueue.cancel(printerId);
376             manager.cancel(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID);
377         } else if (ACTION_P2P_PERMISSION_CANCEL.equals(intent.getAction())) {
378             // Inform p2pPermissionManager the user canceled the notification (non-permanent)
379             mP2pPermissionManager.applyPermissionChange(false);
380         }
381         return START_NOT_STICKY;
382     }
383 }
384