1 /*
2  * Copyright (C) 2019 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.android.settings.bluetooth;
18 
19 import static com.android.settings.bluetooth.Utils.preloadAndRun;
20 
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.database.Cursor;
27 import android.graphics.Bitmap;
28 import android.graphics.PorterDuff;
29 import android.graphics.PorterDuffColorFilter;
30 import android.graphics.drawable.Drawable;
31 import android.net.Uri;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.provider.MediaStore;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.TextView;
43 
44 import androidx.annotation.VisibleForTesting;
45 import androidx.preference.PreferenceScreen;
46 
47 import com.android.settings.R;
48 import com.android.settings.core.BasePreferenceController;
49 import com.android.settings.fuelgauge.BatteryMeterView;
50 import com.android.settingslib.bluetooth.BluetoothUtils;
51 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
52 import com.android.settingslib.core.lifecycle.LifecycleObserver;
53 import com.android.settingslib.core.lifecycle.events.OnDestroy;
54 import com.android.settingslib.core.lifecycle.events.OnStart;
55 import com.android.settingslib.core.lifecycle.events.OnStop;
56 import com.android.settingslib.utils.StringUtil;
57 import com.android.settingslib.utils.ThreadUtils;
58 import com.android.settingslib.widget.LayoutPreference;
59 
60 import com.google.common.base.Supplier;
61 import com.google.common.base.Suppliers;
62 
63 import java.io.IOException;
64 import java.util.HashMap;
65 import java.util.HashSet;
66 import java.util.List;
67 import java.util.Map;
68 import java.util.Set;
69 import java.util.concurrent.TimeUnit;
70 
71 /**
72  * This class adds a header with device name and status (connected/disconnected, etc.).
73  */
74 public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceController implements
75         LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback {
76     private static final String TAG = "AdvancedBtHeaderCtrl";
77     private static final int LOW_BATTERY_LEVEL = 15;
78     private static final int CASE_LOW_BATTERY_LEVEL = 19;
79     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
80 
81     private static final String PATH = "time_remaining";
82     private static final String QUERY_PARAMETER_ADDRESS = "address";
83     private static final String QUERY_PARAMETER_BATTERY_ID = "battery_id";
84     private static final String QUERY_PARAMETER_BATTERY_LEVEL = "battery_level";
85     private static final String QUERY_PARAMETER_TIMESTAMP = "timestamp";
86     private static final String BATTERY_ESTIMATE = "battery_estimate";
87     private static final String ESTIMATE_READY = "estimate_ready";
88     private static final String DATABASE_ID = "id";
89     private static final String DATABASE_BLUETOOTH = "Bluetooth";
90     private static final long TIME_OF_HOUR = TimeUnit.SECONDS.toMillis(3600);
91     private static final long TIME_OF_MINUTE = TimeUnit.SECONDS.toMillis(60);
92     private static final int LEFT_DEVICE_ID = 1;
93     private static final int RIGHT_DEVICE_ID = 2;
94     private static final int CASE_DEVICE_ID = 3;
95     private static final int MAIN_DEVICE_ID = 4;
96     private static final float HALF_ALPHA = 0.5f;
97 
98     @VisibleForTesting
99     LayoutPreference mLayoutPreference;
100     @VisibleForTesting
101     final Map<String, Bitmap> mIconCache;
102     private CachedBluetoothDevice mCachedDevice;
103     private Set<BluetoothDevice> mBluetoothDevices;
104     @VisibleForTesting
105     BluetoothAdapter mBluetoothAdapter;
106     @VisibleForTesting
107     Handler mHandler = new Handler(Looper.getMainLooper());
108     @VisibleForTesting
109     boolean mIsLeftDeviceEstimateReady;
110     @VisibleForTesting
111     boolean mIsRightDeviceEstimateReady;
112     @VisibleForTesting
113     final BluetoothAdapter.OnMetadataChangedListener mMetadataListener =
114             new BluetoothAdapter.OnMetadataChangedListener() {
115                 @Override
116                 public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) {
117                     Log.d(TAG, String.format("Metadata updated in Device %s: %d = %s.",
118                             device.getAnonymizedAddress(),
119                             key, value == null ? null : new String(value)));
120                     refresh();
121                 }
122             };
123 
AdvancedBluetoothDetailsHeaderController(Context context, String prefKey)124     public AdvancedBluetoothDetailsHeaderController(Context context, String prefKey) {
125         super(context, prefKey);
126         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
127         mIconCache = new HashMap<>();
128     }
129 
130     @Override
getAvailabilityStatus()131     public int getAvailabilityStatus() {
132         if (mCachedDevice == null) {
133             return CONDITIONALLY_UNAVAILABLE;
134         }
135         return BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice())
136                 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
137     }
138 
139     @Override
displayPreference(PreferenceScreen screen)140     public void displayPreference(PreferenceScreen screen) {
141         super.displayPreference(screen);
142         mLayoutPreference = screen.findPreference(getPreferenceKey());
143         mLayoutPreference.setVisible(isAvailable());
144     }
145 
146     @Override
onStart()147     public void onStart() {
148         if (!isAvailable()) {
149             return;
150         }
151         registerBluetoothDevice();
152         refresh();
153     }
154 
155     @Override
onStop()156     public void onStop() {
157         unRegisterBluetoothDevice();
158     }
159 
160     @Override
onDestroy()161     public void onDestroy() {
162         // Destroy icon bitmap associated with this header
163         for (Bitmap bitmap : mIconCache.values()) {
164             if (bitmap != null) {
165                 bitmap.recycle();
166             }
167         }
168         mIconCache.clear();
169     }
170 
init(CachedBluetoothDevice cachedBluetoothDevice)171     public void init(CachedBluetoothDevice cachedBluetoothDevice) {
172         mCachedDevice = cachedBluetoothDevice;
173     }
174 
registerBluetoothDevice()175     private void registerBluetoothDevice() {
176         if (mBluetoothAdapter == null) {
177             Log.d(TAG, "No mBluetoothAdapter");
178             return;
179         }
180         if (mBluetoothDevices == null) {
181             mBluetoothDevices = new HashSet<>();
182         }
183         mBluetoothDevices.clear();
184         if (mCachedDevice.getDevice() != null) {
185             mBluetoothDevices.add(mCachedDevice.getDevice());
186         }
187         mCachedDevice.getMemberDevice().forEach(cbd -> {
188             if (cbd != null) {
189                 mBluetoothDevices.add(cbd.getDevice());
190             }
191         });
192         if (mBluetoothDevices.isEmpty()) {
193             Log.d(TAG, "No BT device to register.");
194             return;
195         }
196         mCachedDevice.registerCallback(this);
197         Set<BluetoothDevice> errorDevices = new HashSet<>();
198         mBluetoothDevices.forEach(bd -> {
199             try {
200                 boolean isSuccess = mBluetoothAdapter.addOnMetadataChangedListener(bd,
201                         mContext.getMainExecutor(), mMetadataListener);
202                 if (!isSuccess) {
203                     Log.e(TAG, bd.getAnonymizedAddress() + ": add into Listener failed");
204                     errorDevices.add(bd);
205                 }
206             } catch (NullPointerException e) {
207                 errorDevices.add(bd);
208                 Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString());
209             } catch (IllegalArgumentException e) {
210                 errorDevices.add(bd);
211                 Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString());
212             }
213         });
214         for (BluetoothDevice errorDevice : errorDevices) {
215             mBluetoothDevices.remove(errorDevice);
216             Log.d(TAG, "mBluetoothDevices remove " + errorDevice.getAnonymizedAddress());
217         }
218     }
219 
unRegisterBluetoothDevice()220     private void unRegisterBluetoothDevice() {
221         if (mBluetoothAdapter == null) {
222             Log.d(TAG, "No mBluetoothAdapter");
223             return;
224         }
225         if (mBluetoothDevices == null || mBluetoothDevices.isEmpty()) {
226             Log.d(TAG, "No BT device to unregister.");
227             return;
228         }
229         mCachedDevice.unregisterCallback(this);
230         mBluetoothDevices.forEach(bd -> {
231             try {
232                 mBluetoothAdapter.removeOnMetadataChangedListener(bd, mMetadataListener);
233             } catch (NullPointerException e) {
234                 Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString());
235             } catch (IllegalArgumentException e) {
236                 Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString());
237             }
238         });
239         mBluetoothDevices.clear();
240     }
241 
242     @VisibleForTesting
refresh()243     void refresh() {
244         if (mLayoutPreference != null && mCachedDevice != null) {
245             Supplier<String> deviceName = Suppliers.memoize(() -> mCachedDevice.getName());
246             Supplier<Boolean> disconnected =
247                     Suppliers.memoize(() -> !mCachedDevice.isConnected() || mCachedDevice.isBusy());
248             Supplier<Boolean> isUntetheredHeadset =
249                     Suppliers.memoize(() -> isUntetheredHeadset(mCachedDevice.getDevice()));
250             Supplier<String> summaryText =
251                     Suppliers.memoize(
252                             () -> {
253                                 if (disconnected.get() || isUntetheredHeadset.get()) {
254                                     return mCachedDevice.getConnectionSummary(
255                                             /* shortSummary= */ true);
256                                 }
257                                 return mCachedDevice.getConnectionSummary(
258                                         BluetoothUtils.getIntMetaData(
259                                                         mCachedDevice.getDevice(),
260                                                         BluetoothDevice.METADATA_MAIN_BATTERY)
261                                                 != BluetoothUtils.META_INT_ERROR);
262                             });
263             preloadAndRun(
264                     List.of(deviceName, disconnected, isUntetheredHeadset, summaryText),
265                     () -> {
266                         final TextView title =
267                                 mLayoutPreference.findViewById(R.id.entity_header_title);
268                         title.setText(deviceName.get());
269                         final TextView summary =
270                                 mLayoutPreference.findViewById(R.id.entity_header_summary);
271 
272                         if (disconnected.get()) {
273                             summary.setText(summaryText.get());
274                             updateDisconnectLayout();
275                             return;
276                         }
277                         if (isUntetheredHeadset.get()) {
278                             summary.setText(summaryText.get());
279                             updateSubLayout(
280                                     mLayoutPreference.findViewById(R.id.layout_left),
281                                     BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON,
282                                     BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
283                                     BluetoothDevice.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD,
284                                     BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING,
285                                     R.string.bluetooth_left_name,
286                                     LEFT_DEVICE_ID);
287 
288                             updateSubLayout(
289                                     mLayoutPreference.findViewById(R.id.layout_middle),
290                                     BluetoothDevice.METADATA_UNTETHERED_CASE_ICON,
291                                     BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
292                                     BluetoothDevice.METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD,
293                                     BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING,
294                                     R.string.bluetooth_middle_name,
295                                     CASE_DEVICE_ID);
296 
297                             updateSubLayout(
298                                     mLayoutPreference.findViewById(R.id.layout_right),
299                                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON,
300                                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
301                                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
302                                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
303                                     R.string.bluetooth_right_name,
304                                     RIGHT_DEVICE_ID);
305 
306                             showBothDevicesBatteryPredictionIfNecessary();
307                         } else {
308                             mLayoutPreference
309                                     .findViewById(R.id.layout_left)
310                                     .setVisibility(View.GONE);
311                             mLayoutPreference
312                                     .findViewById(R.id.layout_right)
313                                     .setVisibility(View.GONE);
314 
315                             summary.setText(summaryText.get());
316                             updateSubLayout(
317                                     mLayoutPreference.findViewById(R.id.layout_middle),
318                                     BluetoothDevice.METADATA_MAIN_ICON,
319                                     BluetoothDevice.METADATA_MAIN_BATTERY,
320                                     BluetoothDevice.METADATA_MAIN_LOW_BATTERY_THRESHOLD,
321                                     BluetoothDevice.METADATA_MAIN_CHARGING,
322                                     /* titleResId= */ 0,
323                                     MAIN_DEVICE_ID);
324                         }
325                     });
326         }
327     }
328 
329     @VisibleForTesting
createBtBatteryIcon(Context context, int level, boolean charging)330     Drawable createBtBatteryIcon(Context context, int level, boolean charging) {
331         final BatteryMeterView.BatteryMeterDrawable drawable =
332                 new BatteryMeterView.BatteryMeterDrawable(context,
333                         context.getColor(com.android.settingslib.R.color.meter_background_color),
334                         context.getResources().getDimensionPixelSize(
335                                 R.dimen.advanced_bluetooth_battery_meter_width),
336                         context.getResources().getDimensionPixelSize(
337                                 R.dimen.advanced_bluetooth_battery_meter_height));
338         drawable.setBatteryLevel(level);
339         drawable.setColorFilter(new PorterDuffColorFilter(
340                 com.android.settings.Utils.getColorAttrDefaultColor(context,
341                         android.R.attr.colorControlNormal),
342                 PorterDuff.Mode.SRC));
343         drawable.setCharging(charging);
344 
345         return drawable;
346     }
347 
updateSubLayout( LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, int lowBatteryMetaKey, int chargeMetaKey, int titleResId, int deviceId)348     private void updateSubLayout(
349             LinearLayout linearLayout,
350             int iconMetaKey,
351             int batteryMetaKey,
352             int lowBatteryMetaKey,
353             int chargeMetaKey,
354             int titleResId,
355             int deviceId) {
356         if (linearLayout == null) {
357             return;
358         }
359         BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
360         Supplier<String> iconUri =
361                 Suppliers.memoize(
362                         () -> BluetoothUtils.getStringMetaData(bluetoothDevice, iconMetaKey));
363         Supplier<Integer> batteryLevel =
364                 Suppliers.memoize(
365                         () -> BluetoothUtils.getIntMetaData(bluetoothDevice, batteryMetaKey));
366         Supplier<Boolean> charging =
367                 Suppliers.memoize(
368                         () -> BluetoothUtils.getBooleanMetaData(bluetoothDevice, chargeMetaKey));
369         Supplier<Integer> lowBatteryLevel =
370                 Suppliers.memoize(
371                         () -> {
372                             int level =
373                                     BluetoothUtils.getIntMetaData(
374                                             bluetoothDevice, lowBatteryMetaKey);
375                             if (level == BluetoothUtils.META_INT_ERROR) {
376                                 if (batteryMetaKey
377                                         == BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY) {
378                                     level = CASE_LOW_BATTERY_LEVEL;
379                                 } else {
380                                     level = LOW_BATTERY_LEVEL;
381                                 }
382                             }
383                             return level;
384                         });
385         Supplier<Boolean> isUntethered =
386                 Suppliers.memoize(() -> isUntetheredHeadset(bluetoothDevice));
387         Supplier<Integer> nativeBatteryLevel = Suppliers.memoize(bluetoothDevice::getBatteryLevel);
388         preloadAndRun(
389                 List.of(
390                         iconUri,
391                         batteryLevel,
392                         charging,
393                         lowBatteryLevel,
394                         isUntethered,
395                         nativeBatteryLevel),
396                 () ->
397                         updateSubLayoutUi(
398                                 linearLayout,
399                                 iconMetaKey,
400                                 batteryMetaKey,
401                                 lowBatteryMetaKey,
402                                 chargeMetaKey,
403                                 titleResId,
404                                 deviceId,
405                                 iconUri,
406                                 batteryLevel,
407                                 charging,
408                                 lowBatteryLevel,
409                                 isUntethered,
410                                 nativeBatteryLevel));
411     }
412 
updateSubLayoutUi( LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, int lowBatteryMetaKey, int chargeMetaKey, int titleResId, int deviceId, Supplier<String> preloadedIconUri, Supplier<Integer> preloadedBatteryLevel, Supplier<Boolean> preloadedCharging, Supplier<Integer> preloadedLowBatteryLevel, Supplier<Boolean> preloadedIsUntethered, Supplier<Integer> preloadedNativeBatteryLevel)413     private void updateSubLayoutUi(
414             LinearLayout linearLayout,
415             int iconMetaKey,
416             int batteryMetaKey,
417             int lowBatteryMetaKey,
418             int chargeMetaKey,
419             int titleResId,
420             int deviceId,
421             Supplier<String> preloadedIconUri,
422             Supplier<Integer> preloadedBatteryLevel,
423             Supplier<Boolean> preloadedCharging,
424             Supplier<Integer> preloadedLowBatteryLevel,
425             Supplier<Boolean> preloadedIsUntethered,
426             Supplier<Integer> preloadedNativeBatteryLevel) {
427         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
428         final String iconUri = preloadedIconUri.get();
429         final ImageView imageView = linearLayout.findViewById(R.id.header_icon);
430         if (iconUri != null) {
431             updateIcon(imageView, iconUri);
432         } else {
433             final Pair<Drawable, String> pair =
434                     BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, mCachedDevice);
435             imageView.setImageDrawable(pair.first);
436             imageView.setContentDescription(pair.second);
437         }
438         final int batteryLevel = preloadedBatteryLevel.get();
439         final boolean charging = preloadedCharging.get();
440         int lowBatteryLevel = preloadedLowBatteryLevel.get();
441 
442         Log.d(TAG, "buletoothDevice: " + bluetoothDevice.getAnonymizedAddress()
443                 + ", updateSubLayout() icon : " + iconMetaKey + ", battery : " + batteryMetaKey
444                 + ", charge : " + chargeMetaKey + ", batteryLevel : " + batteryLevel
445                 + ", charging : " + charging + ", iconUri : " + iconUri
446                 + ", lowBatteryLevel : " + lowBatteryLevel);
447 
448         if (deviceId == LEFT_DEVICE_ID || deviceId == RIGHT_DEVICE_ID) {
449             showBatteryPredictionIfNecessary(linearLayout, deviceId, batteryLevel);
450         }
451         final TextView batterySummaryView = linearLayout.findViewById(R.id.bt_battery_summary);
452         if (preloadedIsUntethered.get()) {
453             if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
454                 linearLayout.setVisibility(View.VISIBLE);
455                 batterySummaryView.setText(
456                         com.android.settings.Utils.formatPercentage(batteryLevel));
457                 batterySummaryView.setVisibility(View.VISIBLE);
458                 showBatteryIcon(linearLayout, batteryLevel, lowBatteryLevel, charging);
459             } else {
460                 if (deviceId == MAIN_DEVICE_ID) {
461                     linearLayout.setVisibility(View.VISIBLE);
462                     linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE);
463                     int level = preloadedNativeBatteryLevel.get();
464                     if (level != BluetoothDevice.BATTERY_LEVEL_UNKNOWN
465                             && level != BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) {
466                         batterySummaryView.setText(
467                                 com.android.settings.Utils.formatPercentage(level));
468                         batterySummaryView.setVisibility(View.VISIBLE);
469                     } else {
470                         batterySummaryView.setVisibility(View.GONE);
471                     }
472                 } else {
473                     // Hide it if it doesn't have battery information
474                     linearLayout.setVisibility(View.GONE);
475                 }
476             }
477         } else {
478             if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
479                 linearLayout.setVisibility(View.VISIBLE);
480                 batterySummaryView.setText(
481                         com.android.settings.Utils.formatPercentage(batteryLevel));
482                 batterySummaryView.setVisibility(View.VISIBLE);
483                 showBatteryIcon(linearLayout, batteryLevel, lowBatteryLevel, charging);
484             } else {
485                 batterySummaryView.setVisibility(View.GONE);
486             }
487         }
488         final TextView textView = linearLayout.findViewById(R.id.header_title);
489         if (deviceId == MAIN_DEVICE_ID) {
490             textView.setVisibility(View.GONE);
491         } else {
492             textView.setText(titleResId);
493             textView.setVisibility(View.VISIBLE);
494         }
495     }
496 
isUntetheredHeadset(BluetoothDevice bluetoothDevice)497     private boolean isUntetheredHeadset(BluetoothDevice bluetoothDevice) {
498         return BluetoothUtils.getBooleanMetaData(bluetoothDevice,
499                 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)
500                 || TextUtils.equals(BluetoothUtils.getStringMetaData(bluetoothDevice,
501                 BluetoothDevice.METADATA_DEVICE_TYPE),
502                 BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET);
503     }
504 
showBatteryPredictionIfNecessary(LinearLayout linearLayout, int batteryId, int batteryLevel)505     private void showBatteryPredictionIfNecessary(LinearLayout linearLayout, int batteryId,
506             int batteryLevel) {
507         ThreadUtils.postOnBackgroundThread(() -> {
508             final Uri contentUri = new Uri.Builder()
509                     .scheme(ContentResolver.SCHEME_CONTENT)
510                     .authority(mContext.getString(R.string.config_battery_prediction_authority))
511                     .appendPath(PATH)
512                     .appendPath(DATABASE_ID)
513                     .appendPath(DATABASE_BLUETOOTH)
514                     .appendQueryParameter(QUERY_PARAMETER_ADDRESS, mCachedDevice.getAddress())
515                     .appendQueryParameter(QUERY_PARAMETER_BATTERY_ID, String.valueOf(batteryId))
516                     .appendQueryParameter(QUERY_PARAMETER_BATTERY_LEVEL,
517                             String.valueOf(batteryLevel))
518                     .appendQueryParameter(QUERY_PARAMETER_TIMESTAMP,
519                             String.valueOf(System.currentTimeMillis()))
520                     .build();
521 
522             final String[] columns = new String[] {BATTERY_ESTIMATE, ESTIMATE_READY};
523             final Cursor cursor =
524                     mContext.getContentResolver().query(contentUri, columns, null, null, null);
525             if (cursor == null) {
526                 Log.w(TAG, "showBatteryPredictionIfNecessary() cursor is null!");
527                 return;
528             }
529             try {
530                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
531                     final int estimateReady =
532                             cursor.getInt(cursor.getColumnIndex(ESTIMATE_READY));
533                     final long batteryEstimate =
534                             cursor.getLong(cursor.getColumnIndex(BATTERY_ESTIMATE));
535                     if (DEBUG) {
536                         Log.d(TAG, "showBatteryTimeIfNecessary() batteryId : " + batteryId
537                                 + ", ESTIMATE_READY : " + estimateReady
538                                 + ", BATTERY_ESTIMATE : " + batteryEstimate);
539                     }
540 
541                     showBatteryPredictionIfNecessary(estimateReady, batteryEstimate, linearLayout);
542                     if (batteryId == LEFT_DEVICE_ID) {
543                         mIsLeftDeviceEstimateReady = estimateReady == 1;
544                     } else if (batteryId == RIGHT_DEVICE_ID) {
545                         mIsRightDeviceEstimateReady = estimateReady == 1;
546                     }
547                 }
548             } finally {
549                 cursor.close();
550             }
551         });
552     }
553 
554     @VisibleForTesting
showBatteryPredictionIfNecessary(int estimateReady, long batteryEstimate, LinearLayout linearLayout)555     void showBatteryPredictionIfNecessary(int estimateReady, long batteryEstimate,
556             LinearLayout linearLayout) {
557         ThreadUtils.postOnMainThread(() -> {
558             final TextView textView = linearLayout.findViewById(R.id.bt_battery_prediction);
559             if (estimateReady == 1) {
560                 textView.setText(
561                         StringUtil.formatElapsedTime(
562                                 mContext,
563                                 batteryEstimate,
564                                 /* withSeconds */ false,
565                                 /* collapseTimeUnit */  false));
566             } else {
567                 textView.setVisibility(View.GONE);
568             }
569         });
570     }
571 
572     @VisibleForTesting
showBothDevicesBatteryPredictionIfNecessary()573     void showBothDevicesBatteryPredictionIfNecessary() {
574         TextView leftDeviceTextView =
575                 mLayoutPreference.findViewById(R.id.layout_left)
576                         .findViewById(R.id.bt_battery_prediction);
577         TextView rightDeviceTextView =
578                 mLayoutPreference.findViewById(R.id.layout_right)
579                         .findViewById(R.id.bt_battery_prediction);
580 
581         boolean isBothDevicesEstimateReady =
582                 mIsLeftDeviceEstimateReady && mIsRightDeviceEstimateReady;
583         int visibility = isBothDevicesEstimateReady ? View.VISIBLE : View.GONE;
584         ThreadUtils.postOnMainThread(() -> {
585             leftDeviceTextView.setVisibility(visibility);
586             rightDeviceTextView.setVisibility(visibility);
587         });
588     }
589 
showBatteryIcon(LinearLayout linearLayout, int level, int lowBatteryLevel, boolean charging)590     private void showBatteryIcon(LinearLayout linearLayout, int level, int lowBatteryLevel,
591             boolean charging) {
592         final boolean enableLowBattery = level <= lowBatteryLevel && !charging;
593         final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon);
594         if (enableLowBattery) {
595             imageView.setImageDrawable(mContext.getDrawable(R.drawable.ic_battery_alert_24dp));
596             LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
597                     mContext.getResources().getDimensionPixelSize(
598                             R.dimen.advanced_bluetooth_battery_width),
599                     mContext.getResources().getDimensionPixelSize(
600                             R.dimen.advanced_bluetooth_battery_height));
601             layoutParams.rightMargin = mContext.getResources().getDimensionPixelSize(
602                     R.dimen.advanced_bluetooth_battery_right_margin);
603             imageView.setLayoutParams(layoutParams);
604         } else {
605             imageView.setImageDrawable(createBtBatteryIcon(mContext, level, charging));
606             LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
607                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
608             imageView.setLayoutParams(layoutParams);
609         }
610         imageView.setVisibility(View.VISIBLE);
611     }
612 
updateDisconnectLayout()613     private void updateDisconnectLayout() {
614         mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE);
615         mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE);
616 
617         // Hide title, battery icon and battery summary
618         final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle);
619         linearLayout.setVisibility(View.VISIBLE);
620         linearLayout.findViewById(R.id.header_title).setVisibility(View.GONE);
621         linearLayout.findViewById(R.id.bt_battery_summary).setVisibility(View.GONE);
622         linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE);
623 
624         // Only show bluetooth icon
625         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
626         final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice,
627                 BluetoothDevice.METADATA_MAIN_ICON);
628         if (DEBUG) {
629             Log.d(TAG, "updateDisconnectLayout() iconUri : " + iconUri);
630         }
631         if (iconUri != null) {
632             final ImageView imageView = linearLayout.findViewById(R.id.header_icon);
633             updateIcon(imageView, iconUri);
634         }
635     }
636 
637     /**
638      * Update icon by {@code iconUri}. If icon exists in cache, use it; otherwise extract it
639      * from uri in background thread and update it in main thread.
640      */
641     @VisibleForTesting
updateIcon(ImageView imageView, String iconUri)642     void updateIcon(ImageView imageView, String iconUri) {
643         if (mIconCache.containsKey(iconUri)) {
644             imageView.setAlpha(1f);
645             imageView.setImageBitmap(mIconCache.get(iconUri));
646             return;
647         }
648 
649         imageView.setAlpha(HALF_ALPHA);
650         ThreadUtils.postOnBackgroundThread(() -> {
651             final Uri uri = Uri.parse(iconUri);
652             try {
653                 mContext.getContentResolver().takePersistableUriPermission(uri,
654                         Intent.FLAG_GRANT_READ_URI_PERMISSION);
655 
656                 final Bitmap bitmap = MediaStore.Images.Media.getBitmap(
657                         mContext.getContentResolver(), uri);
658                 ThreadUtils.postOnMainThread(() -> {
659                     mIconCache.put(iconUri, bitmap);
660                     imageView.setAlpha(1f);
661                     imageView.setImageBitmap(bitmap);
662                 });
663             } catch (IOException e) {
664                 Log.e(TAG, "Failed to get bitmap for: " + iconUri, e);
665             } catch (SecurityException e) {
666                 Log.e(TAG, "Failed to take persistable permission for: " + uri, e);
667             }
668         });
669     }
670 
671     @Override
onDeviceAttributesChanged()672     public void onDeviceAttributesChanged() {
673         if (mCachedDevice != null) {
674             refresh();
675         }
676     }
677 }
678