1 /*
2  * Copyright (C) 2016 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.statusbar.car;
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 
addBatteryViewHandler(BatteryViewHandler batteryViewHandler)119     public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
120         mBatteryViewHandler = batteryViewHandler;
121     }
122 
startListening()123     public void startListening() {
124         IntentFilter filter = new IntentFilter();
125         filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
126         filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
127         mContext.registerReceiver(this, filter);
128     }
129 
stopListening()130     public void stopListening() {
131         mContext.unregisterReceiver(this);
132     }
133 
134     @Override
onReceive(Context context, Intent intent)135     public void onReceive(Context context, Intent intent) {
136         String action = intent.getAction();
137 
138         if (Log.isLoggable(TAG, Log.DEBUG)) {
139             Log.d(TAG, "onReceive(). action: " + action);
140         }
141 
142         if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
143             if (Log.isLoggable(TAG, Log.DEBUG)) {
144                 Log.d(TAG, "Received ACTION_AG_EVENT");
145             }
146 
147             int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
148                     INVALID_BATTERY_LEVEL);
149 
150             updateBatteryLevel(batteryLevel);
151 
152             if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
153                 mBatteryViewHandler.showBatteryView();
154             }
155         } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
156             int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
157 
158             if (Log.isLoggable(TAG, Log.DEBUG)) {
159                 int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
160                 Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
161                         + oldState + " -> " + newState);
162 
163             }
164             BluetoothDevice device =
165                     (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
166             updateBatteryIcon(device, newState);
167         }
168     }
169 
170     /**
171      * Converts the battery level to a percentage that can be displayed on-screen and notifies
172      * any {@link BatteryStateChangeCallback}s of this.
173      */
updateBatteryLevel(int batteryLevel)174     private void updateBatteryLevel(int batteryLevel) {
175         if (batteryLevel == INVALID_BATTERY_LEVEL) {
176             if (Log.isLoggable(TAG, Log.DEBUG)) {
177                 Log.d(TAG, "Battery level invalid. Ignoring.");
178             }
179             return;
180         }
181 
182         // The battery level is a value between 0-5. Let the default battery level be 0.
183         switch (batteryLevel) {
184             case 5:
185                 mLevel = BATTERY_LEVEL_FULL;
186                 break;
187             case 4:
188                 mLevel = BATTERY_LEVEL_4;
189                 break;
190             case 3:
191                 mLevel = BATTERY_LEVEL_3;
192                 break;
193             case 2:
194                 mLevel = BATTERY_LEVEL_2;
195                 break;
196             case 1:
197                 mLevel = BATTERY_LEVEL_1;
198                 break;
199             case 0:
200             default:
201                 mLevel = BATTERY_LEVEL_EMPTY;
202         }
203 
204         if (Log.isLoggable(TAG, Log.DEBUG)) {
205             Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel);
206         }
207 
208         notifyBatteryLevelChanged();
209     }
210 
211     /**
212      * Updates the display of the battery icon depending on the given connection state from the
213      * given {@link BluetoothDevice}.
214      */
updateBatteryIcon(BluetoothDevice device, int newState)215     private void updateBatteryIcon(BluetoothDevice device, int newState) {
216         if (newState == BluetoothProfile.STATE_CONNECTED) {
217             if (Log.isLoggable(TAG, Log.DEBUG)) {
218                 Log.d(TAG, "Device connected");
219             }
220 
221             if (mBatteryViewHandler != null) {
222                 mBatteryViewHandler.showBatteryView();
223             }
224 
225             if (mBluetoothHeadsetClient == null || device == null) {
226                 return;
227             }
228 
229             // Check if battery information is available and immediately update.
230             Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
231             if (featuresBundle == null) {
232                 return;
233             }
234 
235             int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
236                     INVALID_BATTERY_LEVEL);
237             updateBatteryLevel(batteryLevel);
238         } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
239             if (Log.isLoggable(TAG, Log.DEBUG)) {
240                 Log.d(TAG, "Device disconnected");
241             }
242 
243             if (mBatteryViewHandler != null) {
244                 mBatteryViewHandler.hideBatteryView();
245             }
246         }
247     }
248 
249     @Override
dispatchDemoCommand(String command, Bundle args)250     public void dispatchDemoCommand(String command, Bundle args) {
251         // TODO: Car demo mode.
252     }
253 
254     @Override
isPowerSave()255     public boolean isPowerSave() {
256         // Power save is not valid for the car, so always return false.
257         return false;
258     }
259 
260     @Override
isAodPowerSave()261     public boolean isAodPowerSave() {
262         return false;
263     }
264 
notifyBatteryLevelChanged()265     private void notifyBatteryLevelChanged() {
266         for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
267             mChangeCallbacks.get(i)
268                     .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
269         }
270     }
271 
272     /**
273      * An interface indicating the container of a View that will display what the information
274      * in the {@link CarBatteryController}.
275      */
276     public interface BatteryViewHandler {
hideBatteryView()277         void hideBatteryView();
278 
showBatteryView()279         void showBatteryView();
280     }
281 
282 }
283