1 /* 2 * Copyright (C) 2021 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.google.android.tv.btservices; 18 19 import android.app.ActivityManager; 20 import android.app.ActivityManager.RunningAppProcessInfo; 21 import android.app.Service; 22 import android.bluetooth.BluetoothA2dp; 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothProfile; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.os.Binder; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Looper; 37 import android.provider.Settings; 38 import android.util.Log; 39 import android.widget.Toast; 40 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 41 import com.google.android.tv.btservices.remote.DefaultProxy; 42 import com.google.android.tv.btservices.remote.DfuBinary; 43 import com.google.android.tv.btservices.remote.DfuManager; 44 import com.google.android.tv.btservices.remote.DfuProvider; 45 import com.google.android.tv.btservices.remote.RemoteProxy; 46 import com.google.android.tv.btservices.remote.RemoteProxy.BatteryResult; 47 import com.google.android.tv.btservices.remote.RemoteProxy.DfuResult; 48 import com.google.android.tv.btservices.remote.Version; 49 import com.google.android.tv.btservices.settings.BluetoothDeviceProvider; 50 import com.google.common.base.Stopwatch; 51 import com.google.common.base.Ticker; 52 import java.io.FileDescriptor; 53 import java.io.PrintWriter; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Collections; 57 import java.util.HashMap; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 import java.util.concurrent.TimeUnit; 62 import java.util.function.Consumer; 63 64 public abstract class BluetoothDeviceService 65 extends Service implements DfuManager.Listener, DfuProvider.Listener { 66 private static final String TAG = "Atv.BtDeviceService"; 67 private static final boolean DEBUG = false; 68 private static final String USER_SETUP_COMPLETE = "user_setup_complete"; 69 private static final String TV_USER_SETUP_COMPLETE = "tv_user_setup_complete"; 70 private static final String FASTPAIR_PROCESS = "com.google.android.gms.ui"; 71 72 private static final long BATTERY_VALIDITY_PERIOD_MS = 73 TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); 74 75 private static final long NOTIFY_FIRMWARE_UPDATE_DELAY_MS = 7000; 76 private static final long PERIODIC_DFU_CHECK_MS = 77 TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); 78 private static final long INITIATE_DFU_CHECK_DELAY_MS = 79 TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); 80 // Following list is describes the set of intents to try in case we need to pair a remote. The 81 // list should be traversed in order. 82 private static final List<Intent> ORDERED_PAIRING_INTENTS = Collections.unmodifiableList( 83 Arrays.asList(new Intent("com.google.android.tvsetup.app.REPAIR_REMOTE"), 84 new Intent("com.google.android.intent.action.CONNECT_INPUT"))); 85 protected final Handler mHandler = new Handler(Looper.getMainLooper()); 86 private final List<BluetoothDeviceProvider.Listener> mListeners = new ArrayList<>(); 87 private final List<DfuManager.Listener> mDfuListeners = new ArrayList<>(); 88 private final Binder mBinder = new LocalBinder(); 89 // Maps a MAC address to the last time battery level was refreshed. This is 90 // used only by devices that uses polling for battery level. 91 private final Map<BluetoothDevice, Stopwatch> mLastBatteryRefreshWatch = new HashMap<>(); 92 private final Map<BluetoothDevice, RemoteProxy> mProxies = new HashMap<>(); 93 private final Ticker ticker = new Ticker() { 94 public long read() { 95 return android.os.SystemClock.elapsedRealtimeNanos(); 96 } 97 }; 98 private final Runnable mCheckDfu = this::checkDfu; 99 BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() { 100 @Override 101 public void onReceive(Context context, Intent intent) { 102 final String action = intent.getAction(); 103 final BluetoothDevice device = getDeviceHelper(intent); 104 // The sequence of a typical connection is: acl connected, bonding, bonded, profile 105 // connecting, profile connected. 106 if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 107 final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); 108 switch (state) { 109 case BluetoothDevice.BOND_BONDED: 110 mHandler.post(() -> addDevice(device)); 111 break; 112 case BluetoothDevice.BOND_NONE: 113 mHandler.post(() -> onDeviceUnbonded(device)); 114 break; 115 case BluetoothDevice.BOND_BONDING: 116 break; 117 default: 118 if (DEBUG) Log.e(TAG, "unknown state " + state + " " + device); 119 } 120 } else { 121 switch (action) { 122 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 123 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 124 mHandler.post(() -> onA2dpConnectionStateChanged(device.getName(), state)); 125 break; 126 case BluetoothDevice.ACTION_ACL_CONNECTED: 127 Log.i(TAG, "acl connected " + device); 128 if (device.getBondState() == BluetoothDevice.BOND_BONDED) { 129 mHandler.post(() -> addDevice(device)); 130 mHandler.post(() -> onDeviceUpdated(device)); 131 } 132 break; 133 case BluetoothDevice.ACTION_ACL_DISCONNECTED: 134 Log.i(TAG, "acl disconnected " + device); 135 mHandler.post(() -> removeDevice(device)); 136 mHandler.post(() -> onDeviceUpdated(device)); 137 break; 138 case BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED: 139 Log.i(TAG, "acl disconnect requested: " + device); 140 break; 141 } 142 } 143 } 144 }; 145 isRemote(BluetoothDevice device)146 protected boolean isRemote(BluetoothDevice device) { 147 boolean res = BluetoothUtils.isRemote(this, device); 148 if (DEBUG) { 149 Log.d(TAG, "Device " + device.getName() + " isRemote(): " + res); 150 } 151 return res; 152 } 153 getPairingIntents()154 protected static List<Intent> getPairingIntents() { 155 return ORDERED_PAIRING_INTENTS; 156 } 157 startPairingRemoteActivity(Context context)158 public static void startPairingRemoteActivity(Context context) { 159 PackageManager pm = context.getPackageManager(); 160 if (pm == null) { 161 return; 162 } 163 Intent candidateIntent = null; 164 intentLoop: 165 for (Intent intent : getPairingIntents()) { 166 for (ResolveInfo info : pm.queryIntentActivities(intent, 0)) { 167 if (info.activityInfo == null || info.activityInfo.applicationInfo == null) { 168 continue; 169 } 170 boolean isSystemApp = ((info.activityInfo.applicationInfo.flags & 171 ApplicationInfo.FLAG_SYSTEM) != 0) || 172 (info.activityInfo.applicationInfo.isOem()); 173 Log.i(TAG, "Found activity: " + info.activityInfo + " for intent: " + intent + 174 " is System/OEM app: " + isSystemApp); 175 if (!isSystemApp) { 176 continue; 177 } 178 candidateIntent = intent; 179 break intentLoop; 180 } 181 } 182 if (candidateIntent != null) { 183 Intent intent = new Intent(candidateIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 184 context.startActivity(intent); 185 } else { 186 Log.w(TAG, "Did not find suitable intents for remote pairing."); 187 } 188 } 189 getDeviceHelper(Intent intent)190 private static BluetoothDevice getDeviceHelper(Intent intent) { 191 if (intent == null) { 192 return null; 193 } 194 return intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 195 } 196 getDevices()197 protected static List<BluetoothDevice> getDevices() { 198 final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); 199 if (btAdapter != null) { 200 Set<BluetoothDevice> devices = btAdapter.getBondedDevices(); 201 if (devices != null) { 202 return new ArrayList<>(devices); 203 } 204 } 205 return Collections.emptyList(); 206 } 207 findDevice(String address)208 public static BluetoothDevice findDevice(String address) { 209 List<BluetoothDevice> devices = getDevices(); 210 BluetoothDevice curDevice = null; 211 for (BluetoothDevice device : devices) { 212 if (address.equals(device.getAddress())) { 213 curDevice = device; 214 break; 215 } 216 } 217 return curDevice; 218 } 219 forgetDevice(BluetoothDevice device)220 private static void forgetDevice(BluetoothDevice device) { 221 if (device == null || !device.removeBond()) { 222 Log.w(TAG, "failed to remove bond: " + device); 223 } 224 } 225 forgetAndRepair(Context context, BluetoothDevice device)226 public static void forgetAndRepair(Context context, BluetoothDevice device) { 227 if (device == null) { 228 Log.w(TAG, "null device"); 229 return; 230 } 231 if (!device.removeBond()) { 232 Log.w(TAG, "failed to remove bond"); 233 } 234 startPairingRemoteActivity(context); 235 } 236 isSetupComplete(Context context)237 private static boolean isSetupComplete(Context context) { 238 return Settings.Secure.getInt(context.getContentResolver(), USER_SETUP_COMPLETE, 0) > 0 239 && Settings.Secure.getInt(context.getContentResolver(), TV_USER_SETUP_COMPLETE, 0) 240 > 0; 241 } 242 getDfuProvider()243 protected abstract DfuProvider getDfuProvider(); 244 createRemoteProxy(BluetoothDevice device)245 protected abstract RemoteProxy createRemoteProxy(BluetoothDevice device); 246 createDefaultProxy(BluetoothDevice device)247 private RemoteProxy createDefaultProxy(BluetoothDevice device){ 248 return new DefaultProxy(this, device); 249 } 250 checkDfu()251 private void checkDfu() { 252 List<BluetoothDevice> devices = getDevices(); 253 for (BluetoothDevice device : devices) { 254 if (!isRemote(device)) { 255 continue; 256 } 257 deviceCheckDfu(device); 258 } 259 mHandler.removeCallbacks(mCheckDfu); 260 mHandler.postDelayed(mCheckDfu, PERIODIC_DFU_CHECK_MS); 261 } 262 onDeviceUpdated(BluetoothDevice device)263 private void onDeviceUpdated(BluetoothDevice device) { 264 mListeners.forEach(listener -> listener.onDeviceUpdated(device)); 265 } 266 onDfuUpdated(BluetoothDevice device, DfuResult res)267 private void onDfuUpdated(BluetoothDevice device, DfuResult res) { 268 mDfuListeners.forEach(listener -> listener.onDfuProgress(device, res)); 269 } 270 addDevice(BluetoothDevice device)271 private void addDevice(BluetoothDevice device) { 272 if (device == null || !device.isConnected()) { 273 return; 274 } 275 276 final RemoteProxy proxy; 277 if (!isRemote(device)) { 278 proxy = createDefaultProxy(device); 279 } else { 280 proxy = createRemoteProxy(device); 281 } 282 mProxies.put(device, proxy); 283 284 if (!proxy.initialize(this)) { 285 removeDevice(device); 286 return; 287 } 288 289 // Initiate version read. 290 refreshRemoteVersion(device, result -> { 291 onDeviceUpdated(device); 292 }); 293 294 // Initiate battery level read. 295 initializeDeviceBatteryLevelRead(device); 296 297 mHandler.postDelayed(() -> 298 deviceCheckDfu(device), NOTIFY_FIRMWARE_UPDATE_DELAY_MS); 299 } 300 removeDevice(BluetoothDevice device)301 private void removeDevice(BluetoothDevice device) { 302 RemoteProxy proxy = getRemoteProxy(device); 303 if (proxy == null) { 304 return; 305 } 306 // Clean up info for the disconnected device. 307 mHandler.post(() -> { 308 NotificationCenter.dismissUpdateNotification(device); 309 mLastBatteryRefreshWatch.remove(device); 310 mProxies.remove(device); 311 }); 312 } 313 onDeviceUnbonded(BluetoothDevice device)314 private void onDeviceUnbonded(BluetoothDevice device) { 315 NotificationCenter.dismissUpdateNotification(device); 316 onDeviceUpdated(device); 317 } 318 deviceCheckDfu(BluetoothDevice device)319 private void deviceCheckDfu(BluetoothDevice device) { 320 if (device == null || !device.isConnected() || !isRemote(device)) { 321 NotificationCenter.dismissUpdateNotification(device); 322 return; 323 } 324 325 RemoteProxy proxy = getRemoteProxy(device); 326 if (proxy == null) { 327 Log.e(TAG, "deviceCheckDfu: no proxy"); 328 NotificationCenter.dismissUpdateNotification(device); 329 return; 330 } 331 332 if (proxy.getDfuState() != null) { 333 Log.e(TAG, "deviceCheckDfu: already doing DFU for: " + device); 334 NotificationCenter.dismissUpdateNotification(device); 335 return; 336 } 337 338 if (proxy.supportsBackgroundDfu()) { 339 // `startRemoteDfu` checks if all criteria for dfu are met. 340 startRemoteDfu(device, true); 341 } else { 342 if (hasRemoteUpgrade(device) && !isRemoteLowBattery(device)) { 343 NotificationCenter.sendDfuNotification(device); 344 } else { 345 NotificationCenter.dismissUpdateNotification(device); 346 } 347 } 348 349 onDeviceUpdated(device); 350 } 351 connectDevice(BluetoothDevice device)352 private void connectDevice(BluetoothDevice device) { 353 if (device != null) { 354 CachedBluetoothDevice cachedDevice = 355 BluetoothUtils.getCachedBluetoothDevice(this, device); 356 if (cachedDevice != null) { 357 cachedDevice.connect(); 358 } 359 } 360 } 361 disconnectDevice(BluetoothDevice device)362 private void disconnectDevice(BluetoothDevice device) { 363 if (device != null) { 364 CachedBluetoothDevice cachedDevice = 365 BluetoothUtils.getCachedBluetoothDevice(this, device); 366 if (cachedDevice != null) { 367 cachedDevice.disconnect(); 368 } 369 } 370 } 371 renameDevice(BluetoothDevice device, String newName)372 private void renameDevice(BluetoothDevice device, String newName) { 373 if (device != null) { 374 device.setAlias(newName); 375 mHandler.post(() -> onDeviceUpdated(device)); 376 } 377 } 378 refreshLowBatteryNotification(BluetoothDevice device, boolean forceNotification)379 private void refreshLowBatteryNotification(BluetoothDevice device, boolean forceNotification) { 380 if (!isRemote(device)){ 381 return; 382 } 383 384 RemoteProxy proxy = getRemoteProxy(device); 385 if (proxy == null) { 386 return; 387 } 388 389 if (isRemoteCriticalBattery(device)) { 390 NotificationCenter.refreshLowBatteryNotification( 391 device, NotificationCenter.BatteryState.CRITICAL, forceNotification); 392 } else if (isRemoteLowBattery(device)) { 393 NotificationCenter.refreshLowBatteryNotification( 394 device, NotificationCenter.BatteryState.LOW, forceNotification); 395 } else { 396 NotificationCenter.refreshLowBatteryNotification( 397 device, NotificationCenter.BatteryState.GOOD, forceNotification); 398 } 399 } 400 getLowBatteryLevel(BluetoothDevice device)401 private int getLowBatteryLevel(BluetoothDevice device) { 402 RemoteProxy proxy = getRemoteProxy(device); 403 if (proxy == null) { 404 return RemoteProxy.DEFAULT_LOW_BATTERY_LEVEL; 405 } 406 return proxy.lowBatteryLevel(); 407 } 408 getCriticalBatteryLevel(BluetoothDevice device)409 private int getCriticalBatteryLevel(BluetoothDevice device) { 410 return RemoteProxy.DEFAULT_CRITICAL_BATTERY_LEVEL; 411 } 412 isRemoteLowBattery(BluetoothDevice device)413 protected boolean isRemoteLowBattery(BluetoothDevice device) { 414 if (device == null) { 415 return false; 416 } 417 if (!BluetoothUtils.isConnected(device)) { 418 return false; 419 } 420 if (!isRemote(device)) { 421 return false; 422 } 423 424 final int battery = getBatteryLevel(device); 425 if (battery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 426 return false; 427 } 428 return battery <= getLowBatteryLevel(device); 429 } 430 isRemoteCriticalBattery(BluetoothDevice device)431 protected boolean isRemoteCriticalBattery(BluetoothDevice device) { 432 if (device == null) { 433 return false; 434 } 435 if (!BluetoothUtils.isConnected(device)) { 436 return false; 437 } 438 if (!isRemote(device)) { 439 return false; 440 } 441 442 final int battery = getBatteryLevel(device); 443 if (battery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 444 return false; 445 } 446 return battery <= getCriticalBatteryLevel(device); 447 } 448 hasRemoteUpgrade(BluetoothDevice device)449 protected boolean hasRemoteUpgrade(BluetoothDevice device) { 450 if (device == null) { 451 return false; 452 } 453 if (!BluetoothUtils.isConnected(device)) { 454 return false; 455 } 456 if (!isRemote(device)) { 457 return false; 458 } 459 460 final DfuProvider provider = getDfuProvider(); 461 if (provider == null) { 462 return false; 463 } 464 465 Version version = getRemoteVersion(device); 466 if (version == null || version.equals(Version.BAD_VERSION)) { 467 return false; 468 } 469 final String name = BluetoothUtils.getOriginalName(device); 470 return provider.getDfu(name, version) != null; 471 } 472 473 // DfuManager.Listener implementation. 474 @Override onDfuProgress(BluetoothDevice device, RemoteProxy.DfuResult result)475 public void onDfuProgress(BluetoothDevice device, RemoteProxy.DfuResult result) { 476 mHandler.post(() -> onDfuUpdated(device, result)); 477 } 478 getRemoteDfuState(BluetoothDevice device)479 private DfuResult getRemoteDfuState(BluetoothDevice device) { 480 if (device == null) { 481 Log.e(TAG, "getRemoteDfuState: no device"); 482 return null; 483 } 484 if (!BluetoothUtils.isConnected(device)) { 485 return null; 486 } 487 if (!isRemote(device)) { 488 Log.e(TAG, "getRemoteDfuState: not a remote " + device); 489 return null; 490 } 491 492 RemoteProxy proxy = getRemoteProxy(device); 493 return proxy != null ? proxy.getDfuState() : null; 494 } 495 startRemoteDfu(String address)496 protected void startRemoteDfu(String address) { 497 BluetoothDevice device = findDevice(address); 498 startRemoteDfu(device, false); 499 } 500 startRemoteDfu(BluetoothDevice device, boolean background)501 protected void startRemoteDfu(BluetoothDevice device, boolean background) { 502 if (device == null) { 503 Log.e(TAG, "startRemoteDfu: "); 504 return; 505 } 506 if (!isRemote(device)) { 507 Log.e(TAG, "startRemoteDfu: not a remote " + device); 508 return; 509 } 510 511 if (!hasRemoteUpgrade(device)) { 512 Log.e(TAG, "startRemoteDfu: not eligible for upgrade " + device); 513 return; 514 } 515 516 if (isRemoteLowBattery(device)) { 517 Log.e(TAG, "startRemoteDfu: cannot update due to low battery " + device); 518 return; 519 } 520 521 if (!isSetupComplete(this)) { 522 Log.e(TAG, "startRemoteDfu: oobe must be completed before updating " + device); 523 return; 524 } 525 526 final DfuProvider provider = getDfuProvider(); 527 if (provider == null) { 528 Log.e(TAG, "startRemoteDfu: no remote dfu provider"); 529 return; 530 } 531 532 RemoteProxy proxy = getRemoteProxy(device); 533 if (proxy == null) { 534 Log.e(TAG, "startRemoteDfu: proxy is null"); 535 return; 536 } 537 538 proxy.refreshVersion().thenAccept(status -> { 539 if (!status) { 540 return; 541 } 542 543 Version currentVersion = proxy.getLastKnownVersion(); 544 final String name = BluetoothUtils.getOriginalName(device); 545 final DfuBinary dfu = provider.getDfu(name, currentVersion); 546 547 if (dfu == null) { 548 Log.e(TAG, "Unexpected null dfu binary"); 549 return; 550 } 551 552 Set<Version> versionsNeedRepairing = provider.getManualReconnectionVersions(); 553 554 final boolean needsRepair = versionsNeedRepairing.contains(currentVersion); 555 556 Log.i(TAG, "current: " + currentVersion + " new version: " + dfu.getVersion() + 557 " repair: " + needsRepair); 558 559 NotificationCenter.dismissUpdateNotification(device); 560 proxy.requestDfu(dfu, this, background).thenAccept(result -> { 561 DfuResult newResult = result; 562 if (result == DfuResult.RESULT_DEVICE_BUSY) { 563 Log.i(TAG, "Device busy, skipping remote update request for " + device); 564 return; 565 } 566 567 if (result == DfuResult.RESULT_SUCCESS && needsRepair) { 568 newResult = DfuResult.RESULT_SUCCESS_NEEDS_PAIRING; 569 } 570 onDfuUpdated(device, newResult); 571 }); 572 }); 573 } 574 onA2dpConnectionStateChanged(String deviceName, int connectionStatus)575 private void onA2dpConnectionStateChanged(String deviceName, int connectionStatus) { 576 // Avoiding showing Toast while Fastpair is in Foreground. 577 if (fastPairInForeground()) { 578 return; 579 } 580 String resStr; 581 String text; 582 switch (connectionStatus) { 583 case BluetoothProfile.STATE_CONNECTED: 584 resStr = getResources().getString(R.string.settings_bt_pair_toast_connected); 585 text = String.format(resStr, deviceName); 586 Toast.makeText(BluetoothDeviceService.this.getApplicationContext(), 587 text, Toast.LENGTH_SHORT).show(); 588 break; 589 case BluetoothProfile.STATE_DISCONNECTED: 590 resStr = getResources().getString(R.string.settings_bt_pair_toast_disconnected); 591 text = String.format(resStr, deviceName); 592 Toast.makeText(BluetoothDeviceService.this.getApplicationContext(), 593 text, Toast.LENGTH_SHORT).show(); 594 break; 595 case BluetoothProfile.STATE_CONNECTING: 596 case BluetoothProfile.STATE_DISCONNECTING: 597 default: 598 break; 599 } 600 } 601 fastPairInForeground()602 private boolean fastPairInForeground() { 603 ActivityManager activityManager = getSystemService(ActivityManager.class); 604 if (activityManager == null) { 605 return false; 606 } 607 List<RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses(); 608 if (appProcesses == null) { 609 return false; 610 } 611 for (RunningAppProcessInfo appProcess : appProcesses) { 612 if (FASTPAIR_PROCESS.equals(appProcess.processName) && 613 appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { 614 return true; 615 } 616 } 617 return false; 618 } 619 getRemoteProxy(BluetoothDevice device)620 private RemoteProxy getRemoteProxy(BluetoothDevice device) { 621 if (device == null) { 622 return null; 623 } 624 return mProxies.get(device); 625 } 626 627 /** 628 * Synchronous remote version read from RemoteProxy. 629 * 630 * This should only be called by UI thread that needs result immediately. 631 */ getRemoteVersion(BluetoothDevice device)632 protected Version getRemoteVersion(BluetoothDevice device) { 633 RemoteProxy proxy = getRemoteProxy(device); 634 635 if (proxy != null) { 636 return proxy.getLastKnownVersion(); 637 } else { 638 return Version.BAD_VERSION; 639 } 640 } 641 getRemoteVersion(String address)642 protected Version getRemoteVersion(String address) { 643 BluetoothDevice device = findDevice(address); 644 return getRemoteVersion(device); 645 } 646 647 /** Asynchronous remote version refresh. */ refreshRemoteVersion(BluetoothDevice device, Consumer<Version> callback)648 private void refreshRemoteVersion(BluetoothDevice device, Consumer<Version> callback) { 649 RemoteProxy proxy = getRemoteProxy(device); 650 651 if (proxy != null) { 652 proxy.refreshVersion().thenAccept(status -> { 653 if (status) { 654 Version version = proxy.getLastKnownVersion(); 655 callback.accept(version); 656 } else { 657 Log.w(TAG, "Failed to refresh remote version."); 658 } 659 }); 660 } 661 } 662 getBatteryLevel(BluetoothDevice device)663 private int getBatteryLevel(BluetoothDevice device) { 664 if (device == null) { 665 return BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 666 } 667 if (!BluetoothUtils.isConnected(device)) { 668 return BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 669 } 670 671 RemoteProxy proxy = getRemoteProxy(device); 672 if (proxy != null) { 673 BatteryResult result = proxy.getLastKnownBatteryLevel(); 674 675 if (result.code() == BatteryResult.SUCCESS) { 676 Stopwatch stopwatch = mLastBatteryRefreshWatch.get(device); 677 if (stopwatch != null && 678 stopwatch.elapsed(TimeUnit.MILLISECONDS) > BATTERY_VALIDITY_PERIOD_MS) { 679 stopwatch.reset(); 680 stopwatch.start(); 681 682 proxy.refreshBatteryLevel().thenAccept(status -> { 683 if (status) { 684 refreshLowBatteryNotification(device, false); 685 onDeviceUpdated(device); 686 } 687 }); 688 } 689 690 return result.battery(); 691 } 692 } 693 694 return BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 695 } 696 mapBatteryLevel(Context context, BluetoothDevice device, int level)697 private String mapBatteryLevel(Context context, BluetoothDevice device, int level) { 698 RemoteProxy proxy = getRemoteProxy(device); 699 700 if (proxy == null) { 701 return context.getString( 702 R.string.settings_remote_battery_level_percentage_label, level); 703 } 704 705 return proxy.mapBatteryLevel(context, level); 706 } 707 initializeDeviceBatteryLevelRead(BluetoothDevice device)708 private void initializeDeviceBatteryLevelRead(BluetoothDevice device) { 709 if (device == null) { 710 Log.w(TAG, "initializeDeviceBatteryLevelRead for null device"); 711 return; 712 } 713 714 RemoteProxy proxy = getRemoteProxy(device); 715 if (proxy != null) { 716 proxy.registerBatteryLevelCallback(() -> { 717 refreshLowBatteryNotification(device, false); 718 onDeviceUpdated(device); 719 }).thenAccept(callbackRegistered -> { 720 proxy.refreshBatteryLevel().thenAccept(result -> { 721 if (result) { 722 if (!callbackRegistered) { 723 // Callback is not registered. Enable polling. 724 Stopwatch stopwatch = Stopwatch.createStarted(ticker); 725 mLastBatteryRefreshWatch.put(device, stopwatch); 726 } else { 727 mLastBatteryRefreshWatch.remove(device); 728 } 729 730 refreshLowBatteryNotification(device, true); 731 onDeviceUpdated(device); 732 } 733 }); 734 }); 735 } 736 } 737 initiateDfuCheck()738 private void initiateDfuCheck() { 739 NotificationCenter.resetUpdateNotification(); 740 checkDfu(); 741 } 742 743 // Implements DfuProvider.Listener 744 @Override onDfuFileAdd()745 public void onDfuFileAdd() { 746 initiateDfuCheck(); 747 } 748 749 @Override onBind(Intent intent)750 public IBinder onBind(Intent intent) { 751 return mBinder; 752 } 753 754 @Override onCreate()755 public void onCreate() { 756 super.onCreate(); 757 if (DEBUG) Log.e(TAG, "onCreate"); 758 759 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); 760 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); 761 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 762 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 763 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); // Headset connection 764 registerReceiver(mBluetoothReceiver, filter); 765 for (BluetoothDevice device : getDevices()) { 766 if (device.isConnected()) { 767 addDevice(device); 768 } 769 } 770 771 mHandler.postDelayed(this::initiateDfuCheck, INITIATE_DFU_CHECK_DELAY_MS); 772 773 NotificationCenter.initialize(this); 774 } 775 776 @Override onDestroy()777 public void onDestroy() { 778 if (DEBUG) Log.e(TAG, "onDestroy"); 779 unregisterReceiver(mBluetoothReceiver); 780 781 mHandler.removeCallbacksAndMessages(null); 782 super.onDestroy(); 783 } 784 785 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)786 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 787 for (BluetoothDevice device : getDevices()) { 788 if (!device.isConnected()) { 789 continue; 790 } 791 792 RemoteProxy proxy = getRemoteProxy(device); 793 794 if (proxy == null) { 795 continue; 796 } 797 798 writer.printf("%s (%s):%n", device.getName(), device.getAddress()); 799 800 Version version = proxy.getLastKnownVersion(); 801 writer.printf(" Firmware Version: %s%n", version.toString()); 802 803 int battLevel = proxy.getLastKnownBatteryLevel().battery(); 804 writer.printf(" Battery Level: %d%n", battLevel); 805 } 806 } 807 808 public class LocalBinder extends Binder implements BluetoothDeviceProvider { 809 getDevices()810 public List<BluetoothDevice> getDevices() { 811 return BluetoothDeviceService.getDevices(); 812 } 813 814 @Override connectDevice(BluetoothDevice device)815 public void connectDevice(BluetoothDevice device) { 816 BluetoothDeviceService.this.connectDevice(device); 817 } 818 819 @Override disconnectDevice(BluetoothDevice device)820 public void disconnectDevice(BluetoothDevice device) { 821 BluetoothDeviceService.this.disconnectDevice(device); 822 } 823 824 @Override forgetDevice(BluetoothDevice device)825 public void forgetDevice(BluetoothDevice device) { 826 BluetoothDeviceService.forgetDevice(device); 827 } 828 829 @Override renameDevice(BluetoothDevice device, String newName)830 public void renameDevice(BluetoothDevice device, String newName) { 831 BluetoothDeviceService.this.renameDevice(device, newName); 832 } 833 834 @Override getBatteryLevel(BluetoothDevice device)835 public int getBatteryLevel(BluetoothDevice device) { 836 return BluetoothDeviceService.this.getBatteryLevel(device); 837 } 838 839 @Override mapBatteryLevel(Context context, BluetoothDevice device, int level)840 public String mapBatteryLevel(Context context, BluetoothDevice device, int level) { 841 return BluetoothDeviceService.this.mapBatteryLevel(context, device, level); 842 } 843 844 @Override getVersion(BluetoothDevice device)845 public Version getVersion(BluetoothDevice device) { 846 return BluetoothDeviceService.this.getRemoteVersion(device); 847 } 848 849 @Override hasUpgrade(BluetoothDevice device)850 public boolean hasUpgrade(BluetoothDevice device) { 851 return BluetoothDeviceService.this.hasRemoteUpgrade(device); 852 } 853 854 @Override isBatteryLow(BluetoothDevice device)855 public boolean isBatteryLow(BluetoothDevice device) { 856 return BluetoothDeviceService.this.isRemoteLowBattery(device); 857 } 858 859 @Override getDfuState(BluetoothDevice device)860 public DfuResult getDfuState(BluetoothDevice device) { 861 return BluetoothDeviceService.this.getRemoteDfuState(device); 862 } 863 864 @Override startDfu(BluetoothDevice device)865 public void startDfu(BluetoothDevice device) { 866 BluetoothDeviceService.this.startRemoteDfu(device, false); 867 } 868 869 @Override addListener(BluetoothDeviceProvider.Listener listener)870 public void addListener(BluetoothDeviceProvider.Listener listener) { 871 mHandler.post(() -> { 872 mListeners.add(listener); 873 874 // Trigger first update after listener callback is registered. 875 for (BluetoothDevice device : getDevices()) { 876 if (device.isConnected()) { 877 listener.onDeviceUpdated(device); 878 } 879 } 880 }); 881 } 882 883 @Override removeListener(BluetoothDeviceProvider.Listener listener)884 public void removeListener(BluetoothDeviceProvider.Listener listener) { 885 mHandler.post(() -> mListeners.remove(listener)); 886 } 887 888 @Override addListener(DfuManager.Listener listener)889 public void addListener(DfuManager.Listener listener) { 890 mHandler.post(() -> mDfuListeners.add(listener)); 891 } 892 893 @Override removeListener(DfuManager.Listener listener)894 public void removeListener(DfuManager.Listener listener) { 895 mHandler.post(() -> mDfuListeners.remove(listener)); 896 } 897 dismissDfuNotification(String address)898 public void dismissDfuNotification(String address) { 899 BluetoothDevice device = BluetoothDeviceService.findDevice(address); 900 NotificationCenter.dismissUpdateNotification(device); 901 } 902 } 903 } 904