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