1 /* 2 * Copyright (C) 2020 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.car.bluetooth; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothHeadsetClient; 22 import android.bluetooth.BluetoothProfile; 23 import android.bluetooth.BluetoothProfile.ServiceListener; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.os.Bundle; 29 import android.util.Log; 30 31 import com.android.systemui.statusbar.policy.BatteryController; 32 33 import java.io.FileDescriptor; 34 import java.io.PrintWriter; 35 import java.util.ArrayList; 36 37 /** 38 * A {@link BatteryController} that is specific to the Auto use-case. For Auto, the battery icon 39 * displays the battery status of a device that is connected via bluetooth and not the system's 40 * battery. 41 */ 42 public class CarBatteryController extends BroadcastReceiver implements BatteryController { 43 private static final String TAG = "CarBatteryController"; 44 45 // According to the Bluetooth HFP 1.5 specification, battery levels are indicated by a 46 // value from 1-5, where these values represent the following: 47 // 0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5 48 // As a result, set the level as the average within that range. 49 private static final int BATTERY_LEVEL_EMPTY = 0; 50 private static final int BATTERY_LEVEL_1 = 12; 51 private static final int BATTERY_LEVEL_2 = 28; 52 private static final int BATTERY_LEVEL_3 = 63; 53 private static final int BATTERY_LEVEL_4 = 87; 54 private static final int BATTERY_LEVEL_FULL = 100; 55 56 private static final int INVALID_BATTERY_LEVEL = -1; 57 58 private final Context mContext; 59 60 private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); 61 private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>(); 62 private BluetoothHeadsetClient mBluetoothHeadsetClient; 63 private final ServiceListener mHfpServiceListener = new ServiceListener() { 64 @Override 65 public void onServiceConnected(int profile, BluetoothProfile proxy) { 66 if (profile == BluetoothProfile.HEADSET_CLIENT) { 67 mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy; 68 } 69 } 70 71 @Override 72 public void onServiceDisconnected(int profile) { 73 if (profile == BluetoothProfile.HEADSET_CLIENT) { 74 mBluetoothHeadsetClient = null; 75 } 76 } 77 }; 78 private int mLevel; 79 private BatteryViewHandler mBatteryViewHandler; 80 CarBatteryController(Context context)81 public CarBatteryController(Context context) { 82 mContext = context; 83 84 if (mAdapter == null) { 85 return; 86 } 87 88 mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener, 89 BluetoothProfile.HEADSET_CLIENT); 90 } 91 92 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)93 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 94 pw.println("CarBatteryController state:"); 95 pw.print(" mLevel="); 96 pw.println(mLevel); 97 } 98 99 @Override setPowerSaveMode(boolean powerSave)100 public void setPowerSaveMode(boolean powerSave) { 101 // No-op. No power save mode for the car. 102 } 103 104 @Override addCallback(BatteryController.BatteryStateChangeCallback cb)105 public void addCallback(BatteryController.BatteryStateChangeCallback cb) { 106 mChangeCallbacks.add(cb); 107 108 // There is no way to know if the phone is plugged in or charging via bluetooth, so pass 109 // false for these values. 110 cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */); 111 cb.onPowerSaveChanged(false /* isPowerSave */); 112 } 113 114 @Override removeCallback(BatteryController.BatteryStateChangeCallback cb)115 public void removeCallback(BatteryController.BatteryStateChangeCallback cb) { 116 mChangeCallbacks.remove(cb); 117 } 118 119 /** Sets {@link BatteryViewHandler}. */ addBatteryViewHandler(BatteryViewHandler batteryViewHandler)120 public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) { 121 mBatteryViewHandler = batteryViewHandler; 122 } 123 124 /** Starts listening for bluetooth broadcast messages. */ startListening()125 public void startListening() { 126 IntentFilter filter = new IntentFilter(); 127 filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); 128 filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT); 129 mContext.registerReceiver(this, filter); 130 } 131 132 /** Stops listening for bluetooth broadcast messages. */ stopListening()133 public void stopListening() { 134 mContext.unregisterReceiver(this); 135 } 136 137 @Override onReceive(Context context, Intent intent)138 public void onReceive(Context context, Intent intent) { 139 String action = intent.getAction(); 140 141 if (Log.isLoggable(TAG, Log.DEBUG)) { 142 Log.d(TAG, "onReceive(). action: " + action); 143 } 144 145 if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) { 146 if (Log.isLoggable(TAG, Log.DEBUG)) { 147 Log.d(TAG, "Received ACTION_AG_EVENT"); 148 } 149 150 int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL, 151 INVALID_BATTERY_LEVEL); 152 153 updateBatteryLevel(batteryLevel); 154 155 if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) { 156 mBatteryViewHandler.showBatteryView(); 157 } 158 } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 159 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 160 161 if (Log.isLoggable(TAG, Log.DEBUG)) { 162 int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 163 Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: " 164 + oldState + " -> " + newState); 165 166 } 167 BluetoothDevice device = 168 (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE); 169 updateBatteryIcon(device, newState); 170 } 171 } 172 173 /** 174 * Converts the battery level to a percentage that can be displayed on-screen and notifies 175 * any {@link BatteryStateChangeCallback}s of this. 176 */ updateBatteryLevel(int batteryLevel)177 private void updateBatteryLevel(int batteryLevel) { 178 if (batteryLevel == INVALID_BATTERY_LEVEL) { 179 if (Log.isLoggable(TAG, Log.DEBUG)) { 180 Log.d(TAG, "Battery level invalid. Ignoring."); 181 } 182 return; 183 } 184 185 // The battery level is a value between 0-5. Let the default battery level be 0. 186 switch (batteryLevel) { 187 case 5: 188 mLevel = BATTERY_LEVEL_FULL; 189 break; 190 case 4: 191 mLevel = BATTERY_LEVEL_4; 192 break; 193 case 3: 194 mLevel = BATTERY_LEVEL_3; 195 break; 196 case 2: 197 mLevel = BATTERY_LEVEL_2; 198 break; 199 case 1: 200 mLevel = BATTERY_LEVEL_1; 201 break; 202 case 0: 203 default: 204 mLevel = BATTERY_LEVEL_EMPTY; 205 } 206 207 if (Log.isLoggable(TAG, Log.DEBUG)) { 208 Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel); 209 } 210 211 notifyBatteryLevelChanged(); 212 } 213 214 /** 215 * Updates the display of the battery icon depending on the given connection state from the 216 * given {@link BluetoothDevice}. 217 */ updateBatteryIcon(BluetoothDevice device, int newState)218 private void updateBatteryIcon(BluetoothDevice device, int newState) { 219 if (newState == BluetoothProfile.STATE_CONNECTED) { 220 if (Log.isLoggable(TAG, Log.DEBUG)) { 221 Log.d(TAG, "Device connected"); 222 } 223 224 if (mBatteryViewHandler != null) { 225 mBatteryViewHandler.showBatteryView(); 226 } 227 228 if (mBluetoothHeadsetClient == null || device == null) { 229 return; 230 } 231 232 // Check if battery information is available and immediately update. 233 Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device); 234 if (featuresBundle == null) { 235 return; 236 } 237 238 int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL, 239 INVALID_BATTERY_LEVEL); 240 updateBatteryLevel(batteryLevel); 241 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { 242 if (Log.isLoggable(TAG, Log.DEBUG)) { 243 Log.d(TAG, "Device disconnected"); 244 } 245 246 if (mBatteryViewHandler != null) { 247 mBatteryViewHandler.hideBatteryView(); 248 } 249 } 250 } 251 252 @Override dispatchDemoCommand(String command, Bundle args)253 public void dispatchDemoCommand(String command, Bundle args) { 254 // TODO: Car demo mode. 255 } 256 257 @Override isPluggedIn()258 public boolean isPluggedIn() { 259 return true; 260 } 261 262 @Override isPowerSave()263 public boolean isPowerSave() { 264 // Power save is not valid for the car, so always return false. 265 return false; 266 } 267 268 @Override isAodPowerSave()269 public boolean isAodPowerSave() { 270 return false; 271 } 272 notifyBatteryLevelChanged()273 private void notifyBatteryLevelChanged() { 274 for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) { 275 mChangeCallbacks.get(i) 276 .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */); 277 } 278 } 279 280 /** 281 * An interface indicating the container of a View that will display what the information 282 * in the {@link CarBatteryController}. 283 */ 284 public interface BatteryViewHandler { 285 /** Hides the battery view. */ hideBatteryView()286 void hideBatteryView(); 287 288 /** Shows the battery view. */ showBatteryView()289 void showBatteryView(); 290 } 291 292 } 293