ContextMap.java revision 9d48f7ed161cb7c201b91f21fb1be36522a562e0
1/* 2 * Copyright (C) 2013 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 */ 16package com.android.bluetooth.gatt; 17 18import android.bluetooth.le.ScanSettings; 19import android.content.Context; 20import android.os.Binder; 21import android.os.IBinder; 22import android.os.IBinder.DeathRecipient; 23import android.os.IInterface; 24import android.os.RemoteException; 25import android.util.Log; 26import java.text.DateFormat; 27import java.text.SimpleDateFormat; 28import java.util.ArrayList; 29import java.util.Date; 30import java.util.HashSet; 31import java.util.Iterator; 32import java.util.LinkedList; 33import java.util.List; 34import java.util.NoSuchElementException; 35import java.util.Set; 36import java.util.UUID; 37import java.util.HashMap; 38import java.util.Map; 39 40import com.android.bluetooth.btservice.BluetoothProto; 41 42/** 43 * Helper class that keeps track of registered GATT applications. 44 * This class manages application callbacks and keeps track of GATT connections. 45 * @hide 46 */ 47/*package*/ class ContextMap<T> { 48 private static final String TAG = GattServiceConfig.TAG_PREFIX + "ContextMap"; 49 50 static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); 51 static final int NUM_SCAN_EVENTS_KEPT = 20; 52 ArrayList<BluetoothProto.ScanEvent> mScanEvents = 53 new ArrayList<BluetoothProto.ScanEvent>(NUM_SCAN_EVENTS_KEPT); 54 55 /** 56 * ScanStats class helps keep track of information about scans 57 * on a per application basis. 58 */ 59 class ScanStats { 60 static final int NUM_SCAN_DURATIONS_KEPT = 5; 61 62 String appName; 63 int scansStarted = 0; 64 int scansStopped = 0; 65 boolean isScanning = false; 66 boolean isRegistered = false; 67 boolean isOpportunisticScan = false; 68 boolean isBackgroundScan = false; 69 long minScanTime = Long.MAX_VALUE; 70 long maxScanTime = 0; 71 long totalScanTime = 0; 72 List<Long> lastScans = new ArrayList<Long>(NUM_SCAN_DURATIONS_KEPT + 1); 73 List<Long> lastScanTimestamps = new ArrayList<Long>(NUM_SCAN_DURATIONS_KEPT + 1); 74 long startTime = 0; 75 long stopTime = 0; 76 77 public ScanStats(String name) { 78 appName = name; 79 } 80 81 void recordScanStart(ScanSettings settings) { 82 if (isScanning) 83 return; 84 85 this.scansStarted++; 86 isScanning = true; 87 startTime = System.currentTimeMillis(); 88 if (settings != null) { 89 isOpportunisticScan = settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC; 90 isBackgroundScan = (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0; 91 } 92 93 BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent(); 94 scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_START); 95 scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE); 96 scanEvent.setInitiator(appName); 97 scanEvent.setEventTimeMillis(System.currentTimeMillis()); 98 99 lastScanTimestamps.add(startTime); 100 if (lastScanTimestamps.size() > NUM_SCAN_DURATIONS_KEPT) { 101 lastScanTimestamps.remove(0); 102 } 103 104 synchronized(mScanEvents) { 105 if(mScanEvents.size() == NUM_SCAN_EVENTS_KEPT) 106 mScanEvents.remove(0); 107 mScanEvents.add(scanEvent); 108 } 109 } 110 111 void recordScanStop() { 112 if (!isScanning) 113 return; 114 115 this.scansStopped++; 116 isScanning = false; 117 stopTime = System.currentTimeMillis(); 118 long currTime = stopTime - startTime; 119 120 isOpportunisticScan = false; 121 isBackgroundScan = false; 122 123 minScanTime = Math.min(currTime, minScanTime); 124 maxScanTime = Math.max(currTime, maxScanTime); 125 totalScanTime += currTime; 126 lastScans.add(currTime); 127 if (lastScans.size() > NUM_SCAN_DURATIONS_KEPT) { 128 lastScans.remove(0); 129 } 130 131 BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent(); 132 scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_STOP); 133 scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE); 134 scanEvent.setInitiator(appName); 135 scanEvent.setEventTimeMillis(System.currentTimeMillis()); 136 synchronized(mScanEvents) { 137 if (mScanEvents.size() == NUM_SCAN_EVENTS_KEPT) 138 mScanEvents.remove(0); 139 mScanEvents.add(scanEvent); 140 } 141 } 142 } 143 144 /** 145 * Connection class helps map connection IDs to device addresses. 146 */ 147 class Connection { 148 int connId; 149 String address; 150 int appId; 151 152 Connection(int connId, String address,int appId) { 153 this.connId = connId; 154 this.address = address; 155 this.appId = appId; 156 } 157 } 158 159 /** 160 * Application entry mapping UUIDs to appIDs and callbacks. 161 */ 162 class App { 163 /** The UUID of the application */ 164 UUID uuid; 165 166 /** The id of the application */ 167 int id; 168 169 /** The package name of the application */ 170 String name; 171 172 /** Application callbacks */ 173 T callback; 174 175 /** Death receipient */ 176 private IBinder.DeathRecipient mDeathRecipient; 177 178 /** Flag to signal that transport is congested */ 179 Boolean isCongested = false; 180 181 /** Internal callback info queue, waiting to be send on congestion clear */ 182 private List<CallbackInfo> congestionQueue = new ArrayList<CallbackInfo>(); 183 184 /** 185 * Creates a new app context. 186 */ 187 App(UUID uuid, T callback, String name) { 188 this.uuid = uuid; 189 this.callback = callback; 190 this.name = name; 191 } 192 193 /** 194 * Link death recipient 195 */ 196 void linkToDeath(IBinder.DeathRecipient deathRecipient) { 197 try { 198 IBinder binder = ((IInterface)callback).asBinder(); 199 binder.linkToDeath(deathRecipient, 0); 200 mDeathRecipient = deathRecipient; 201 } catch (RemoteException e) { 202 Log.e(TAG, "Unable to link deathRecipient for app id " + id); 203 } 204 } 205 206 /** 207 * Unlink death recipient 208 */ 209 void unlinkToDeath() { 210 if (mDeathRecipient != null) { 211 try { 212 IBinder binder = ((IInterface)callback).asBinder(); 213 binder.unlinkToDeath(mDeathRecipient,0); 214 } catch (NoSuchElementException e) { 215 Log.e(TAG, "Unable to unlink deathRecipient for app id " + id); 216 } 217 } 218 } 219 220 void queueCallback(CallbackInfo callbackInfo) { 221 congestionQueue.add(callbackInfo); 222 } 223 224 CallbackInfo popQueuedCallback() { 225 if (congestionQueue.size() == 0) return null; 226 return congestionQueue.remove(0); 227 } 228 } 229 230 /** Our internal application list */ 231 List<App> mApps = new ArrayList<App>(); 232 233 /** Internal map to keep track of logging information by app name */ 234 HashMap<String, ScanStats> mScanStats = new HashMap<String, ScanStats>(); 235 236 /** Internal list of connected devices **/ 237 Set<Connection> mConnections = new HashSet<Connection>(); 238 239 /** 240 * Add an entry to the application context list. 241 */ 242 void add(UUID uuid, T callback, Context context) { 243 String appName = context.getPackageManager().getNameForUid( 244 Binder.getCallingUid()); 245 if (appName == null) { 246 // Assign an app name if one isn't found 247 appName = "Unknown App (UID: " + Binder.getCallingUid() + ")"; 248 } 249 synchronized (mApps) { 250 mApps.add(new App(uuid, callback, appName)); 251 ScanStats scanStats = mScanStats.get(appName); 252 if (scanStats == null) { 253 scanStats = new ScanStats(appName); 254 mScanStats.put(appName, scanStats); 255 } 256 scanStats.isRegistered = true; 257 } 258 } 259 260 /** 261 * Remove the context for a given UUID 262 */ 263 void remove(UUID uuid) { 264 synchronized (mApps) { 265 Iterator<App> i = mApps.iterator(); 266 while (i.hasNext()) { 267 App entry = i.next(); 268 if (entry.uuid.equals(uuid)) { 269 entry.unlinkToDeath(); 270 mScanStats.get(entry.name).isRegistered = false; 271 i.remove(); 272 break; 273 } 274 } 275 } 276 } 277 278 /** 279 * Remove the context for a given application ID. 280 */ 281 void remove(int id) { 282 synchronized (mApps) { 283 Iterator<App> i = mApps.iterator(); 284 while (i.hasNext()) { 285 App entry = i.next(); 286 if (entry.id == id) { 287 entry.unlinkToDeath(); 288 mScanStats.get(entry.name).isRegistered = false; 289 i.remove(); 290 break; 291 } 292 } 293 } 294 } 295 296 /** 297 * Add a new connection for a given application ID. 298 */ 299 void addConnection(int id, int connId, String address) { 300 synchronized (mConnections) { 301 App entry = getById(id); 302 if (entry != null){ 303 mConnections.add(new Connection(connId, address, id)); 304 } 305 } 306 } 307 308 /** 309 * Remove a connection with the given ID. 310 */ 311 void removeConnection(int id, int connId) { 312 synchronized (mConnections) { 313 Iterator<Connection> i = mConnections.iterator(); 314 while (i.hasNext()) { 315 Connection connection = i.next(); 316 if (connection.connId == connId) { 317 i.remove(); 318 break; 319 } 320 } 321 } 322 } 323 324 /** 325 * Get an application context by ID. 326 */ 327 App getById(int id) { 328 Iterator<App> i = mApps.iterator(); 329 while (i.hasNext()) { 330 App entry = i.next(); 331 if (entry.id == id) return entry; 332 } 333 Log.e(TAG, "Context not found for ID " + id); 334 return null; 335 } 336 337 /** 338 * Get an application context by UUID. 339 */ 340 App getByUuid(UUID uuid) { 341 Iterator<App> i = mApps.iterator(); 342 while (i.hasNext()) { 343 App entry = i.next(); 344 if (entry.uuid.equals(uuid)) return entry; 345 } 346 Log.e(TAG, "Context not found for UUID " + uuid); 347 return null; 348 } 349 350 /** 351 * Get an application context by the calling Apps name. 352 */ 353 App getByName(String name) { 354 Iterator<App> i = mApps.iterator(); 355 while (i.hasNext()) { 356 App entry = i.next(); 357 if (entry.name.equals(name)) return entry; 358 } 359 Log.e(TAG, "Context not found for name " + name); 360 return null; 361 } 362 363 /** 364 * Get Logging info by ID 365 */ 366 ScanStats getScanStatsById(int id) { 367 App temp = getById(id); 368 if (temp != null) { 369 return mScanStats.get(temp.name); 370 } 371 return null; 372 } 373 374 /** 375 * Get Logging info by application name 376 */ 377 ScanStats getScanStatsByName(String name) { 378 return mScanStats.get(name); 379 } 380 381 /** 382 * Get the device addresses for all connected devices 383 */ 384 Set<String> getConnectedDevices() { 385 Set<String> addresses = new HashSet<String>(); 386 Iterator<Connection> i = mConnections.iterator(); 387 while (i.hasNext()) { 388 Connection connection = i.next(); 389 addresses.add(connection.address); 390 } 391 return addresses; 392 } 393 394 /** 395 * Get an application context by a connection ID. 396 */ 397 App getByConnId(int connId) { 398 Iterator<Connection> ii = mConnections.iterator(); 399 while (ii.hasNext()) { 400 Connection connection = ii.next(); 401 if (connection.connId == connId){ 402 return getById(connection.appId); 403 } 404 } 405 return null; 406 } 407 408 /** 409 * Returns a connection ID for a given device address. 410 */ 411 Integer connIdByAddress(int id, String address) { 412 App entry = getById(id); 413 if (entry == null) return null; 414 415 Iterator<Connection> i = mConnections.iterator(); 416 while (i.hasNext()) { 417 Connection connection = i.next(); 418 if (connection.address.equals(address) && connection.appId == id) 419 return connection.connId; 420 } 421 return null; 422 } 423 424 /** 425 * Returns the device address for a given connection ID. 426 */ 427 String addressByConnId(int connId) { 428 Iterator<Connection> i = mConnections.iterator(); 429 while (i.hasNext()) { 430 Connection connection = i.next(); 431 if (connection.connId == connId) return connection.address; 432 } 433 return null; 434 } 435 436 List<Connection> getConnectionByApp(int appId) { 437 List<Connection> currentConnections = new ArrayList<Connection>(); 438 Iterator<Connection> i = mConnections.iterator(); 439 while (i.hasNext()) { 440 Connection connection = i.next(); 441 if (connection.appId == appId) 442 currentConnections.add(connection); 443 } 444 return currentConnections; 445 } 446 447 /** 448 * Erases all application context entries. 449 */ 450 void clear() { 451 synchronized (mApps) { 452 Iterator<App> i = mApps.iterator(); 453 while (i.hasNext()) { 454 App entry = i.next(); 455 entry.unlinkToDeath(); 456 i.remove(); 457 } 458 } 459 460 synchronized (mConnections) { 461 mConnections.clear(); 462 } 463 } 464 465 /** 466 * Returns connect device map with addr and appid 467 */ 468 Map<Integer, String> getConnectedMap(){ 469 Map<Integer, String> connectedmap = new HashMap<Integer, String>(); 470 for(Connection conn: mConnections){ 471 connectedmap.put(conn.appId, conn.address); 472 } 473 return connectedmap; 474 } 475 476 /** 477 * Logs debug information. 478 */ 479 void dump(StringBuilder sb) { 480 long currTime = System.currentTimeMillis(); 481 482 sb.append(" Entries: " + mScanStats.size() + "\n\n"); 483 484 Iterator<Map.Entry<String,ScanStats>> it = mScanStats.entrySet().iterator(); 485 while (it.hasNext()) { 486 Map.Entry<String, ScanStats> entry = it.next(); 487 488 String name = entry.getKey(); 489 ScanStats scanStats = entry.getValue(); 490 491 long maxScanTime = scanStats.maxScanTime; 492 long minScanTime = scanStats.minScanTime; 493 long currScanTime = 0; 494 495 if (scanStats.isScanning) { 496 currScanTime = currTime - scanStats.startTime; 497 minScanTime = Math.min(currScanTime, minScanTime); 498 maxScanTime = Math.max(currScanTime, maxScanTime); 499 } 500 501 if (minScanTime == Long.MAX_VALUE) { 502 minScanTime = 0; 503 } 504 505 long lastScan = 0; 506 if (scanStats.stopTime != 0) { 507 lastScan = currTime - scanStats.stopTime; 508 } 509 510 long avgScanTime = 0; 511 if (scanStats.scansStarted > 0) { 512 avgScanTime = (scanStats.totalScanTime + currScanTime) / 513 scanStats.scansStarted; 514 } 515 516 sb.append(" " + name); 517 if (scanStats.isRegistered) sb.append(" (Registered)"); 518 if (scanStats.isOpportunisticScan) sb.append(" (Opportunistic)"); 519 if (scanStats.isBackgroundScan) sb.append(" (Background)"); 520 sb.append("\n"); 521 522 sb.append(" LE scans (started/stopped) : " + 523 scanStats.scansStarted + " / " + 524 scanStats.scansStopped + "\n"); 525 sb.append(" Scan time in ms (min/max/avg) : " + 526 minScanTime + " / " + 527 maxScanTime + " / " + 528 avgScanTime + "\n"); 529 530 if (scanStats.lastScans.size() != 0) { 531 sb.append(" Last " + scanStats.lastScans.size() + 532 " scans (timestamp - duration):\n"); 533 534 for (int i = 0; i < scanStats.lastScans.size(); i++) { 535 Date timestamp = new Date(scanStats.lastScanTimestamps.get(i)); 536 sb.append(" " + dateFormat.format(timestamp) + " - "); 537 sb.append(scanStats.lastScans.get(i) + "ms\n"); 538 } 539 } 540 541 if (scanStats.isRegistered) { 542 App appEntry = getByName(name); 543 sb.append(" Application ID : " + 544 appEntry.id + "\n"); 545 sb.append(" UUID : " + 546 appEntry.uuid + "\n"); 547 548 if (scanStats.isScanning) { 549 sb.append(" Current scan duration in ms : " + 550 currScanTime + "\n"); 551 } 552 553 List<Connection> connections = getConnectionByApp(appEntry.id); 554 sb.append(" Connections: " + connections.size() + "\n"); 555 556 Iterator<Connection> ii = connections.iterator(); 557 while(ii.hasNext()) { 558 Connection connection = ii.next(); 559 sb.append(" " + connection.connId + ": " + 560 connection.address + "\n"); 561 } 562 } 563 sb.append("\n"); 564 } 565 } 566 567 void dumpProto(BluetoothProto.BluetoothLog proto) { 568 synchronized(mScanEvents) { 569 for (BluetoothProto.ScanEvent event : mScanEvents) { 570 proto.addScanEvent(event); 571 } 572 } 573 } 574} 575