The original method name implied that the returned profiles were guaranteed to be connectable. However, it actually filters profiles based on user can access the profile in the UI and initiate the connection. Change the method name to getUiAccessibleProfiles() aligns the name with the actual behavior. Flag: EXEMPT simple renaming Bug: 356530795 Test: m Change-Id: Ib7d29a4bf9c213ddcecc567864583165ab97a099
316 lines
13 KiB
Java
316 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;
|
|
LocalBluetoothManager mManager;
|
|
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.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 : 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;
|
|
mManager = bluetoothManager;
|
|
mProfileManager = bluetoothManager.getProfileManager();
|
|
mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, 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(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;
|
|
}
|
|
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(
|
|
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 : mAllOfCachedDevices) {
|
|
item.unregisterCallback(this);
|
|
}
|
|
mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
|
|
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
|
|
item.registerCallback(this);
|
|
}
|
|
|
|
if (!mAllOfCachedDevices.isEmpty()) {
|
|
refresh();
|
|
}
|
|
}
|
|
}
|