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