blob: e30bbfb398ace88b638746c38f13cf7088ca4eca [file] [log] [blame]
/*
* 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();
}
}
}