/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.bluetooth; import static com.android.settings.bluetooth.Utils.preloadAndRun; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.fuelgauge.BatteryMeterView; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnDestroy; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; import com.android.settingslib.utils.StringUtil; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.LayoutPreference; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * This class adds a header with device name and status (connected/disconnected, etc.). */ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceController implements LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback { private static final String TAG = "AdvancedBtHeaderCtrl"; private static final int LOW_BATTERY_LEVEL = 15; private static final int CASE_LOW_BATTERY_LEVEL = 19; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final String PATH = "time_remaining"; private static final String QUERY_PARAMETER_ADDRESS = "address"; private static final String QUERY_PARAMETER_BATTERY_ID = "battery_id"; private static final String QUERY_PARAMETER_BATTERY_LEVEL = "battery_level"; private static final String QUERY_PARAMETER_TIMESTAMP = "timestamp"; private static final String BATTERY_ESTIMATE = "battery_estimate"; private static final String ESTIMATE_READY = "estimate_ready"; private static final String DATABASE_ID = "id"; private static final String DATABASE_BLUETOOTH = "Bluetooth"; private static final long TIME_OF_HOUR = TimeUnit.SECONDS.toMillis(3600); private static final long TIME_OF_MINUTE = TimeUnit.SECONDS.toMillis(60); private static final int LEFT_DEVICE_ID = 1; private static final int RIGHT_DEVICE_ID = 2; private static final int CASE_DEVICE_ID = 3; private static final int MAIN_DEVICE_ID = 4; private static final float HALF_ALPHA = 0.5f; @VisibleForTesting LayoutPreference mLayoutPreference; @VisibleForTesting final Map<String, Bitmap> mIconCache; private CachedBluetoothDevice mCachedDevice; private Set<BluetoothDevice> mBluetoothDevices; @VisibleForTesting BluetoothAdapter mBluetoothAdapter; @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper()); @VisibleForTesting boolean mIsLeftDeviceEstimateReady; @VisibleForTesting boolean mIsRightDeviceEstimateReady; @VisibleForTesting final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = new BluetoothAdapter.OnMetadataChangedListener() { @Override public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) { Log.d(TAG, String.format("Metadata updated in Device %s: %d = %s.", device.getAnonymizedAddress(), key, value == null ? null : new String(value))); refresh(); } }; public AdvancedBluetoothDetailsHeaderController(Context context, String prefKey) { super(context, prefKey); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mIconCache = new HashMap<>(); } @Override public int getAvailabilityStatus() { if (mCachedDevice == null) { return CONDITIONALLY_UNAVAILABLE; } return BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice()) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mLayoutPreference = screen.findPreference(getPreferenceKey()); mLayoutPreference.setVisible(isAvailable()); } @Override public void onStart() { if (!isAvailable()) { return; } registerBluetoothDevice(); refresh(); } @Override public void onStop() { unRegisterBluetoothDevice(); } @Override public void onDestroy() { // Destroy icon bitmap associated with this header for (Bitmap bitmap : mIconCache.values()) { if (bitmap != null) { bitmap.recycle(); } } mIconCache.clear(); } public void init(CachedBluetoothDevice cachedBluetoothDevice) { mCachedDevice = cachedBluetoothDevice; } private void registerBluetoothDevice() { if (mBluetoothAdapter == null) { Log.d(TAG, "No mBluetoothAdapter"); return; } if (mBluetoothDevices == null) { mBluetoothDevices = new HashSet<>(); } mBluetoothDevices.clear(); if (mCachedDevice.getDevice() != null) { mBluetoothDevices.add(mCachedDevice.getDevice()); } mCachedDevice.getMemberDevice().forEach(cbd -> { if (cbd != null) { mBluetoothDevices.add(cbd.getDevice()); } }); if (mBluetoothDevices.isEmpty()) { Log.d(TAG, "No BT device to register."); return; } mCachedDevice.registerCallback(this); Set<BluetoothDevice> errorDevices = new HashSet<>(); mBluetoothDevices.forEach(bd -> { try { boolean isSuccess = mBluetoothAdapter.addOnMetadataChangedListener(bd, mContext.getMainExecutor(), mMetadataListener); if (!isSuccess) { Log.e(TAG, bd.getAnonymizedAddress() + ": add into Listener failed"); errorDevices.add(bd); } } catch (NullPointerException e) { errorDevices.add(bd); Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString()); } catch (IllegalArgumentException e) { errorDevices.add(bd); Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString()); } }); for (BluetoothDevice errorDevice : errorDevices) { mBluetoothDevices.remove(errorDevice); Log.d(TAG, "mBluetoothDevices remove " + errorDevice.getAnonymizedAddress()); } } private void unRegisterBluetoothDevice() { if (mBluetoothAdapter == null) { Log.d(TAG, "No mBluetoothAdapter"); return; } if (mBluetoothDevices == null || mBluetoothDevices.isEmpty()) { Log.d(TAG, "No BT device to unregister."); return; } mCachedDevice.unregisterCallback(this); mBluetoothDevices.forEach(bd -> { try { mBluetoothAdapter.removeOnMetadataChangedListener(bd, mMetadataListener); } catch (NullPointerException e) { Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString()); } catch (IllegalArgumentException e) { Log.e(TAG, bd.getAnonymizedAddress() + ":" + e.toString()); } }); mBluetoothDevices.clear(); } @VisibleForTesting void refresh() { if (mLayoutPreference != null && mCachedDevice != null) { Supplier<String> deviceName = Suppliers.memoize(() -> mCachedDevice.getName()); Supplier<Boolean> disconnected = Suppliers.memoize(() -> !mCachedDevice.isConnected() || mCachedDevice.isBusy()); Supplier<Boolean> isUntetheredHeadset = Suppliers.memoize(() -> isUntetheredHeadset(mCachedDevice.getDevice())); Supplier<String> summaryText = Suppliers.memoize( () -> { if (disconnected.get() || isUntetheredHeadset.get()) { return mCachedDevice.getConnectionSummary( /* shortSummary= */ true); } return mCachedDevice.getConnectionSummary( BluetoothUtils.getIntMetaData( mCachedDevice.getDevice(), BluetoothDevice.METADATA_MAIN_BATTERY) != BluetoothUtils.META_INT_ERROR); }); preloadAndRun( List.of(deviceName, disconnected, isUntetheredHeadset, summaryText), () -> { final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title); title.setText(deviceName.get()); final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); if (disconnected.get()) { summary.setText(summaryText.get()); updateDisconnectLayout(); return; } if (isUntetheredHeadset.get()) { summary.setText(summaryText.get()); updateSubLayout( mLayoutPreference.findViewById(R.id.layout_left), BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY, BluetoothDevice.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD, BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING, R.string.bluetooth_left_name, LEFT_DEVICE_ID); updateSubLayout( mLayoutPreference.findViewById(R.id.layout_middle), BluetoothDevice.METADATA_UNTETHERED_CASE_ICON, BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY, BluetoothDevice.METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD, BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING, R.string.bluetooth_middle_name, CASE_DEVICE_ID); updateSubLayout( mLayoutPreference.findViewById(R.id.layout_right), BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY, BluetoothDevice.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD, BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING, R.string.bluetooth_right_name, RIGHT_DEVICE_ID); showBothDevicesBatteryPredictionIfNecessary(); } else { mLayoutPreference .findViewById(R.id.layout_left) .setVisibility(View.GONE); mLayoutPreference .findViewById(R.id.layout_right) .setVisibility(View.GONE); summary.setText(summaryText.get()); updateSubLayout( mLayoutPreference.findViewById(R.id.layout_middle), BluetoothDevice.METADATA_MAIN_ICON, BluetoothDevice.METADATA_MAIN_BATTERY, BluetoothDevice.METADATA_MAIN_LOW_BATTERY_THRESHOLD, BluetoothDevice.METADATA_MAIN_CHARGING, /* titleResId= */ 0, MAIN_DEVICE_ID); } }); } } @VisibleForTesting Drawable createBtBatteryIcon(Context context, int level, boolean charging) { final BatteryMeterView.BatteryMeterDrawable drawable = new BatteryMeterView.BatteryMeterDrawable(context, context.getColor(com.android.settingslib.R.color.meter_background_color), context.getResources().getDimensionPixelSize( R.dimen.advanced_bluetooth_battery_meter_width), context.getResources().getDimensionPixelSize( R.dimen.advanced_bluetooth_battery_meter_height)); drawable.setBatteryLevel(level); drawable.setColorFilter(new PorterDuffColorFilter( com.android.settings.Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlNormal), PorterDuff.Mode.SRC)); drawable.setCharging(charging); return drawable; } private void updateSubLayout( LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, int lowBatteryMetaKey, int chargeMetaKey, int titleResId, int deviceId) { if (linearLayout == null) { return; } BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); Supplier<String> iconUri = Suppliers.memoize( () -> BluetoothUtils.getStringMetaData(bluetoothDevice, iconMetaKey)); Supplier<Integer> batteryLevel = Suppliers.memoize( () -> BluetoothUtils.getIntMetaData(bluetoothDevice, batteryMetaKey)); Supplier<Boolean> charging = Suppliers.memoize( () -> BluetoothUtils.getBooleanMetaData(bluetoothDevice, chargeMetaKey)); Supplier<Integer> lowBatteryLevel = Suppliers.memoize( () -> { int level = BluetoothUtils.getIntMetaData( bluetoothDevice, lowBatteryMetaKey); if (level == BluetoothUtils.META_INT_ERROR) { if (batteryMetaKey == BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY) { level = CASE_LOW_BATTERY_LEVEL; } else { level = LOW_BATTERY_LEVEL; } } return level; }); Supplier<Boolean> isUntethered = Suppliers.memoize(() -> isUntetheredHeadset(bluetoothDevice)); Supplier<Integer> nativeBatteryLevel = Suppliers.memoize(bluetoothDevice::getBatteryLevel); preloadAndRun( List.of( iconUri, batteryLevel, charging, lowBatteryLevel, isUntethered, nativeBatteryLevel), () -> updateSubLayoutUi( linearLayout, iconMetaKey, batteryMetaKey, lowBatteryMetaKey, chargeMetaKey, titleResId, deviceId, iconUri, batteryLevel, charging, lowBatteryLevel, isUntethered, nativeBatteryLevel)); } private void 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) { final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); final String iconUri = preloadedIconUri.get(); final ImageView imageView = linearLayout.findViewById(R.id.header_icon); if (iconUri != null) { updateIcon(imageView, iconUri); } else { final Pair<Drawable, String> pair = BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, mCachedDevice); imageView.setImageDrawable(pair.first); imageView.setContentDescription(pair.second); } final int batteryLevel = preloadedBatteryLevel.get(); final boolean charging = preloadedCharging.get(); int lowBatteryLevel = preloadedLowBatteryLevel.get(); Log.d(TAG, "buletoothDevice: " + bluetoothDevice.getAnonymizedAddress() + ", updateSubLayout() icon : " + iconMetaKey + ", battery : " + batteryMetaKey + ", charge : " + chargeMetaKey + ", batteryLevel : " + batteryLevel + ", charging : " + charging + ", iconUri : " + iconUri + ", lowBatteryLevel : " + lowBatteryLevel); if (deviceId == LEFT_DEVICE_ID || deviceId == RIGHT_DEVICE_ID) { showBatteryPredictionIfNecessary(linearLayout, deviceId, batteryLevel); } final TextView batterySummaryView = linearLayout.findViewById(R.id.bt_battery_summary); if (preloadedIsUntethered.get()) { if (batteryLevel != BluetoothUtils.META_INT_ERROR) { linearLayout.setVisibility(View.VISIBLE); batterySummaryView.setText( com.android.settings.Utils.formatPercentage(batteryLevel)); batterySummaryView.setVisibility(View.VISIBLE); showBatteryIcon(linearLayout, batteryLevel, lowBatteryLevel, charging); } else { if (deviceId == MAIN_DEVICE_ID) { linearLayout.setVisibility(View.VISIBLE); linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE); int level = preloadedNativeBatteryLevel.get(); if (level != BluetoothDevice.BATTERY_LEVEL_UNKNOWN && level != BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) { batterySummaryView.setText( com.android.settings.Utils.formatPercentage(level)); batterySummaryView.setVisibility(View.VISIBLE); } else { batterySummaryView.setVisibility(View.GONE); } } else { // Hide it if it doesn't have battery information linearLayout.setVisibility(View.GONE); } } } else { if (batteryLevel != BluetoothUtils.META_INT_ERROR) { linearLayout.setVisibility(View.VISIBLE); batterySummaryView.setText( com.android.settings.Utils.formatPercentage(batteryLevel)); batterySummaryView.setVisibility(View.VISIBLE); showBatteryIcon(linearLayout, batteryLevel, lowBatteryLevel, charging); } else { batterySummaryView.setVisibility(View.GONE); } } final TextView textView = linearLayout.findViewById(R.id.header_title); if (deviceId == MAIN_DEVICE_ID) { textView.setVisibility(View.GONE); } else { textView.setText(titleResId); textView.setVisibility(View.VISIBLE); } } private boolean isUntetheredHeadset(BluetoothDevice bluetoothDevice) { return BluetoothUtils.getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET) || TextUtils.equals(BluetoothUtils.getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE), BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET); } private void showBatteryPredictionIfNecessary(LinearLayout linearLayout, int batteryId, int batteryLevel) { ThreadUtils.postOnBackgroundThread(() -> { final Uri contentUri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(mContext.getString(R.string.config_battery_prediction_authority)) .appendPath(PATH) .appendPath(DATABASE_ID) .appendPath(DATABASE_BLUETOOTH) .appendQueryParameter(QUERY_PARAMETER_ADDRESS, mCachedDevice.getAddress()) .appendQueryParameter(QUERY_PARAMETER_BATTERY_ID, String.valueOf(batteryId)) .appendQueryParameter(QUERY_PARAMETER_BATTERY_LEVEL, String.valueOf(batteryLevel)) .appendQueryParameter(QUERY_PARAMETER_TIMESTAMP, String.valueOf(System.currentTimeMillis())) .build(); final String[] columns = new String[] {BATTERY_ESTIMATE, ESTIMATE_READY}; final Cursor cursor = mContext.getContentResolver().query(contentUri, columns, null, null, null); if (cursor == null) { Log.w(TAG, "showBatteryPredictionIfNecessary() cursor is null!"); return; } try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { final int estimateReady = cursor.getInt(cursor.getColumnIndex(ESTIMATE_READY)); final long batteryEstimate = cursor.getLong(cursor.getColumnIndex(BATTERY_ESTIMATE)); if (DEBUG) { Log.d(TAG, "showBatteryTimeIfNecessary() batteryId : " + batteryId + ", ESTIMATE_READY : " + estimateReady + ", BATTERY_ESTIMATE : " + batteryEstimate); } showBatteryPredictionIfNecessary(estimateReady, batteryEstimate, linearLayout); if (batteryId == LEFT_DEVICE_ID) { mIsLeftDeviceEstimateReady = estimateReady == 1; } else if (batteryId == RIGHT_DEVICE_ID) { mIsRightDeviceEstimateReady = estimateReady == 1; } } } finally { cursor.close(); } }); } @VisibleForTesting void showBatteryPredictionIfNecessary(int estimateReady, long batteryEstimate, LinearLayout linearLayout) { ThreadUtils.postOnMainThread(() -> { final TextView textView = linearLayout.findViewById(R.id.bt_battery_prediction); if (estimateReady == 1) { textView.setText( StringUtil.formatElapsedTime( mContext, batteryEstimate, /* withSeconds */ false, /* collapseTimeUnit */ false)); } else { textView.setVisibility(View.GONE); } }); } @VisibleForTesting void showBothDevicesBatteryPredictionIfNecessary() { TextView leftDeviceTextView = mLayoutPreference.findViewById(R.id.layout_left) .findViewById(R.id.bt_battery_prediction); TextView rightDeviceTextView = mLayoutPreference.findViewById(R.id.layout_right) .findViewById(R.id.bt_battery_prediction); boolean isBothDevicesEstimateReady = mIsLeftDeviceEstimateReady && mIsRightDeviceEstimateReady; int visibility = isBothDevicesEstimateReady ? View.VISIBLE : View.GONE; ThreadUtils.postOnMainThread(() -> { leftDeviceTextView.setVisibility(visibility); rightDeviceTextView.setVisibility(visibility); }); } private void showBatteryIcon(LinearLayout linearLayout, int level, int lowBatteryLevel, boolean charging) { final boolean enableLowBattery = level <= lowBatteryLevel && !charging; final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon); if (enableLowBattery) { imageView.setImageDrawable(mContext.getDrawable(R.drawable.ic_battery_alert_24dp)); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( mContext.getResources().getDimensionPixelSize( R.dimen.advanced_bluetooth_battery_width), mContext.getResources().getDimensionPixelSize( R.dimen.advanced_bluetooth_battery_height)); layoutParams.rightMargin = mContext.getResources().getDimensionPixelSize( R.dimen.advanced_bluetooth_battery_right_margin); imageView.setLayoutParams(layoutParams); } else { imageView.setImageDrawable(createBtBatteryIcon(mContext, level, charging)); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); imageView.setLayoutParams(layoutParams); } imageView.setVisibility(View.VISIBLE); } private void updateDisconnectLayout() { mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE); mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE); // Hide title, battery icon and battery summary final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle); linearLayout.setVisibility(View.VISIBLE); linearLayout.findViewById(R.id.header_title).setVisibility(View.GONE); linearLayout.findViewById(R.id.bt_battery_summary).setVisibility(View.GONE); linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE); // Only show bluetooth icon final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON); if (DEBUG) { Log.d(TAG, "updateDisconnectLayout() iconUri : " + iconUri); } if (iconUri != null) { final ImageView imageView = linearLayout.findViewById(R.id.header_icon); updateIcon(imageView, iconUri); } } /** * Update icon by {@code iconUri}. If icon exists in cache, use it; otherwise extract it * from uri in background thread and update it in main thread. */ @VisibleForTesting void updateIcon(ImageView imageView, String iconUri) { if (mIconCache.containsKey(iconUri)) { imageView.setAlpha(1f); imageView.setImageBitmap(mIconCache.get(iconUri)); return; } imageView.setAlpha(HALF_ALPHA); ThreadUtils.postOnBackgroundThread(() -> { final Uri uri = Uri.parse(iconUri); try { mContext.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); final Bitmap bitmap = MediaStore.Images.Media.getBitmap( mContext.getContentResolver(), uri); ThreadUtils.postOnMainThread(() -> { mIconCache.put(iconUri, bitmap); imageView.setAlpha(1f); imageView.setImageBitmap(bitmap); }); } catch (IOException e) { Log.e(TAG, "Failed to get bitmap for: " + iconUri, e); } catch (SecurityException e) { Log.e(TAG, "Failed to take persistable permission for: " + uri, e); } }); } @Override public void onDeviceAttributesChanged() { if (mCachedDevice != null) { refresh(); } } }