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.settingslib.graph.BluetoothDeviceLayerDrawable.createLayerDrawable; 20 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothProfile; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.graphics.drawable.Drawable; 27 import android.provider.Settings; 28 import android.service.quicksettings.Tile; 29 import android.text.TextUtils; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.Switch; 33 34 import com.android.internal.logging.MetricsLogger; 35 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 36 import com.android.settingslib.Utils; 37 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.R; 40 import com.android.systemui.plugins.ActivityStarter; 41 import com.android.systemui.plugins.qs.DetailAdapter; 42 import com.android.systemui.plugins.qs.QSTile.BooleanState; 43 import com.android.systemui.qs.QSDetailItems; 44 import com.android.systemui.qs.QSDetailItems.Item; 45 import com.android.systemui.qs.QSHost; 46 import com.android.systemui.qs.tileimpl.QSTileImpl; 47 import com.android.systemui.statusbar.policy.BluetoothController; 48 49 import java.util.ArrayList; 50 import java.util.Collection; 51 52 /** Quick settings tile: Bluetooth **/ 53 public class BluetoothTile extends QSTileImpl<BooleanState> { 54 private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); 55 56 private final BluetoothController mController; 57 private final BluetoothDetailAdapter mDetailAdapter; 58 private final ActivityStarter mActivityStarter; 59 60 public BluetoothTile(QSHost host) { 61 super(host); 62 mController = Dependency.get(BluetoothController.class); 63 mActivityStarter = Dependency.get(ActivityStarter.class); 64 mDetailAdapter = (BluetoothDetailAdapter) createDetailAdapter(); 65 } 66 67 @Override 68 public DetailAdapter getDetailAdapter() { 69 return mDetailAdapter; 70 } 71 72 @Override 73 public BooleanState newTileState() { 74 return new BooleanState(); 75 } 76 77 @Override 78 public void handleSetListening(boolean listening) { 79 if (listening) { 80 mController.addCallback(mCallback); 81 } else { 82 mController.removeCallback(mCallback); 83 } 84 } 85 86 @Override 87 protected void handleClick() { 88 // Secondary clicks are header clicks, just toggle. 89 final boolean isEnabled = mState.value; 90 mController.setBluetoothEnabled(!isEnabled); 91 } 92 93 @Override 94 public Intent getLongClickIntent() { 95 return new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); 96 } 97 98 @Override 99 protected void handleSecondaryClick() { 100 if (!mController.canConfigBluetooth()) { 101 mActivityStarter.postStartActivityDismissingKeyguard( 102 new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0); 103 return; 104 } 105 showDetail(true); 106 if (!mState.value) { 107 mController.setBluetoothEnabled(true); 108 } 109 } 110 111 @Override 112 public CharSequence getTileLabel() { 113 return mContext.getString(R.string.quick_settings_bluetooth_label); 114 } 115 116 @Override 117 protected void handleUpdateState(BooleanState state, Object arg) { 118 final boolean enabled = mController.isBluetoothEnabled(); 119 final boolean connected = mController.isBluetoothConnected(); 120 state.isTransient = mController.isBluetoothConnecting() 121 || mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON; 122 state.dualTarget = true; 123 state.value = enabled; 124 if (state.slash == null) { 125 state.slash = new SlashState(); 126 } 127 state.slash.isSlashed = !enabled; 128 if (enabled) { 129 state.label = null; 130 if (connected) { 131 state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_connected); 132 state.label = mController.getLastDeviceName(); 133 CachedBluetoothDevice lastDevice = mController.getLastDevice(); 134 if (lastDevice != null) { 135 int batteryLevel = lastDevice.getBatteryLevel(); 136 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 137 state.icon = new BluetoothBatteryDrawable(batteryLevel, 138 mContext.getResources().getFraction( 139 R.fraction.bt_battery_scale_fraction, 1, 1)); 140 } 141 } 142 state.contentDescription = mContext.getString( 143 R.string.accessibility_bluetooth_name, state.label); 144 } else if (state.isTransient) { 145 state.icon = ResourceIcon.get(R.drawable.ic_bluetooth_transient_animation); 146 state.contentDescription = mContext.getString( 147 R.string.accessibility_quick_settings_bluetooth_connecting); 148 state.label = mContext.getString(R.string.quick_settings_bluetooth_label); 149 } else { 150 state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on); 151 state.contentDescription = mContext.getString( 152 R.string.accessibility_quick_settings_bluetooth_on) + "," 153 + mContext.getString(R.string.accessibility_not_connected); 154 } 155 if (TextUtils.isEmpty(state.label)) { 156 state.label = mContext.getString(R.string.quick_settings_bluetooth_label); 157 } 158 state.state = Tile.STATE_ACTIVE; 159 } else { 160 state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on); 161 state.label = mContext.getString(R.string.quick_settings_bluetooth_label); 162 state.contentDescription = mContext.getString( 163 R.string.accessibility_quick_settings_bluetooth_off); 164 state.state = Tile.STATE_INACTIVE; 165 } 166 167 state.dualLabelContentDescription = mContext.getResources().getString( 168 R.string.accessibility_quick_settings_open_settings, getTileLabel()); 169 state.expandedAccessibilityClassName = Switch.class.getName(); 170 } 171 172 @Override 173 public int getMetricsCategory() { 174 return MetricsEvent.QS_BLUETOOTH; 175 } 176 177 @Override 178 protected String composeChangeAnnouncement() { 179 if (mState.value) { 180 return mContext.getString(R.string.accessibility_quick_settings_bluetooth_changed_on); 181 } else { 182 return mContext.getString(R.string.accessibility_quick_settings_bluetooth_changed_off); 183 } 184 } 185 186 @Override 187 public boolean isAvailable() { 188 return mController.isBluetoothSupported(); 189 } 190 191 private final BluetoothController.Callback mCallback = new BluetoothController.Callback() { 192 @Override 193 public void onBluetoothStateChange(boolean enabled) { 194 refreshState(); 195 if (isShowingDetail()) { 196 mDetailAdapter.updateItems(); 197 fireToggleStateChanged(mDetailAdapter.getToggleState()); 198 } 199 } 200 201 @Override 202 public void onBluetoothDevicesChanged() { 203 refreshState(); 204 if (isShowingDetail()) { 205 mDetailAdapter.updateItems(); 206 } 207 } 208 }; 209 210 @Override 211 protected DetailAdapter createDetailAdapter() { 212 return new BluetoothDetailAdapter(); 213 } 214 215 private class BluetoothBatteryDrawable extends Icon { 216 private int mLevel; 217 private float mIconScale; 218 219 BluetoothBatteryDrawable(int level) { 220 this(level, 1 /* iconScale */); 221 } 222 223 BluetoothBatteryDrawable(int level, float iconScale) { 224 mLevel = level; 225 mIconScale = iconScale; 226 } 227 228 @Override 229 public Drawable getDrawable(Context context) { 230 return createLayerDrawable(context, 231 R.drawable.ic_qs_bluetooth_connected, mLevel, mIconScale); 232 } 233 } 234 235 protected class BluetoothDetailAdapter implements DetailAdapter, QSDetailItems.Callback { 236 // We probably won't ever have space in the UI for more than 20 devices, so don't 237 // get info for them. 238 private static final int MAX_DEVICES = 20; 239 private QSDetailItems mItems; 240 241 @Override 242 public CharSequence getTitle() { 243 return mContext.getString(R.string.quick_settings_bluetooth_label); 244 } 245 246 @Override 247 public Boolean getToggleState() { 248 return mState.value; 249 } 250 251 @Override 252 public boolean getToggleEnabled() { 253 return mController.getBluetoothState() == BluetoothAdapter.STATE_OFF 254 || mController.getBluetoothState() == BluetoothAdapter.STATE_ON; 255 } 256 257 @Override 258 public Intent getSettingsIntent() { 259 return BLUETOOTH_SETTINGS; 260 } 261 262 @Override 263 public void setToggleState(boolean state) { 264 MetricsLogger.action(mContext, MetricsEvent.QS_BLUETOOTH_TOGGLE, state); 265 mController.setBluetoothEnabled(state); 266 } 267 268 @Override 269 public int getMetricsCategory() { 270 return MetricsEvent.QS_BLUETOOTH_DETAILS; 271 } 272 273 @Override 274 public View createDetailView(Context context, View convertView, ViewGroup parent) { 275 mItems = QSDetailItems.convertOrInflate(context, convertView, parent); 276 mItems.setTagSuffix("Bluetooth"); 277 mItems.setCallback(this); 278 updateItems(); 279 setItemsVisible(mState.value); 280 return mItems; 281 } 282 283 public void setItemsVisible(boolean visible) { 284 if (mItems == null) return; 285 mItems.setItemsVisible(visible); 286 } 287 288 private void updateItems() { 289 if (mItems == null) return; 290 if (mController.isBluetoothEnabled()) { 291 mItems.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, 292 R.string.quick_settings_bluetooth_detail_empty_text); 293 } else { 294 mItems.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, 295 R.string.bt_is_off); 296 } 297 ArrayList<Item> items = new ArrayList<Item>(); 298 final Collection<CachedBluetoothDevice> devices = mController.getDevices(); 299 if (devices != null) { 300 int connectedDevices = 0; 301 int count = 0; 302 for (CachedBluetoothDevice device : devices) { 303 if (mController.getBondState(device) == BluetoothDevice.BOND_NONE) continue; 304 final Item item = new Item(); 305 item.icon = R.drawable.ic_qs_bluetooth_on; 306 item.line1 = device.getName(); 307 item.tag = device; 308 int state = device.getMaxConnectionState(); 309 if (state == BluetoothProfile.STATE_CONNECTED) { 310 item.icon = R.drawable.ic_qs_bluetooth_connected; 311 int batteryLevel = device.getBatteryLevel(); 312 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 313 item.iconDrawable = new BluetoothBatteryDrawable(batteryLevel); 314 item.line2 = mContext.getString( 315 R.string.quick_settings_connected_battery_level, 316 Utils.formatPercentage(batteryLevel)); 317 } else { 318 item.line2 = mContext.getString(R.string.quick_settings_connected); 319 } 320 item.canDisconnect = true; 321 items.add(connectedDevices, item); 322 connectedDevices++; 323 } else if (state == BluetoothProfile.STATE_CONNECTING) { 324 item.icon = R.drawable.ic_qs_bluetooth_connecting; 325 item.line2 = mContext.getString(R.string.quick_settings_connecting); 326 items.add(connectedDevices, item); 327 } else { 328 items.add(item); 329 } 330 if (++count == MAX_DEVICES) { 331 break; 332 } 333 } 334 } 335 mItems.setItems(items.toArray(new Item[items.size()])); 336 } 337 338 @Override 339 public void onDetailItemClick(Item item) { 340 if (item == null || item.tag == null) return; 341 final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; 342 if (device != null && device.getMaxConnectionState() 343 == BluetoothProfile.STATE_DISCONNECTED) { 344 mController.connect(device); 345 } 346 } 347 348 @Override 349 public void onDetailItemDisconnect(Item item) { 350 if (item == null || item.tag == null) return; 351 final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; 352 if (device != null) { 353 mController.disconnect(device); 354 } 355 } 356 } 357 } 358