1 /*
2  * Copyright (C) 2022 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.server.companion.presence;
18 
19 import static android.os.Process.ROOT_UID;
20 import static android.os.Process.SHELL_UID;
21 
22 import android.annotation.NonNull;
23 import android.annotation.SuppressLint;
24 import android.annotation.TestApi;
25 import android.bluetooth.BluetoothAdapter;
26 import android.bluetooth.BluetoothDevice;
27 import android.companion.AssociationInfo;
28 import android.content.Context;
29 import android.os.Binder;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.os.UserManager;
34 import android.util.Log;
35 import android.util.Slog;
36 import android.util.SparseArray;
37 
38 import com.android.server.companion.AssociationStore;
39 
40 import java.io.PrintWriter;
41 import java.util.HashSet;
42 import java.util.Set;
43 
44 /**
45  * Class responsible for monitoring companion devices' "presence" status (i.e.
46  * connected/disconnected for Bluetooth devices; nearby or not for BLE devices).
47  *
48  * <p>
49  * Should only be used by
50  * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
51  * to which it provides the following API:
52  * <ul>
53  * <li> {@link #onSelfManagedDeviceConnected(int)}
54  * <li> {@link #onSelfManagedDeviceDisconnected(int)}
55  * <li> {@link #isDevicePresent(int)}
56  * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)}
57  * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)}
58  * </ul>
59  */
60 @SuppressLint("LongLogTag")
61 public class CompanionDevicePresenceMonitor implements AssociationStore.OnChangeListener,
62         BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback {
63     static final boolean DEBUG = false;
64     private static final String TAG = "CDM_CompanionDevicePresenceMonitor";
65 
66     /** Callback for notifying about changes to status of companion devices. */
67     public interface Callback {
68         /** Invoked when companion device is found nearby or connects. */
onDeviceAppeared(int associationId)69         void onDeviceAppeared(int associationId);
70 
71         /** Invoked when a companion device no longer seen nearby or disconnects. */
onDeviceDisappeared(int associationId)72         void onDeviceDisappeared(int associationId);
73     }
74 
75     private final @NonNull AssociationStore mAssociationStore;
76     private final @NonNull Callback mCallback;
77     private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
78     private final @NonNull BleCompanionDeviceScanner mBleScanner;
79 
80     // NOTE: Same association may appear in more than one of the following sets at the same time.
81     // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
82     // companion applications, while at the same be connected via BT, or detected nearby by BLE
83     // scanner)
84     private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>();
85     private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>();
86     private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
87 
88     // Tracking "simulated" presence. Used for debugging and testing only.
89     private final @NonNull Set<Integer> mSimulated = new HashSet<>();
90     private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper =
91             new SimulatedDevicePresenceSchedulerHelper();
92 
CompanionDevicePresenceMonitor(UserManager userManager, @NonNull AssociationStore associationStore, @NonNull Callback callback)93     public CompanionDevicePresenceMonitor(UserManager userManager,
94             @NonNull AssociationStore associationStore, @NonNull Callback callback) {
95         mAssociationStore = associationStore;
96         mCallback = callback;
97         mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
98                 associationStore, /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
99         mBleScanner = new BleCompanionDeviceScanner(associationStore,
100                 /* BleCompanionDeviceScanner.Callback */ this);
101     }
102 
103     /** Initialize {@link CompanionDevicePresenceMonitor} */
init(Context context)104     public void init(Context context) {
105         if (DEBUG) Log.i(TAG, "init()");
106 
107         final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
108         if (btAdapter != null) {
109             mBtConnectionListener.init(btAdapter);
110             mBleScanner.init(context, btAdapter);
111         } else {
112             Log.w(TAG, "BluetoothAdapter is NOT available.");
113         }
114 
115         mAssociationStore.registerListener(this);
116     }
117 
118     /**
119      * @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
120      *         or devices is connected (for Bluetooth); or reported (by the application) to be
121      *         nearby (for "self-managed" associations).
122      */
isDevicePresent(int associationId)123     public boolean isDevicePresent(int associationId) {
124         return mReportedSelfManagedDevices.contains(associationId)
125                 || mConnectedBtDevices.contains(associationId)
126                 || mNearbyBleDevices.contains(associationId)
127                 || mSimulated.contains(associationId);
128     }
129 
130     /**
131      * Marks a "self-managed" device as connected.
132      *
133      * <p>
134      * Must ONLY be invoked by the
135      * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
136      * when an application invokes
137      * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()}
138      */
onSelfManagedDeviceConnected(int associationId)139     public void onSelfManagedDeviceConnected(int associationId) {
140         onDevicePresent(mReportedSelfManagedDevices, associationId, "application-reported");
141     }
142 
143     /**
144      * Marks a "self-managed" device as disconnected.
145      *
146      * <p>
147      * Must ONLY be invoked by the
148      * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
149      * when an application invokes
150      * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()}
151      */
onSelfManagedDeviceDisconnected(int associationId)152     public void onSelfManagedDeviceDisconnected(int associationId) {
153         onDeviceGone(mReportedSelfManagedDevices, associationId, "application-reported");
154     }
155 
156     /**
157      * Marks a "self-managed" device as disconnected when binderDied.
158      */
onSelfManagedDeviceReporterBinderDied(int associationId)159     public void onSelfManagedDeviceReporterBinderDied(int associationId) {
160         onDeviceGone(mReportedSelfManagedDevices, associationId, "application-reported");
161     }
162 
163     @Override
onBluetoothCompanionDeviceConnected(int associationId)164     public void onBluetoothCompanionDeviceConnected(int associationId) {
165         onDevicePresent(mConnectedBtDevices, associationId, /* sourceLoggingTag */ "bt");
166     }
167 
168     @Override
onBluetoothCompanionDeviceDisconnected(int associationId)169     public void onBluetoothCompanionDeviceDisconnected(int associationId) {
170         // If disconnected device is also a BLE device, skip the 2-minute timer and mark it as gone.
171         boolean isConnectableBleDevice = mNearbyBleDevices.remove(associationId);
172         if (DEBUG && isConnectableBleDevice) {
173             Log.d(TAG, "Bluetooth device disconnect was detected."
174                     + " Pre-emptively marking the BLE device as lost.");
175         }
176         onDeviceGone(mConnectedBtDevices, associationId, /* sourceLoggingTag */ "bt");
177     }
178 
179     @Override
onBleCompanionDeviceFound(int associationId)180     public void onBleCompanionDeviceFound(int associationId) {
181         onDevicePresent(mNearbyBleDevices, associationId, /* sourceLoggingTag */ "ble");
182     }
183 
184     @Override
onBleCompanionDeviceLost(int associationId)185     public void onBleCompanionDeviceLost(int associationId) {
186         onDeviceGone(mNearbyBleDevices, associationId, /* sourceLoggingTag */ "ble");
187     }
188 
189     /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
190     @TestApi
simulateDeviceAppeared(int associationId)191     public void simulateDeviceAppeared(int associationId) {
192         // IMPORTANT: this API should only be invoked via the
193         // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
194         // make this call are SHELL and ROOT.
195         // No other caller (including SYSTEM!) should be allowed.
196         enforceCallerShellOrRoot();
197         // Make sure the association exists.
198         enforceAssociationExists(associationId);
199 
200         onDevicePresent(mSimulated, associationId, /* sourceLoggingTag */ "simulated");
201 
202         mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
203     }
204 
205     /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
206     @TestApi
simulateDeviceDisappeared(int associationId)207     public void simulateDeviceDisappeared(int associationId) {
208         // IMPORTANT: this API should only be invoked via the
209         // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
210         // make this call are SHELL and ROOT.
211         // No other caller (including SYSTEM!) should be allowed.
212         enforceCallerShellOrRoot();
213         // Make sure the association exists.
214         enforceAssociationExists(associationId);
215 
216         mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
217 
218         onDeviceGone(mSimulated, associationId, /* sourceLoggingTag */ "simulated");
219     }
220 
enforceAssociationExists(int associationId)221     private void enforceAssociationExists(int associationId) {
222         if (mAssociationStore.getAssociationById(associationId) == null) {
223             throw new IllegalArgumentException(
224                     "Association with id " + associationId + " does not exist.");
225         }
226     }
227 
onDevicePresent(@onNull Set<Integer> presentDevicesForSource, int newDeviceAssociationId, @NonNull String sourceLoggingTag)228     private void onDevicePresent(@NonNull Set<Integer> presentDevicesForSource,
229             int newDeviceAssociationId, @NonNull String sourceLoggingTag) {
230         Slog.i(TAG, "onDevice_Present() id=" + newDeviceAssociationId
231                 + ", source=" + sourceLoggingTag);
232 
233         final boolean alreadyPresent = isDevicePresent(newDeviceAssociationId);
234         if (alreadyPresent) {
235             Slog.i(TAG, "Device" + "id (" + newDeviceAssociationId + ") already present.");
236         }
237 
238         final boolean added = presentDevicesForSource.add(newDeviceAssociationId);
239         if (!added) {
240             Slog.i(TAG, "Association with id "
241                     + newDeviceAssociationId + " is ALREADY reported as "
242                     + "present by this source (" + sourceLoggingTag + ")");
243         }
244 
245         mCallback.onDeviceAppeared(newDeviceAssociationId);
246     }
247 
onDeviceGone(@onNull Set<Integer> presentDevicesForSource, int goneDeviceAssociationId, @NonNull String sourceLoggingTag)248     private void onDeviceGone(@NonNull Set<Integer> presentDevicesForSource,
249             int goneDeviceAssociationId, @NonNull String sourceLoggingTag) {
250         Slog.i(TAG, "onDevice_Gone() id=" + goneDeviceAssociationId
251                 + ", source=" + sourceLoggingTag);
252 
253         final boolean removed = presentDevicesForSource.remove(goneDeviceAssociationId);
254         if (!removed) {
255             Slog.w(TAG, "Association with id " + goneDeviceAssociationId + " was NOT reported "
256                     + "as present by this source (" + sourceLoggingTag + ")");
257             return;
258         }
259 
260         final boolean stillPresent = isDevicePresent(goneDeviceAssociationId);
261 
262         if (stillPresent) {
263             Slog.w(TAG, "  Device id (" + goneDeviceAssociationId + ") is still present.");
264         }
265 
266         mCallback.onDeviceDisappeared(goneDeviceAssociationId);
267     }
268 
269     /**
270      * Implements
271      * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
272      */
273     @Override
onAssociationRemoved(@onNull AssociationInfo association)274     public void onAssociationRemoved(@NonNull AssociationInfo association) {
275         final int id = association.getId();
276         if (DEBUG) {
277             Log.i(TAG, "onAssociationRemoved() id=" + id);
278             Log.d(TAG, "  > association=" + association);
279         }
280 
281         mConnectedBtDevices.remove(id);
282         mNearbyBleDevices.remove(id);
283         mReportedSelfManagedDevices.remove(id);
284         mSimulated.remove(id);
285 
286         // Do NOT call mCallback.onDeviceDisappeared()!
287         // CompanionDeviceManagerService will know that the association is removed, and will do
288         // what's needed.
289     }
290 
291     /**
292      * Return a set of devices that pending to report connectivity
293      */
getPendingConnectedDevices()294     public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
295         synchronized (mBtConnectionListener.mPendingConnectedDevices) {
296             return mBtConnectionListener.mPendingConnectedDevices;
297         }
298     }
299 
enforceCallerShellOrRoot()300     private static void enforceCallerShellOrRoot() {
301         final int callingUid = Binder.getCallingUid();
302         if (callingUid == SHELL_UID || callingUid == ROOT_UID) return;
303 
304         throw new SecurityException("Caller is neither Shell nor Root");
305     }
306 
307     /**
308      * Dumps system information about devices that are marked as "present".
309      */
dump(@onNull PrintWriter out)310     public void dump(@NonNull PrintWriter out) {
311         out.append("Companion Device Present: ");
312         if (mConnectedBtDevices.isEmpty()
313                 && mNearbyBleDevices.isEmpty()
314                 && mReportedSelfManagedDevices.isEmpty()) {
315             out.append("<empty>\n");
316             return;
317         } else {
318             out.append("\n");
319         }
320 
321         out.append("  Connected Bluetooth Devices: ");
322         if (mConnectedBtDevices.isEmpty()) {
323             out.append("<empty>\n");
324         } else {
325             out.append("\n");
326             for (int associationId : mConnectedBtDevices) {
327                 AssociationInfo a = mAssociationStore.getAssociationById(associationId);
328                 out.append("    ").append(a.toShortString()).append('\n');
329             }
330         }
331 
332         out.append("  Nearby BLE Devices: ");
333         if (mNearbyBleDevices.isEmpty()) {
334             out.append("<empty>\n");
335         } else {
336             out.append("\n");
337             for (int associationId : mNearbyBleDevices) {
338                 AssociationInfo a = mAssociationStore.getAssociationById(associationId);
339                 out.append("    ").append(a.toShortString()).append('\n');
340             }
341         }
342 
343         out.append("  Self-Reported Devices: ");
344         if (mReportedSelfManagedDevices.isEmpty()) {
345             out.append("<empty>\n");
346         } else {
347             out.append("\n");
348             for (int associationId : mReportedSelfManagedDevices) {
349                 AssociationInfo a = mAssociationStore.getAssociationById(associationId);
350                 out.append("    ").append(a.toShortString()).append('\n');
351             }
352         }
353     }
354 
355     private class SimulatedDevicePresenceSchedulerHelper extends Handler {
SimulatedDevicePresenceSchedulerHelper()356         SimulatedDevicePresenceSchedulerHelper() {
357             super(Looper.getMainLooper());
358         }
359 
scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId)360         void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
361             // First, unschedule if it was scheduled previously.
362             if (hasMessages(/* what */ associationId)) {
363                 removeMessages(/* what */ associationId);
364             }
365 
366             sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */);
367         }
368 
unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId)369         void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
370             removeMessages(/* what */ associationId);
371         }
372 
373         @Override
handleMessage(@onNull Message msg)374         public void handleMessage(@NonNull Message msg) {
375             final int associationId = msg.what;
376             if (mSimulated.contains(associationId)) {
377                 onDeviceGone(mSimulated, associationId, /* sourceLoggingTag */ "simulated");
378             }
379         }
380     }
381 }
382