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