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