1 /*
2  * Copyright (C) 2014 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.systemui.qs.tiles;
18 
19 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
20 
21 import android.annotation.Nullable;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothClass;
24 import android.bluetooth.BluetoothDevice;
25 import android.content.Intent;
26 import android.os.Handler;
27 import android.os.HandlerExecutor;
28 import android.os.Looper;
29 import android.os.UserManager;
30 import android.provider.Settings;
31 import android.service.quicksettings.Tile;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.View;
35 import android.widget.Switch;
36 
37 import com.android.internal.logging.MetricsLogger;
38 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
39 import com.android.settingslib.Utils;
40 import com.android.settingslib.bluetooth.BluetoothUtils;
41 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
42 import com.android.systemui.R;
43 import com.android.systemui.dagger.qualifiers.Background;
44 import com.android.systemui.dagger.qualifiers.Main;
45 import com.android.systemui.plugins.ActivityStarter;
46 import com.android.systemui.plugins.FalsingManager;
47 import com.android.systemui.plugins.qs.QSTile.BooleanState;
48 import com.android.systemui.plugins.statusbar.StatusBarStateController;
49 import com.android.systemui.qs.QSHost;
50 import com.android.systemui.qs.QsEventLogger;
51 import com.android.systemui.qs.logging.QSLogger;
52 import com.android.systemui.qs.tileimpl.QSTileImpl;
53 import com.android.systemui.statusbar.policy.BluetoothController;
54 
55 import java.util.List;
56 import java.util.concurrent.Executor;
57 
58 import javax.inject.Inject;
59 
60 /** Quick settings tile: Bluetooth **/
61 public class BluetoothTile extends QSTileImpl<BooleanState> {
62 
63     public static final String TILE_SPEC = "bt";
64 
65     private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
66 
67     private static final String TAG = BluetoothTile.class.getSimpleName();
68 
69     private final BluetoothController mController;
70 
71     private CachedBluetoothDevice mMetadataRegisteredDevice = null;
72 
73     private final Executor mExecutor;
74 
75     @Inject
BluetoothTile( QSHost host, QsEventLogger uiEventLogger, @Background Looper backgroundLooper, @Main Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, BluetoothController bluetoothController )76     public BluetoothTile(
77             QSHost host,
78             QsEventLogger uiEventLogger,
79             @Background Looper backgroundLooper,
80             @Main Handler mainHandler,
81             FalsingManager falsingManager,
82             MetricsLogger metricsLogger,
83             StatusBarStateController statusBarStateController,
84             ActivityStarter activityStarter,
85             QSLogger qsLogger,
86             BluetoothController bluetoothController
87     ) {
88         super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
89                 statusBarStateController, activityStarter, qsLogger);
90         mController = bluetoothController;
91         mController.observe(getLifecycle(), mCallback);
92         mExecutor = new HandlerExecutor(mainHandler);
93     }
94 
95     @Override
newTileState()96     public BooleanState newTileState() {
97         return new BooleanState();
98     }
99 
100     @Override
handleClick(@ullable View view)101     protected void handleClick(@Nullable View view) {
102         // Secondary clicks are header clicks, just toggle.
103         final boolean isEnabled = mState.value;
104         // Immediately enter transient enabling state when turning bluetooth on.
105         refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
106         mController.setBluetoothEnabled(!isEnabled);
107     }
108 
109     @Override
getLongClickIntent()110     public Intent getLongClickIntent() {
111         return new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
112     }
113 
114     @Override
handleSecondaryClick(@ullable View view)115     protected void handleSecondaryClick(@Nullable View view) {
116         if (!mController.canConfigBluetooth()) {
117             mActivityStarter.postStartActivityDismissingKeyguard(
118                     new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0);
119             return;
120         }
121         if (!mState.value) {
122             mController.setBluetoothEnabled(true);
123         }
124     }
125 
126     @Override
getTileLabel()127     public CharSequence getTileLabel() {
128         return mContext.getString(R.string.quick_settings_bluetooth_label);
129     }
130 
131     @Override
handleSetListening(boolean listening)132     protected void handleSetListening(boolean listening) {
133         super.handleSetListening(listening);
134 
135         if (!listening) {
136             stopListeningToStaleDeviceMetadata();
137         }
138     }
139 
140     @Override
handleUpdateState(BooleanState state, Object arg)141     protected void handleUpdateState(BooleanState state, Object arg) {
142         checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH);
143         final boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
144         final boolean enabled = transientEnabling || mController.isBluetoothEnabled();
145         final boolean connected = mController.isBluetoothConnected();
146         final boolean connecting = mController.isBluetoothConnecting();
147         state.isTransient = transientEnabling || connecting ||
148                 mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON;
149         if (!enabled || !connected || state.isTransient) {
150             stopListeningToStaleDeviceMetadata();
151         }
152         state.dualTarget = true;
153         state.value = enabled;
154         if (state.slash == null) {
155             state.slash = new SlashState();
156         }
157         state.slash.isSlashed = !enabled;
158         state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
159         state.secondaryLabel = TextUtils.emptyIfNull(
160                 getSecondaryLabel(enabled, connecting, connected, state.isTransient));
161         state.contentDescription = mContext.getString(
162                 R.string.accessibility_quick_settings_bluetooth);
163         state.stateDescription = "";
164 
165         if (enabled) {
166             if (connected) {
167                 state.icon = ResourceIcon.get(R.drawable.qs_bluetooth_icon_on);
168                 if (!TextUtils.isEmpty(mController.getConnectedDeviceName())) {
169                     state.label = mController.getConnectedDeviceName();
170                 }
171                 state.stateDescription =
172                         mContext.getString(R.string.accessibility_bluetooth_name, state.label)
173                                 + ", " + state.secondaryLabel;
174             } else if (state.isTransient) {
175                 state.icon = ResourceIcon.get(
176                         R.drawable.qs_bluetooth_icon_search);
177                 state.stateDescription = state.secondaryLabel;
178             } else {
179                 state.icon =
180                         ResourceIcon.get(R.drawable.qs_bluetooth_icon_off);
181                 state.stateDescription = mContext.getString(R.string.accessibility_not_connected);
182             }
183             state.state = Tile.STATE_ACTIVE;
184         } else {
185             state.icon = ResourceIcon.get(R.drawable.qs_bluetooth_icon_off);
186             state.state = Tile.STATE_INACTIVE;
187         }
188 
189         state.expandedAccessibilityClassName = Switch.class.getName();
190     }
191 
192     /**
193      * Returns the secondary label to use for the given bluetooth connection in the form of the
194      * battery level or bluetooth profile name. If the bluetooth is disabled, there's no connected
195      * devices, or we can't map the bluetooth class to a profile, this instead returns {@code null}.
196      * @param enabled whether bluetooth is enabled
197      * @param connecting whether bluetooth is connecting to a device
198      * @param connected whether there's a device connected via bluetooth
199      * @param isTransient whether bluetooth is currently in a transient state turning on
200      */
201     @Nullable
getSecondaryLabel(boolean enabled, boolean connecting, boolean connected, boolean isTransient)202     private String getSecondaryLabel(boolean enabled, boolean connecting, boolean connected,
203             boolean isTransient) {
204         if (connecting) {
205             return mContext.getString(R.string.quick_settings_connecting);
206         }
207         if (isTransient) {
208             return mContext.getString(R.string.quick_settings_bluetooth_secondary_label_transient);
209         }
210 
211         List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices();
212         if (enabled && connected && !connectedDevices.isEmpty()) {
213             if (connectedDevices.size() > 1) {
214                 stopListeningToStaleDeviceMetadata();
215                 return icuMessageFormat(mContext.getResources(),
216                         R.string.quick_settings_hotspot_secondary_label_num_devices,
217                         connectedDevices.size());
218             }
219 
220             CachedBluetoothDevice device = connectedDevices.get(0);
221 
222             // Use battery level provided by FastPair metadata if available.
223             // If not, fallback to the default battery level from bluetooth.
224             int batteryLevel = getMetadataBatteryLevel(device);
225             if (batteryLevel > BluetoothUtils.META_INT_ERROR) {
226                 listenToMetadata(device);
227             } else {
228                 stopListeningToStaleDeviceMetadata();
229                 batteryLevel = device.getMinBatteryLevelWithMemberDevices();
230             }
231 
232             if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
233                 return mContext.getString(
234                         R.string.quick_settings_bluetooth_secondary_label_battery_level,
235                         Utils.formatPercentage(batteryLevel));
236             } else {
237                 final BluetoothClass bluetoothClass = device.getBtClass();
238                 if (bluetoothClass != null) {
239                     if (device.isHearingAidDevice()) {
240                         return mContext.getString(
241                                 R.string.quick_settings_bluetooth_secondary_label_hearing_aids);
242                     } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
243                         return mContext.getString(
244                                 R.string.quick_settings_bluetooth_secondary_label_audio);
245                     } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
246                         return mContext.getString(
247                                 R.string.quick_settings_bluetooth_secondary_label_headset);
248                     } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HID)) {
249                         return mContext.getString(
250                                 R.string.quick_settings_bluetooth_secondary_label_input);
251                     }
252                 }
253             }
254         }
255 
256         return null;
257     }
258 
259     @Override
getMetricsCategory()260     public int getMetricsCategory() {
261         return MetricsEvent.QS_BLUETOOTH;
262     }
263 
264     @Override
isAvailable()265     public boolean isAvailable() {
266         return mController.isBluetoothSupported();
267     }
268 
getMetadataBatteryLevel(CachedBluetoothDevice device)269     private int getMetadataBatteryLevel(CachedBluetoothDevice device) {
270         return BluetoothUtils.getIntMetaData(device.getDevice(),
271                 BluetoothDevice.METADATA_MAIN_BATTERY);
272     }
273 
listenToMetadata(CachedBluetoothDevice cachedDevice)274     private void listenToMetadata(CachedBluetoothDevice cachedDevice) {
275         if (cachedDevice == mMetadataRegisteredDevice) return;
276         stopListeningToStaleDeviceMetadata();
277         try {
278             mController.addOnMetadataChangedListener(cachedDevice,
279                     mExecutor,
280                     mMetadataChangedListener);
281             mMetadataRegisteredDevice = cachedDevice;
282         } catch (IllegalArgumentException e) {
283             Log.e(TAG, "Battery metadata listener already registered for device.");
284         }
285     }
286 
stopListeningToStaleDeviceMetadata()287     private void stopListeningToStaleDeviceMetadata() {
288         if (mMetadataRegisteredDevice == null) return;
289         try {
290             mController.removeOnMetadataChangedListener(
291                     mMetadataRegisteredDevice,
292                     mMetadataChangedListener);
293             mMetadataRegisteredDevice = null;
294         } catch (IllegalArgumentException e) {
295             Log.e(TAG, "Battery metadata listener already unregistered for device.");
296         }
297     }
298 
299     private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
300         @Override
301         public void onBluetoothStateChange(boolean enabled) {
302             refreshState();
303         }
304 
305         @Override
306         public void onBluetoothDevicesChanged() {
307             refreshState();
308         }
309     };
310 
311     private final BluetoothAdapter.OnMetadataChangedListener mMetadataChangedListener =
312             (device, key, value) -> {
313                 if (key == BluetoothDevice.METADATA_MAIN_BATTERY) refreshState();
314             };
315 }
316