1 /* 2 * Copyright (C) 2022 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.BluetoothLeAudio; 20 import android.bluetooth.BluetoothProfile; 21 import android.content.Context; 22 import android.graphics.PorterDuff; 23 import android.graphics.PorterDuffColorFilter; 24 import android.graphics.drawable.Drawable; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.util.Log; 28 import android.util.Pair; 29 import android.view.View; 30 import android.widget.ImageView; 31 import android.widget.TextView; 32 33 import androidx.annotation.VisibleForTesting; 34 import androidx.preference.PreferenceScreen; 35 36 import com.android.settings.R; 37 import com.android.settings.core.BasePreferenceController; 38 import com.android.settings.fuelgauge.BatteryMeterView; 39 import com.android.settingslib.bluetooth.BluetoothUtils; 40 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 41 import com.android.settingslib.bluetooth.LeAudioProfile; 42 import com.android.settingslib.bluetooth.LocalBluetoothManager; 43 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 44 import com.android.settingslib.core.lifecycle.LifecycleObserver; 45 import com.android.settingslib.core.lifecycle.events.OnDestroy; 46 import com.android.settingslib.core.lifecycle.events.OnStart; 47 import com.android.settingslib.core.lifecycle.events.OnStop; 48 import com.android.settingslib.widget.LayoutPreference; 49 50 import java.util.List; 51 52 /** 53 * This class adds a header with device name and status (connected/disconnected, etc.). 54 */ 55 public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceController implements 56 LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback { 57 private static final String TAG = "LeAudioBtHeaderCtrl"; 58 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 59 60 @VisibleForTesting 61 static final int LEFT_DEVICE_ID = 62 BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT 63 | BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT 64 | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER 65 | BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT 66 | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT 67 | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT 68 | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT 69 | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT 70 | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE 71 | BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND; 72 73 @VisibleForTesting 74 static final int RIGHT_DEVICE_ID = 75 BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT 76 | BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT 77 | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER 78 | BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT 79 | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT 80 | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT 81 | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT 82 | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT 83 | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE 84 | BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND; 85 86 @VisibleForTesting 87 static final int INVALID_RESOURCE_ID = -1; 88 89 @VisibleForTesting 90 LayoutPreference mLayoutPreference; 91 LocalBluetoothManager mManager; 92 private CachedBluetoothDevice mCachedDevice; 93 private List<CachedBluetoothDevice> mAllOfCachedDevices; 94 @VisibleForTesting 95 Handler mHandler = new Handler(Looper.getMainLooper()); 96 @VisibleForTesting 97 boolean mIsRegisterCallback = false; 98 99 private LocalBluetoothProfileManager mProfileManager; 100 LeAudioBluetoothDetailsHeaderController(Context context, String prefKey)101 public LeAudioBluetoothDetailsHeaderController(Context context, String prefKey) { 102 super(context, prefKey); 103 } 104 105 @Override getAvailabilityStatus()106 public int getAvailabilityStatus() { 107 if (mCachedDevice == null || mProfileManager == null) { 108 return CONDITIONALLY_UNAVAILABLE; 109 } 110 boolean hasLeAudio = mCachedDevice.getConnectableProfiles() 111 .stream() 112 .anyMatch(profile -> profile.getProfileId() == BluetoothProfile.LE_AUDIO); 113 114 return !BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice()) && hasLeAudio 115 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; 116 } 117 118 @Override displayPreference(PreferenceScreen screen)119 public void displayPreference(PreferenceScreen screen) { 120 super.displayPreference(screen); 121 mLayoutPreference = screen.findPreference(getPreferenceKey()); 122 mLayoutPreference.setVisible(isAvailable()); 123 } 124 125 @Override onStart()126 public void onStart() { 127 if (!isAvailable()) { 128 return; 129 } 130 mIsRegisterCallback = true; 131 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 132 item.registerCallback(this); 133 } 134 refresh(); 135 } 136 137 @Override onStop()138 public void onStop() { 139 if (!mIsRegisterCallback) { 140 return; 141 } 142 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 143 item.unregisterCallback(this); 144 } 145 146 mIsRegisterCallback = false; 147 } 148 149 @Override onDestroy()150 public void onDestroy() { 151 } 152 init(CachedBluetoothDevice cachedBluetoothDevice, LocalBluetoothManager bluetoothManager)153 public void init(CachedBluetoothDevice cachedBluetoothDevice, 154 LocalBluetoothManager bluetoothManager) { 155 mCachedDevice = cachedBluetoothDevice; 156 mManager = bluetoothManager; 157 mProfileManager = bluetoothManager.getProfileManager(); 158 mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice); 159 } 160 161 @VisibleForTesting refresh()162 void refresh() { 163 if (mLayoutPreference == null || mCachedDevice == null) { 164 return; 165 } 166 final ImageView imageView = mLayoutPreference.findViewById(R.id.entity_header_icon); 167 if (imageView != null) { 168 final Pair<Drawable, String> pair = 169 BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, mCachedDevice); 170 imageView.setImageDrawable(pair.first); 171 imageView.setContentDescription(pair.second); 172 } 173 174 final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title); 175 if (title != null) { 176 title.setText(mCachedDevice.getName()); 177 } 178 final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); 179 if (summary != null) { 180 summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */)); 181 } 182 183 if (!mCachedDevice.isConnected() || mCachedDevice.isBusy()) { 184 hideAllOfBatteryLayouts(); 185 return; 186 } 187 188 updateBatteryLayout(); 189 } 190 191 @VisibleForTesting createBtBatteryIcon(Context context, int level)192 Drawable createBtBatteryIcon(Context context, int level) { 193 final BatteryMeterView.BatteryMeterDrawable drawable = 194 new BatteryMeterView.BatteryMeterDrawable(context, 195 context.getColor(com.android.settingslib.R.color.meter_background_color), 196 context.getResources().getDimensionPixelSize( 197 R.dimen.advanced_bluetooth_battery_meter_width), 198 context.getResources().getDimensionPixelSize( 199 R.dimen.advanced_bluetooth_battery_meter_height)); 200 drawable.setBatteryLevel(level); 201 drawable.setColorFilter(new PorterDuffColorFilter( 202 com.android.settings.Utils.getColorAttrDefaultColor(context, 203 android.R.attr.colorControlNormal), 204 PorterDuff.Mode.SRC)); 205 return drawable; 206 } 207 getBatterySummaryResource(int containerId)208 private int getBatterySummaryResource(int containerId) { 209 if (containerId == R.id.bt_battery_case) { 210 return R.id.bt_battery_case_summary; 211 } else if (containerId == R.id.bt_battery_left) { 212 return R.id.bt_battery_left_summary; 213 } else if (containerId == R.id.bt_battery_right) { 214 return R.id.bt_battery_right_summary; 215 } 216 Log.d(TAG, "No summary resource id. The containerId is " + containerId); 217 return INVALID_RESOURCE_ID; 218 } 219 hideAllOfBatteryLayouts()220 private void hideAllOfBatteryLayouts() { 221 // hide the case 222 updateBatteryLayout(R.id.bt_battery_case, BluetoothUtils.META_INT_ERROR); 223 // hide the left 224 updateBatteryLayout(R.id.bt_battery_left, BluetoothUtils.META_INT_ERROR); 225 // hide the right 226 updateBatteryLayout(R.id.bt_battery_right, BluetoothUtils.META_INT_ERROR); 227 } 228 updateBatteryLayout()229 private void updateBatteryLayout() { 230 // Init the battery layouts. 231 hideAllOfBatteryLayouts(); 232 LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); 233 if (mAllOfCachedDevices.isEmpty()) { 234 Log.e(TAG, "There is no LeAudioProfile."); 235 return; 236 } 237 238 if (!leAudioProfile.isEnabled(mCachedDevice.getDevice())) { 239 Log.d(TAG, "Show the legacy battery style if the LeAudio is not enabled."); 240 final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); 241 if (summary != null) { 242 summary.setText(mCachedDevice.getConnectionSummary()); 243 } 244 return; 245 } 246 247 for (CachedBluetoothDevice cachedDevice : mAllOfCachedDevices) { 248 int deviceId = leAudioProfile.getAudioLocation(cachedDevice.getDevice()); 249 Log.d(TAG, "LeAudioDevices:" + cachedDevice.getDevice().getAnonymizedAddress() 250 + ", deviceId:" + deviceId); 251 252 if (deviceId == BluetoothLeAudio.AUDIO_LOCATION_INVALID) { 253 Log.d(TAG, "The device does not support the AUDIO_LOCATION."); 254 return; 255 } 256 boolean isLeft = (deviceId & LEFT_DEVICE_ID) != 0; 257 boolean isRight = (deviceId & RIGHT_DEVICE_ID) != 0; 258 boolean isLeftRight = isLeft && isRight; 259 // The LE device updates the BatteryLayout 260 if (isLeftRight) { 261 Log.d(TAG, "Show the legacy battery style if the device id is left+right."); 262 final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); 263 if (summary != null) { 264 summary.setText(mCachedDevice.getConnectionSummary()); 265 } 266 } else if (isLeft) { 267 updateBatteryLayout(R.id.bt_battery_left, cachedDevice.getBatteryLevel()); 268 } else if (isRight) { 269 updateBatteryLayout(R.id.bt_battery_right, cachedDevice.getBatteryLevel()); 270 } else { 271 Log.d(TAG, "The device id is other Audio Location. Do nothing."); 272 } 273 } 274 } 275 updateBatteryLayout(int resId, int batteryLevel)276 private void updateBatteryLayout(int resId, int batteryLevel) { 277 final View batteryView = mLayoutPreference.findViewById(resId); 278 if (batteryView == null) { 279 Log.e(TAG, "updateBatteryLayout: No View"); 280 return; 281 } 282 if (batteryLevel != BluetoothUtils.META_INT_ERROR) { 283 batteryView.setVisibility(View.VISIBLE); 284 final TextView batterySummaryView = 285 batteryView.requireViewById(getBatterySummaryResource(resId)); 286 final String batteryLevelPercentageString = 287 com.android.settings.Utils.formatPercentage(batteryLevel); 288 batterySummaryView.setText(batteryLevelPercentageString); 289 batterySummaryView.setContentDescription(mContext.getString( 290 com.android.settingslib.R.string.bluetooth_battery_level, 291 batteryLevelPercentageString)); 292 batterySummaryView.setCompoundDrawablesRelativeWithIntrinsicBounds( 293 createBtBatteryIcon(mContext, batteryLevel), /* top */ null, 294 /* end */ null, /* bottom */ null); 295 } else { 296 Log.d(TAG, "updateBatteryLayout: Hide it if it doesn't have battery information."); 297 batteryView.setVisibility(View.GONE); 298 } 299 } 300 301 @Override onDeviceAttributesChanged()302 public void onDeviceAttributesChanged() { 303 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 304 item.unregisterCallback(this); 305 } 306 mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice); 307 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 308 item.registerCallback(this); 309 } 310 311 if (!mAllOfCachedDevices.isEmpty()) { 312 refresh(); 313 } 314 } 315 } 316