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.Context; 22 import android.graphics.Bitmap; 23 import android.graphics.PorterDuff; 24 import android.graphics.PorterDuffColorFilter; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.provider.DeviceConfig; 30 import android.provider.MediaStore; 31 import android.util.Log; 32 import android.view.View; 33 import android.widget.ImageView; 34 import android.widget.LinearLayout; 35 import android.widget.TextView; 36 37 import androidx.annotation.VisibleForTesting; 38 import androidx.preference.PreferenceScreen; 39 40 import com.android.settings.R; 41 import com.android.settings.core.BasePreferenceController; 42 import com.android.settings.core.SettingsUIDeviceConfig; 43 import com.android.settings.fuelgauge.BatteryMeterView; 44 import com.android.settingslib.bluetooth.BluetoothUtils; 45 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 46 import com.android.settingslib.core.lifecycle.LifecycleObserver; 47 import com.android.settingslib.core.lifecycle.events.OnDestroy; 48 import com.android.settingslib.core.lifecycle.events.OnStart; 49 import com.android.settingslib.core.lifecycle.events.OnStop; 50 import com.android.settingslib.utils.ThreadUtils; 51 import com.android.settingslib.widget.LayoutPreference; 52 53 import java.io.IOException; 54 import java.util.HashMap; 55 import java.util.Map; 56 57 /** 58 * This class adds a header with device name and status (connected/disconnected, etc.). 59 */ 60 public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceController implements 61 LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback { 62 private static final String TAG = "AdvancedBtHeaderCtrl"; 63 64 @VisibleForTesting 65 LayoutPreference mLayoutPreference; 66 @VisibleForTesting 67 final Map<String, Bitmap> mIconCache; 68 private CachedBluetoothDevice mCachedDevice; 69 @VisibleForTesting 70 BluetoothAdapter mBluetoothAdapter; 71 @VisibleForTesting 72 Handler mHandler = new Handler(Looper.getMainLooper()); 73 @VisibleForTesting 74 final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = 75 new BluetoothAdapter.OnMetadataChangedListener() { 76 @Override 77 public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) { 78 Log.i(TAG, String.format("Metadata updated in Device %s: %d = %s.", device, key, 79 value == null ? null : new String(value))); 80 refresh(); 81 } 82 }; 83 AdvancedBluetoothDetailsHeaderController(Context context, String prefKey)84 public AdvancedBluetoothDetailsHeaderController(Context context, String prefKey) { 85 super(context, prefKey); 86 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 87 mIconCache = new HashMap<>(); 88 } 89 90 @Override getAvailabilityStatus()91 public int getAvailabilityStatus() { 92 final boolean advancedEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, 93 SettingsUIDeviceConfig.BT_ADVANCED_HEADER_ENABLED, true); 94 final boolean untetheredHeadset = BluetoothUtils.getBooleanMetaData( 95 mCachedDevice.getDevice(), BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET); 96 return advancedEnabled && untetheredHeadset ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; 97 } 98 99 @Override displayPreference(PreferenceScreen screen)100 public void displayPreference(PreferenceScreen screen) { 101 super.displayPreference(screen); 102 mLayoutPreference = screen.findPreference(getPreferenceKey()); 103 mLayoutPreference.setVisible(isAvailable()); 104 105 refresh(); 106 } 107 108 @Override onStart()109 public void onStart() { 110 if (!isAvailable()) { 111 return; 112 } 113 mCachedDevice.registerCallback(this::onDeviceAttributesChanged); 114 mBluetoothAdapter.addOnMetadataChangedListener(mCachedDevice.getDevice(), 115 mContext.getMainExecutor(), mMetadataListener); 116 } 117 118 @Override onStop()119 public void onStop() { 120 if (!isAvailable()) { 121 return; 122 } 123 mCachedDevice.unregisterCallback(this::onDeviceAttributesChanged); 124 mBluetoothAdapter.removeOnMetadataChangedListener(mCachedDevice.getDevice(), 125 mMetadataListener); 126 } 127 128 @Override onDestroy()129 public void onDestroy() { 130 if (!isAvailable()) { 131 return; 132 } 133 // Destroy icon bitmap associated with this header 134 for (Bitmap bitmap : mIconCache.values()) { 135 if (bitmap != null) { 136 bitmap.recycle(); 137 } 138 } 139 mIconCache.clear(); 140 } 141 init(CachedBluetoothDevice cachedBluetoothDevice)142 public void init(CachedBluetoothDevice cachedBluetoothDevice) { 143 mCachedDevice = cachedBluetoothDevice; 144 } 145 146 @VisibleForTesting refresh()147 void refresh() { 148 if (mLayoutPreference != null && mCachedDevice != null) { 149 final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title); 150 title.setText(mCachedDevice.getName()); 151 final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); 152 summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */)); 153 154 if (!mCachedDevice.isConnected()) { 155 updateDisconnectLayout(); 156 return; 157 } 158 159 updateSubLayout(mLayoutPreference.findViewById(R.id.layout_left), 160 BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON, 161 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY, 162 BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING, 163 R.string.bluetooth_left_name); 164 165 updateSubLayout(mLayoutPreference.findViewById(R.id.layout_middle), 166 BluetoothDevice.METADATA_UNTETHERED_CASE_ICON, 167 BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY, 168 BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING, 169 R.string.bluetooth_middle_name); 170 171 updateSubLayout(mLayoutPreference.findViewById(R.id.layout_right), 172 BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON, 173 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY, 174 BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING, 175 R.string.bluetooth_right_name); 176 } 177 } 178 179 @VisibleForTesting createBtBatteryIcon(Context context, int level, boolean charging)180 Drawable createBtBatteryIcon(Context context, int level, boolean charging) { 181 final BatteryMeterView.BatteryMeterDrawable drawable = 182 new BatteryMeterView.BatteryMeterDrawable(context, 183 context.getColor(R.color.meter_background_color)); 184 drawable.setBatteryLevel(level); 185 drawable.setColorFilter(new PorterDuffColorFilter( 186 com.android.settings.Utils.getColorAttrDefaultColor(context, 187 android.R.attr.colorControlNormal), 188 PorterDuff.Mode.SRC_IN)); 189 drawable.setCharging(charging); 190 191 return drawable; 192 } 193 updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, int chargeMetaKey, int titleResId)194 private void updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, 195 int chargeMetaKey, int titleResId) { 196 if (linearLayout == null) { 197 return; 198 } 199 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 200 final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice, iconMetaKey); 201 if (iconUri != null) { 202 final ImageView imageView = linearLayout.findViewById(R.id.header_icon); 203 updateIcon(imageView, iconUri); 204 } 205 206 final int batteryLevel = BluetoothUtils.getIntMetaData(bluetoothDevice, batteryMetaKey); 207 final boolean charging = BluetoothUtils.getBooleanMetaData(bluetoothDevice, chargeMetaKey); 208 if (batteryLevel != BluetoothUtils.META_INT_ERROR) { 209 linearLayout.setVisibility(View.VISIBLE); 210 final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon); 211 imageView.setImageDrawable(createBtBatteryIcon(mContext, batteryLevel, charging)); 212 imageView.setVisibility(View.VISIBLE); 213 final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary); 214 textView.setText(com.android.settings.Utils.formatPercentage(batteryLevel)); 215 textView.setVisibility(View.VISIBLE); 216 } else { 217 // Hide it if it doesn't have battery information 218 linearLayout.setVisibility(View.GONE); 219 } 220 221 final TextView textView = linearLayout.findViewById(R.id.header_title); 222 textView.setText(titleResId); 223 textView.setVisibility(View.VISIBLE); 224 } 225 updateDisconnectLayout()226 private void updateDisconnectLayout() { 227 mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE); 228 mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE); 229 230 // Hide title, battery icon and battery summary 231 final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle); 232 linearLayout.setVisibility(View.VISIBLE); 233 linearLayout.findViewById(R.id.header_title).setVisibility(View.GONE); 234 linearLayout.findViewById(R.id.bt_battery_summary).setVisibility(View.GONE); 235 linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE); 236 237 // Only show bluetooth icon 238 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 239 final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice, 240 BluetoothDevice.METADATA_MAIN_ICON); 241 if (iconUri != null) { 242 final ImageView imageView = linearLayout.findViewById(R.id.header_icon); 243 updateIcon(imageView, iconUri); 244 } 245 } 246 247 /** 248 * Update icon by {@code iconUri}. If icon exists in cache, use it; otherwise extract it 249 * from uri in background thread and update it in main thread. 250 */ 251 @VisibleForTesting updateIcon(ImageView imageView, String iconUri)252 void updateIcon(ImageView imageView, String iconUri) { 253 if (mIconCache.containsKey(iconUri)) { 254 imageView.setImageBitmap(mIconCache.get(iconUri)); 255 return; 256 } 257 258 ThreadUtils.postOnBackgroundThread(() -> { 259 try { 260 final Bitmap bitmap = MediaStore.Images.Media.getBitmap( 261 mContext.getContentResolver(), Uri.parse(iconUri)); 262 ThreadUtils.postOnMainThread(() -> { 263 mIconCache.put(iconUri, bitmap); 264 imageView.setImageBitmap(bitmap); 265 }); 266 } catch (IOException e) { 267 Log.e(TAG, "Failed to get bitmap for: " + iconUri); 268 } 269 }); 270 } 271 272 @Override onDeviceAttributesChanged()273 public void onDeviceAttributesChanged() { 274 if (mCachedDevice != null) { 275 refresh(); 276 } 277 } 278 } 279