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