/* * 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.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; 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(); } } }