Since the LE audio have two or more devices in the same group and the UI show their status at one preference, the UI need to register callback for each of the devices, and also refresh the deviceList. Bug: 278155752 Test: local test to pairing the device and check the battery part Change-Id: I2fcde92a1f68b8437465b234820f7bad13dfc444
313 lines
13 KiB
Java
313 lines
13 KiB
Java
/*
|
|
* Copyright (C) 2022 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 android.bluetooth.BluetoothLeAudio;
|
|
import android.bluetooth.BluetoothProfile;
|
|
import android.content.Context;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.PorterDuffColorFilter;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.util.Log;
|
|
import android.util.Pair;
|
|
import android.view.View;
|
|
import android.widget.ImageView;
|
|
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.bluetooth.LeAudioProfile;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
|
|
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.widget.LayoutPreference;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* This class adds a header with device name and status (connected/disconnected, etc.).
|
|
*/
|
|
public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceController implements
|
|
LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback {
|
|
private static final String TAG = "LeAudioBtHeaderCtrl";
|
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
|
|
|
@VisibleForTesting
|
|
static final int LEFT_DEVICE_ID =
|
|
BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER
|
|
| BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE
|
|
| BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND;
|
|
|
|
@VisibleForTesting
|
|
static final int RIGHT_DEVICE_ID =
|
|
BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER
|
|
| BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT
|
|
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE
|
|
| BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND;
|
|
|
|
@VisibleForTesting
|
|
static final int INVALID_RESOURCE_ID = -1;
|
|
|
|
@VisibleForTesting
|
|
LayoutPreference mLayoutPreference;
|
|
private CachedBluetoothDevice mCachedDevice;
|
|
private List<CachedBluetoothDevice> mAllOfCachedDevices;
|
|
@VisibleForTesting
|
|
Handler mHandler = new Handler(Looper.getMainLooper());
|
|
@VisibleForTesting
|
|
boolean mIsRegisterCallback = false;
|
|
|
|
private LocalBluetoothProfileManager mProfileManager;
|
|
|
|
public LeAudioBluetoothDetailsHeaderController(Context context, String prefKey) {
|
|
super(context, prefKey);
|
|
}
|
|
|
|
@Override
|
|
public int getAvailabilityStatus() {
|
|
if (mCachedDevice == null || mProfileManager == null) {
|
|
return CONDITIONALLY_UNAVAILABLE;
|
|
}
|
|
boolean hasLeAudio = mCachedDevice.getConnectableProfiles()
|
|
.stream()
|
|
.anyMatch(profile -> profile.getProfileId() == BluetoothProfile.LE_AUDIO);
|
|
|
|
return !BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice()) && hasLeAudio
|
|
? 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;
|
|
}
|
|
mIsRegisterCallback = true;
|
|
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
|
|
item.registerCallback(this);
|
|
}
|
|
refresh();
|
|
}
|
|
|
|
@Override
|
|
public void onStop() {
|
|
if (!mIsRegisterCallback) {
|
|
return;
|
|
}
|
|
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
|
|
item.unregisterCallback(this);
|
|
}
|
|
|
|
mIsRegisterCallback = false;
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
}
|
|
|
|
public void init(CachedBluetoothDevice cachedBluetoothDevice,
|
|
LocalBluetoothManager bluetoothManager) {
|
|
mCachedDevice = cachedBluetoothDevice;
|
|
mProfileManager = bluetoothManager.getProfileManager();
|
|
mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void refresh() {
|
|
if (mLayoutPreference == null || mCachedDevice == null) {
|
|
return;
|
|
}
|
|
final ImageView imageView = mLayoutPreference.findViewById(R.id.entity_header_icon);
|
|
if (imageView != null) {
|
|
final Pair<Drawable, String> pair =
|
|
BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, mCachedDevice);
|
|
imageView.setImageDrawable(pair.first);
|
|
imageView.setContentDescription(pair.second);
|
|
}
|
|
|
|
final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title);
|
|
if (title != null) {
|
|
title.setText(mCachedDevice.getName());
|
|
}
|
|
final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
|
|
if (summary != null) {
|
|
summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */));
|
|
}
|
|
|
|
if (!mCachedDevice.isConnected() || mCachedDevice.isBusy()) {
|
|
hideAllOfBatteryLayouts();
|
|
return;
|
|
}
|
|
|
|
updateBatteryLayout();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
Drawable createBtBatteryIcon(Context context, int level) {
|
|
final BatteryMeterView.BatteryMeterDrawable drawable =
|
|
new BatteryMeterView.BatteryMeterDrawable(context,
|
|
context.getColor(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));
|
|
return drawable;
|
|
}
|
|
|
|
private int getBatterySummaryResource(int containerId) {
|
|
if (containerId == R.id.bt_battery_case) {
|
|
return R.id.bt_battery_case_summary;
|
|
} else if (containerId == R.id.bt_battery_left) {
|
|
return R.id.bt_battery_left_summary;
|
|
} else if (containerId == R.id.bt_battery_right) {
|
|
return R.id.bt_battery_right_summary;
|
|
}
|
|
Log.d(TAG, "No summary resource id. The containerId is " + containerId);
|
|
return INVALID_RESOURCE_ID;
|
|
}
|
|
|
|
private void hideAllOfBatteryLayouts() {
|
|
// hide the case
|
|
updateBatteryLayout(R.id.bt_battery_case, BluetoothUtils.META_INT_ERROR);
|
|
// hide the left
|
|
updateBatteryLayout(R.id.bt_battery_left, BluetoothUtils.META_INT_ERROR);
|
|
// hide the right
|
|
updateBatteryLayout(R.id.bt_battery_right, BluetoothUtils.META_INT_ERROR);
|
|
}
|
|
|
|
private void updateBatteryLayout() {
|
|
// Init the battery layouts.
|
|
hideAllOfBatteryLayouts();
|
|
LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
|
|
if (mAllOfCachedDevices.isEmpty()) {
|
|
Log.e(TAG, "There is no LeAudioProfile.");
|
|
return;
|
|
}
|
|
|
|
if (!leAudioProfile.isEnabled(mCachedDevice.getDevice())) {
|
|
Log.d(TAG, "Show the legacy battery style if the LeAudio is not enabled.");
|
|
final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
|
|
if (summary != null) {
|
|
summary.setText(mCachedDevice.getConnectionSummary());
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (CachedBluetoothDevice cachedDevice : mAllOfCachedDevices) {
|
|
int deviceId = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
|
|
Log.d(TAG, "LeAudioDevices:" + cachedDevice.getDevice().getAnonymizedAddress()
|
|
+ ", deviceId:" + deviceId);
|
|
|
|
if (deviceId == BluetoothLeAudio.AUDIO_LOCATION_INVALID) {
|
|
Log.d(TAG, "The device does not support the AUDIO_LOCATION.");
|
|
return;
|
|
}
|
|
boolean isLeft = (deviceId & LEFT_DEVICE_ID) != 0;
|
|
boolean isRight = (deviceId & RIGHT_DEVICE_ID) != 0;
|
|
boolean isLeftRight = isLeft && isRight;
|
|
// The LE device updates the BatteryLayout
|
|
if (isLeftRight) {
|
|
Log.d(TAG, "Show the legacy battery style if the device id is left+right.");
|
|
final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
|
|
if (summary != null) {
|
|
summary.setText(mCachedDevice.getConnectionSummary());
|
|
}
|
|
} else if (isLeft) {
|
|
updateBatteryLayout(R.id.bt_battery_left, cachedDevice.getBatteryLevel());
|
|
} else if (isRight) {
|
|
updateBatteryLayout(R.id.bt_battery_right, cachedDevice.getBatteryLevel());
|
|
} else {
|
|
Log.d(TAG, "The device id is other Audio Location. Do nothing.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updateBatteryLayout(int resId, int batteryLevel) {
|
|
final View batteryView = mLayoutPreference.findViewById(resId);
|
|
if (batteryView == null) {
|
|
Log.e(TAG, "updateBatteryLayout: No View");
|
|
return;
|
|
}
|
|
if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
|
|
batteryView.setVisibility(View.VISIBLE);
|
|
final TextView batterySummaryView =
|
|
batteryView.requireViewById(getBatterySummaryResource(resId));
|
|
final String batteryLevelPercentageString =
|
|
com.android.settings.Utils.formatPercentage(batteryLevel);
|
|
batterySummaryView.setText(batteryLevelPercentageString);
|
|
batterySummaryView.setContentDescription(mContext.getString(
|
|
R.string.bluetooth_battery_level, batteryLevelPercentageString));
|
|
batterySummaryView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
|
createBtBatteryIcon(mContext, batteryLevel), /* top */ null,
|
|
/* end */ null, /* bottom */ null);
|
|
} else {
|
|
Log.d(TAG, "updateBatteryLayout: Hide it if it doesn't have battery information.");
|
|
batteryView.setVisibility(View.GONE);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDeviceAttributesChanged() {
|
|
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
|
|
item.unregisterCallback(this);
|
|
}
|
|
mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
|
|
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
|
|
item.registerCallback(this);
|
|
}
|
|
|
|
if (!mAllOfCachedDevices.isEmpty()) {
|
|
refresh();
|
|
}
|
|
}
|
|
}
|