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