• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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