1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.bluetooth;
18 
19 import android.bluetooth.BluetoothLeAudio;
20 import android.bluetooth.BluetoothProfile;
21 import android.content.Context;
22 import android.graphics.PorterDuff;
23 import android.graphics.PorterDuffColorFilter;
24 import android.graphics.drawable.Drawable;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.util.Log;
28 import android.util.Pair;
29 import android.view.View;
30 import android.widget.ImageView;
31 import android.widget.TextView;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.preference.PreferenceScreen;
35 
36 import com.android.settings.R;
37 import com.android.settings.core.BasePreferenceController;
38 import com.android.settings.fuelgauge.BatteryMeterView;
39 import com.android.settingslib.bluetooth.BluetoothUtils;
40 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
41 import com.android.settingslib.bluetooth.LeAudioProfile;
42 import com.android.settingslib.bluetooth.LocalBluetoothManager;
43 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
44 import com.android.settingslib.core.lifecycle.LifecycleObserver;
45 import com.android.settingslib.core.lifecycle.events.OnDestroy;
46 import com.android.settingslib.core.lifecycle.events.OnStart;
47 import com.android.settingslib.core.lifecycle.events.OnStop;
48 import com.android.settingslib.widget.LayoutPreference;
49 
50 import java.util.List;
51 
52 /**
53  * This class adds a header with device name and status (connected/disconnected, etc.).
54  */
55 public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceController implements
56         LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback {
57     private static final String TAG = "LeAudioBtHeaderCtrl";
58     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
59 
60     @VisibleForTesting
61     static final int LEFT_DEVICE_ID =
62             BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT
63                     | BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT
64                     | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER
65                     | BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT
66                     | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT
67                     | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT
68                     | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT
69                     | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT
70                     | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE
71                     | BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND;
72 
73     @VisibleForTesting
74     static final int RIGHT_DEVICE_ID =
75             BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT
76                     | BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT
77                     | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER
78                     | BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT
79                     | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT
80                     | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT
81                     | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT
82                     | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT
83                     | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE
84                     | BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND;
85 
86     @VisibleForTesting
87     static final int INVALID_RESOURCE_ID = -1;
88 
89     @VisibleForTesting
90     LayoutPreference mLayoutPreference;
91     LocalBluetoothManager mManager;
92     private CachedBluetoothDevice mCachedDevice;
93     private List<CachedBluetoothDevice> mAllOfCachedDevices;
94     @VisibleForTesting
95     Handler mHandler = new Handler(Looper.getMainLooper());
96     @VisibleForTesting
97     boolean mIsRegisterCallback = false;
98 
99     private LocalBluetoothProfileManager mProfileManager;
100 
LeAudioBluetoothDetailsHeaderController(Context context, String prefKey)101     public LeAudioBluetoothDetailsHeaderController(Context context, String prefKey) {
102         super(context, prefKey);
103     }
104 
105     @Override
getAvailabilityStatus()106     public int getAvailabilityStatus() {
107         if (mCachedDevice == null || mProfileManager == null) {
108             return CONDITIONALLY_UNAVAILABLE;
109         }
110         boolean hasLeAudio = mCachedDevice.getConnectableProfiles()
111                 .stream()
112                 .anyMatch(profile -> profile.getProfileId() == BluetoothProfile.LE_AUDIO);
113 
114         return !BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice()) && hasLeAudio
115                 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
116     }
117 
118     @Override
displayPreference(PreferenceScreen screen)119     public void displayPreference(PreferenceScreen screen) {
120         super.displayPreference(screen);
121         mLayoutPreference = screen.findPreference(getPreferenceKey());
122         mLayoutPreference.setVisible(isAvailable());
123     }
124 
125     @Override
onStart()126     public void onStart() {
127         if (!isAvailable()) {
128             return;
129         }
130         mIsRegisterCallback = true;
131         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
132             item.registerCallback(this);
133         }
134         refresh();
135     }
136 
137     @Override
onStop()138     public void onStop() {
139         if (!mIsRegisterCallback) {
140             return;
141         }
142         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
143             item.unregisterCallback(this);
144         }
145 
146         mIsRegisterCallback = false;
147     }
148 
149     @Override
onDestroy()150     public void onDestroy() {
151     }
152 
init(CachedBluetoothDevice cachedBluetoothDevice, LocalBluetoothManager bluetoothManager)153     public void init(CachedBluetoothDevice cachedBluetoothDevice,
154             LocalBluetoothManager bluetoothManager) {
155         mCachedDevice = cachedBluetoothDevice;
156         mManager = bluetoothManager;
157         mProfileManager = bluetoothManager.getProfileManager();
158         mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
159     }
160 
161     @VisibleForTesting
refresh()162     void refresh() {
163         if (mLayoutPreference == null || mCachedDevice == null) {
164             return;
165         }
166         final ImageView imageView = mLayoutPreference.findViewById(R.id.entity_header_icon);
167         if (imageView != null) {
168             final Pair<Drawable, String> pair =
169                     BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, mCachedDevice);
170             imageView.setImageDrawable(pair.first);
171             imageView.setContentDescription(pair.second);
172         }
173 
174         final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title);
175         if (title != null) {
176             title.setText(mCachedDevice.getName());
177         }
178         final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
179         if (summary != null) {
180             summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */));
181         }
182 
183         if (!mCachedDevice.isConnected() || mCachedDevice.isBusy()) {
184             hideAllOfBatteryLayouts();
185             return;
186         }
187 
188         updateBatteryLayout();
189     }
190 
191     @VisibleForTesting
createBtBatteryIcon(Context context, int level)192     Drawable createBtBatteryIcon(Context context, int level) {
193         final BatteryMeterView.BatteryMeterDrawable drawable =
194                 new BatteryMeterView.BatteryMeterDrawable(context,
195                         context.getColor(com.android.settingslib.R.color.meter_background_color),
196                         context.getResources().getDimensionPixelSize(
197                                 R.dimen.advanced_bluetooth_battery_meter_width),
198                         context.getResources().getDimensionPixelSize(
199                                 R.dimen.advanced_bluetooth_battery_meter_height));
200         drawable.setBatteryLevel(level);
201         drawable.setColorFilter(new PorterDuffColorFilter(
202                 com.android.settings.Utils.getColorAttrDefaultColor(context,
203                         android.R.attr.colorControlNormal),
204                 PorterDuff.Mode.SRC));
205         return drawable;
206     }
207 
getBatterySummaryResource(int containerId)208     private int getBatterySummaryResource(int containerId) {
209         if (containerId == R.id.bt_battery_case) {
210             return R.id.bt_battery_case_summary;
211         } else if (containerId == R.id.bt_battery_left) {
212             return R.id.bt_battery_left_summary;
213         } else if (containerId == R.id.bt_battery_right) {
214             return R.id.bt_battery_right_summary;
215         }
216         Log.d(TAG, "No summary resource id. The containerId is " + containerId);
217         return INVALID_RESOURCE_ID;
218     }
219 
hideAllOfBatteryLayouts()220     private void hideAllOfBatteryLayouts() {
221         // hide the case
222         updateBatteryLayout(R.id.bt_battery_case, BluetoothUtils.META_INT_ERROR);
223         // hide the left
224         updateBatteryLayout(R.id.bt_battery_left, BluetoothUtils.META_INT_ERROR);
225         // hide the right
226         updateBatteryLayout(R.id.bt_battery_right, BluetoothUtils.META_INT_ERROR);
227     }
228 
updateBatteryLayout()229     private void updateBatteryLayout() {
230         // Init the battery layouts.
231         hideAllOfBatteryLayouts();
232         LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
233         if (mAllOfCachedDevices.isEmpty()) {
234             Log.e(TAG, "There is no LeAudioProfile.");
235             return;
236         }
237 
238         if (!leAudioProfile.isEnabled(mCachedDevice.getDevice())) {
239             Log.d(TAG, "Show the legacy battery style if the LeAudio is not enabled.");
240             final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
241             if (summary != null) {
242                 summary.setText(mCachedDevice.getConnectionSummary());
243             }
244             return;
245         }
246 
247         for (CachedBluetoothDevice cachedDevice : mAllOfCachedDevices) {
248             int deviceId = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
249             Log.d(TAG, "LeAudioDevices:" + cachedDevice.getDevice().getAnonymizedAddress()
250                     + ", deviceId:" + deviceId);
251 
252             if (deviceId == BluetoothLeAudio.AUDIO_LOCATION_INVALID) {
253                 Log.d(TAG, "The device does not support the AUDIO_LOCATION.");
254                 return;
255             }
256             boolean isLeft = (deviceId & LEFT_DEVICE_ID) != 0;
257             boolean isRight = (deviceId & RIGHT_DEVICE_ID) != 0;
258             boolean isLeftRight = isLeft && isRight;
259             // The LE device updates the BatteryLayout
260             if (isLeftRight) {
261                 Log.d(TAG, "Show the legacy battery style if the device id is left+right.");
262                 final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
263                 if (summary != null) {
264                     summary.setText(mCachedDevice.getConnectionSummary());
265                 }
266             } else if (isLeft) {
267                 updateBatteryLayout(R.id.bt_battery_left, cachedDevice.getBatteryLevel());
268             } else if (isRight) {
269                 updateBatteryLayout(R.id.bt_battery_right, cachedDevice.getBatteryLevel());
270             } else {
271                 Log.d(TAG, "The device id is other Audio Location. Do nothing.");
272             }
273         }
274     }
275 
updateBatteryLayout(int resId, int batteryLevel)276     private void updateBatteryLayout(int resId, int batteryLevel) {
277         final View batteryView = mLayoutPreference.findViewById(resId);
278         if (batteryView == null) {
279             Log.e(TAG, "updateBatteryLayout: No View");
280             return;
281         }
282         if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
283             batteryView.setVisibility(View.VISIBLE);
284             final TextView batterySummaryView =
285                     batteryView.requireViewById(getBatterySummaryResource(resId));
286             final String batteryLevelPercentageString =
287                     com.android.settings.Utils.formatPercentage(batteryLevel);
288             batterySummaryView.setText(batteryLevelPercentageString);
289             batterySummaryView.setContentDescription(mContext.getString(
290                     com.android.settingslib.R.string.bluetooth_battery_level,
291                     batteryLevelPercentageString));
292             batterySummaryView.setCompoundDrawablesRelativeWithIntrinsicBounds(
293                     createBtBatteryIcon(mContext, batteryLevel), /* top */ null,
294                     /* end */ null, /* bottom */ null);
295         } else {
296             Log.d(TAG, "updateBatteryLayout: Hide it if it doesn't have battery information.");
297             batteryView.setVisibility(View.GONE);
298         }
299     }
300 
301     @Override
onDeviceAttributesChanged()302     public void onDeviceAttributesChanged() {
303         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
304             item.unregisterCallback(this);
305         }
306         mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
307         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
308             item.registerCallback(this);
309         }
310 
311         if (!mAllOfCachedDevices.isEmpty()) {
312             refresh();
313         }
314     }
315 }
316