1. Show mono battery info in LeAudioBluetoothDetailsHeaderController without side text in front of the battery icon 2. Show mono side device on Settings > Accessibility > Hearing devices's summary without side information Flag: EXEMPT bugfix Bug: 379616650 Test: atest AccessibilityHearingAidPreferenceControllerTest Test: manually check UI with real device Change-Id: I4a1a3357e2cef51df505923e38da33767c78e8f3
330 lines
14 KiB
Java
330 lines
14 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.ImageButton;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.preference.PreferenceFragmentCompat;
|
|
import androidx.preference.PreferenceScreen;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.core.BasePreferenceController;
|
|
import com.android.settings.flags.Flags;
|
|
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.Set;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
PreferenceFragmentCompat mFragment;
|
|
@VisibleForTesting
|
|
LayoutPreference mLayoutPreference;
|
|
LocalBluetoothManager mManager;
|
|
private CachedBluetoothDevice mCachedDevice;
|
|
private Set<CachedBluetoothDevice> mCachedDeviceGroup;
|
|
@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.getUiAccessibleProfiles()
|
|
.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 : mCachedDeviceGroup) {
|
|
item.registerCallback(this);
|
|
}
|
|
refresh();
|
|
}
|
|
|
|
@Override
|
|
public void onStop() {
|
|
if (!mIsRegisterCallback) {
|
|
return;
|
|
}
|
|
for (CachedBluetoothDevice item : mCachedDeviceGroup) {
|
|
item.unregisterCallback(this);
|
|
}
|
|
|
|
mIsRegisterCallback = false;
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
}
|
|
|
|
public void init(CachedBluetoothDevice cachedBluetoothDevice,
|
|
LocalBluetoothManager bluetoothManager, PreferenceFragmentCompat fragment) {
|
|
mCachedDevice = cachedBluetoothDevice;
|
|
mManager = bluetoothManager;
|
|
mProfileManager = bluetoothManager.getProfileManager();
|
|
mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
|
|
mFragment = fragment;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void refresh() {
|
|
if (mLayoutPreference == null || mCachedDevice == null) {
|
|
return;
|
|
}
|
|
if (Flags.enableBluetoothDeviceDetailsPolish()) {
|
|
ImageButton renameButton = mLayoutPreference.findViewById(R.id.rename_button);
|
|
renameButton.setVisibility(View.VISIBLE);
|
|
renameButton.setOnClickListener(view -> {
|
|
RemoteDeviceNameDialogFragment.newInstance(mCachedDevice).show(
|
|
mFragment.getFragmentManager(), RemoteDeviceNameDialogFragment.TAG);
|
|
});
|
|
}
|
|
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(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));
|
|
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;
|
|
} else if (containerId == R.id.bt_battery_mono) {
|
|
return R.id.bt_battery_mono_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);
|
|
// hide the mono
|
|
updateBatteryLayout(R.id.bt_battery_mono, BluetoothUtils.META_INT_ERROR);
|
|
}
|
|
|
|
private void updateBatteryLayout() {
|
|
// Init the battery layouts.
|
|
hideAllOfBatteryLayouts();
|
|
LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
|
|
if (mCachedDeviceGroup.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 : mCachedDeviceGroup) {
|
|
int deviceId = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
|
|
Log.d(TAG, "LeAudioDevices:" + cachedDevice.getDevice().getAnonymizedAddress()
|
|
+ ", deviceId:" + deviceId);
|
|
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 if (deviceId == BluetoothLeAudio.AUDIO_LOCATION_MONO) {
|
|
updateBatteryLayout(R.id.bt_battery_mono, 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(
|
|
com.android.settingslib.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 : mCachedDeviceGroup) {
|
|
item.unregisterCallback(this);
|
|
}
|
|
mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
|
|
for (CachedBluetoothDevice item : mCachedDeviceGroup) {
|
|
item.registerCallback(this);
|
|
}
|
|
|
|
if (!mCachedDeviceGroup.isEmpty()) {
|
|
refresh();
|
|
}
|
|
}
|
|
}
|