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