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