1 /* 2 * Copyright (C) 2024 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 package com.android.bluetooth.le_scan; 17 18 import static com.android.bluetooth.Utils.getSystemClock; 19 import static com.android.bluetooth.util.AttributionSourceUtil.getLastAttributionTag; 20 21 import android.annotation.Nullable; 22 import android.app.PendingIntent; 23 import android.bluetooth.le.IScannerCallback; 24 import android.content.AttributionSource; 25 import android.os.Binder; 26 import android.os.IBinder; 27 import android.os.IInterface; 28 import android.os.RemoteException; 29 import android.os.UserHandle; 30 import android.os.WorkSource; 31 import android.util.Log; 32 33 import com.android.bluetooth.btservice.AdapterService; 34 35 import java.util.HashMap; 36 import java.util.Iterator; 37 import java.util.List; 38 import java.util.NoSuchElementException; 39 import java.util.UUID; 40 import java.util.concurrent.ConcurrentLinkedQueue; 41 import java.util.function.BiConsumer; 42 import java.util.function.Predicate; 43 import java.util.stream.Collectors; 44 45 /** List of our registered scanners. */ 46 public class ScannerMap { 47 private static final String TAG = ScannerMap.class.getSimpleName(); 48 49 /** Internal map to keep track of logging information by app name */ 50 private final HashMap<Integer, AppScanStats> mAppScanStatsMap = new HashMap<>(); 51 52 private final ConcurrentLinkedQueue<ScannerApp> mApps = new ConcurrentLinkedQueue<>(); 53 54 /** Add an entry to the application context list with a callback. */ add( UUID uuid, AttributionSource source, WorkSource workSource, IScannerCallback callback, AdapterService adapterService, ScanController scanController)55 ScannerApp add( 56 UUID uuid, 57 AttributionSource source, 58 WorkSource workSource, 59 IScannerCallback callback, 60 AdapterService adapterService, 61 ScanController scanController) { 62 return add(uuid, source, workSource, callback, null, adapterService, scanController); 63 } 64 65 /** Add an entry to the application context list with a pending intent. */ add( UUID uuid, AttributionSource source, ScanController.PendingIntentInfo piInfo, AdapterService adapterService, ScanController scanController)66 ScannerApp add( 67 UUID uuid, 68 AttributionSource source, 69 ScanController.PendingIntentInfo piInfo, 70 AdapterService adapterService, 71 ScanController scanController) { 72 return add(uuid, source, null, null, piInfo, adapterService, scanController); 73 } 74 add( UUID uuid, AttributionSource source, @Nullable WorkSource workSource, @Nullable IScannerCallback callback, @Nullable ScanController.PendingIntentInfo piInfo, AdapterService adapterService, ScanController scanController)75 private ScannerApp add( 76 UUID uuid, 77 AttributionSource source, 78 @Nullable WorkSource workSource, 79 @Nullable IScannerCallback callback, 80 @Nullable ScanController.PendingIntentInfo piInfo, 81 AdapterService adapterService, 82 ScanController scanController) { 83 int appUid; 84 String appName = null; 85 if (piInfo != null) { 86 appUid = piInfo.callingUid(); 87 appName = piInfo.callingPackage(); 88 } else { 89 appUid = Binder.getCallingUid(); 90 appName = adapterService.getPackageManager().getNameForUid(appUid); 91 } 92 if (appName == null) { 93 // Assign an app name if one isn't found 94 appName = "Unknown App (UID: " + appUid + ")"; 95 } 96 AppScanStats appScanStats = mAppScanStatsMap.get(appUid); 97 if (appScanStats == null) { 98 appScanStats = 99 new AppScanStats( 100 appName, 101 workSource, 102 this, 103 adapterService, 104 scanController, 105 getSystemClock()); 106 mAppScanStatsMap.put(appUid, appScanStats); 107 } 108 ScannerApp app = 109 new ScannerApp( 110 uuid, 111 getLastAttributionTag(source), 112 callback, 113 piInfo, 114 appName, 115 appScanStats); 116 mApps.add(app); 117 appScanStats.isRegistered = true; 118 return app; 119 } 120 121 /** Remove the context for a given application ID. */ remove(int id)122 void remove(int id) { 123 Iterator<ScannerApp> i = mApps.iterator(); 124 while (i.hasNext()) { 125 ScannerApp entry = i.next(); 126 if (entry.mId == id) { 127 entry.cleanup(); 128 i.remove(); 129 break; 130 } 131 } 132 } 133 134 /** Erases all application context entries. */ clear()135 public void clear() { 136 for (ScannerApp entry : mApps) { 137 entry.cleanup(); 138 } 139 mApps.clear(); 140 } 141 142 /** Get Logging info by application UID */ getAppScanStatsByUid(int uid)143 AppScanStats getAppScanStatsByUid(int uid) { 144 return mAppScanStatsMap.get(uid); 145 } 146 147 /** Get Logging info by ID */ getAppScanStatsById(int id)148 AppScanStats getAppScanStatsById(int id) { 149 ScannerApp temp = (ScannerApp) getById(id); 150 if (temp != null) { 151 return temp.mAppScanStats; 152 } 153 return null; 154 } 155 156 /** Get an application context by ID. */ getById(int id)157 ScannerApp getById(int id) { 158 ScannerApp app = getAppByPredicate(entry -> entry.mId == id); 159 if (app == null) { 160 Log.e(TAG, "Context not found for ID " + id); 161 } 162 return app; 163 } 164 165 /** Get an application context by UUID. */ getByUuid(UUID uuid)166 ScannerApp getByUuid(UUID uuid) { 167 ScannerApp app = getAppByPredicate(entry -> entry.mUuid.equals(uuid)); 168 if (app == null) { 169 Log.e(TAG, "Context not found for UUID " + uuid); 170 } 171 return app; 172 } 173 174 /** Get application contexts by the calling app's name. */ getByName(String name)175 List<ScannerApp> getByName(String name) { 176 return mApps.stream() 177 .filter(app -> app.mName.equals(name)) 178 .collect(Collectors.toUnmodifiableList()); 179 } 180 181 /** Get an application context by the pending intent info object's intent. */ getByPendingIntentInfo(PendingIntent intent)182 ScannerApp getByPendingIntentInfo(PendingIntent intent) { 183 ScannerApp app = 184 getAppByPredicate( 185 entry -> entry.mInfo != null && entry.mInfo.intent().equals(intent)); 186 if (app == null) { 187 Log.e(TAG, "Context not found for intent " + intent); 188 } 189 return app; 190 } 191 getAppByPredicate(Predicate<ScannerApp> predicate)192 private ScannerApp getAppByPredicate(Predicate<ScannerApp> predicate) { 193 // Intentionally using a for-loop over a stream for performance. 194 for (ScannerApp app : mApps) { 195 if (predicate.test(app)) { 196 return app; 197 } 198 } 199 return null; 200 } 201 202 /** Logs debug information. */ dump(StringBuilder sb)203 public void dump(StringBuilder sb) { 204 sb.append(" Entries: ").append(mAppScanStatsMap.size()).append("\n\n"); 205 for (AppScanStats appScanStats : mAppScanStatsMap.values()) { 206 appScanStats.dumpToString(sb); 207 } 208 } 209 210 /** Logs all apps for debugging. */ dumpApps(StringBuilder sb, BiConsumer<StringBuilder, String> bf)211 public void dumpApps(StringBuilder sb, BiConsumer<StringBuilder, String> bf) { 212 for (ScannerApp entry : mApps) { 213 bf.accept( 214 sb, 215 " app_if: " 216 + entry.mId 217 + ", appName: " 218 + entry.mName 219 + (entry.mAttributionTag == null 220 ? "" 221 : ", tag: " + entry.mAttributionTag)); 222 } 223 } 224 225 public static class ScannerApp { 226 /** Context information */ 227 @Nullable ScanController.PendingIntentInfo mInfo; 228 229 /** Statistics for this app */ 230 AppScanStats mAppScanStats; 231 232 /** The UUID of the application */ 233 final UUID mUuid; 234 235 /** The package name of the application */ 236 final String mName; 237 238 /** The last attribution tag in the attribution source chain */ 239 @Nullable final String mAttributionTag; 240 241 /** Application callbacks */ 242 @Nullable IScannerCallback mCallback; 243 244 /** The id of the application */ 245 int mId; 246 247 /** Whether the calling app has location permission */ 248 boolean mHasLocationPermission; 249 250 /** The user handle of the app that started the scan */ 251 @Nullable UserHandle mUserHandle; 252 253 /** Whether the calling app has the network settings permission */ 254 boolean mHasNetworkSettingsPermission; 255 256 /** Whether the calling app has the network setup wizard permission */ 257 boolean mHasNetworkSetupWizardPermission; 258 259 /** Whether the calling app has the network setup wizard permission */ 260 boolean mHasScanWithoutLocationPermission; 261 262 /** Whether the calling app has disavowed the use of bluetooth for location */ 263 boolean mHasDisavowedLocation; 264 265 boolean mEligibleForSanitizedExposureNotification; 266 267 @Nullable List<String> mAssociatedDevices; 268 269 /** Death recipient */ 270 @Nullable private IBinder.DeathRecipient mDeathRecipient; 271 272 /** Creates a new app context. */ ScannerApp( UUID uuid, @Nullable String attributionTag, @Nullable IScannerCallback callback, @Nullable ScanController.PendingIntentInfo info, String name, AppScanStats appScanStats)273 ScannerApp( 274 UUID uuid, 275 @Nullable String attributionTag, 276 @Nullable IScannerCallback callback, 277 @Nullable ScanController.PendingIntentInfo info, 278 String name, 279 AppScanStats appScanStats) { 280 this.mUuid = uuid; 281 this.mAttributionTag = attributionTag; 282 this.mCallback = callback; 283 this.mName = name; 284 this.mInfo = info; 285 this.mAppScanStats = appScanStats; 286 } 287 288 /** Link death recipient */ linkToDeath(IBinder.DeathRecipient deathRecipient)289 void linkToDeath(IBinder.DeathRecipient deathRecipient) { 290 // It might not be a binder object 291 if (mCallback == null) { 292 return; 293 } 294 try { 295 IBinder binder = ((IInterface) mCallback).asBinder(); 296 binder.linkToDeath(deathRecipient, 0); 297 mDeathRecipient = deathRecipient; 298 } catch (RemoteException e) { 299 Log.e(TAG, "Unable to link deathRecipient for app id " + mId); 300 } 301 } 302 303 /** Unlink death recipient */ cleanup()304 void cleanup() { 305 if (mDeathRecipient != null) { 306 try { 307 IBinder binder = ((IInterface) mCallback).asBinder(); 308 binder.unlinkToDeath(mDeathRecipient, 0); 309 } catch (NoSuchElementException e) { 310 Log.e(TAG, "Unable to unlink deathRecipient for app id " + mId); 311 } 312 } 313 mAppScanStats.isRegistered = false; 314 } 315 } 316 } 317