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.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.os.Bundle;
28 import android.util.Log;
29 
30 import com.android.systemui.animation.Expandable;
31 import com.android.systemui.statusbar.policy.BatteryController;
32 
33 import java.io.PrintWriter;
34 import java.util.ArrayList;
35 
36 /**
37  * A {@link BatteryController} that is specific to the Auto use-case. For Auto, the battery icon
38  * displays the battery status of a device that is connected via bluetooth and not the system's
39  * battery.
40  */
41 public class CarBatteryController extends BroadcastReceiver implements BatteryController {
42     private static final String TAG = "CarBatteryController";
43 
44     private static final int INVALID_BATTERY_LEVEL = -1;
45 
46     private final Context mContext;
47 
48     private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
49     private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
50     private int mLevel;
51     private BatteryViewHandler mBatteryViewHandler;
52 
CarBatteryController(Context context)53     public CarBatteryController(Context context) {
54         mContext = context;
55 
56         if (mAdapter == null) {
57             return;
58         }
59     }
60 
61     @Override
dump(PrintWriter pw, String[] args)62     public void dump(PrintWriter pw, String[] args) {
63         pw.println("CarBatteryController state:");
64         pw.print("    mLevel=");
65         pw.println(mLevel);
66     }
67 
68     @Override
setPowerSaveMode(boolean powerSave)69     public void setPowerSaveMode(boolean powerSave) {
70         // No-op. No power save mode for the car.
71     }
72 
73     @Override
setPowerSaveMode(boolean powerSave, Expandable expandable)74     public void setPowerSaveMode(boolean powerSave, Expandable expandable) {
75         // No-op. No power save mode for the car.
76     }
77 
78     @Override
addCallback(BatteryController.BatteryStateChangeCallback cb)79     public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
80         mChangeCallbacks.add(cb);
81 
82         // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
83         // false for these values.
84         cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
85         cb.onPowerSaveChanged(false /* isPowerSave */);
86     }
87 
88     @Override
removeCallback(BatteryController.BatteryStateChangeCallback cb)89     public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
90         mChangeCallbacks.remove(cb);
91     }
92 
93     /** Sets {@link BatteryViewHandler}. */
addBatteryViewHandler(BatteryViewHandler batteryViewHandler)94     public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
95         mBatteryViewHandler = batteryViewHandler;
96     }
97 
98     /** Starts listening for bluetooth broadcast messages. */
startListening()99     public void startListening() {
100         IntentFilter filter = new IntentFilter();
101         filter.addAction(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED);
102         filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
103         mContext.registerReceiver(this, filter);
104     }
105 
106     /** Stops listening for bluetooth broadcast messages. */
stopListening()107     public void stopListening() {
108         mContext.unregisterReceiver(this);
109     }
110 
111     @Override
onReceive(Context context, Intent intent)112     public void onReceive(Context context, Intent intent) {
113         String action = intent.getAction();
114 
115         if (Log.isLoggable(TAG, Log.DEBUG)) {
116             Log.d(TAG, "onReceive(). action: " + action);
117         }
118 
119         if (BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED.equals(action)) {
120             BluetoothDevice device =
121                     (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
122             int batteryLevel = intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL,
123                     INVALID_BATTERY_LEVEL);
124 
125             if (Log.isLoggable(TAG, Log.DEBUG)) {
126                 Log.d(TAG, "Received ACTION_BATTERY_LEVEL_CHANGED event: device=" + device
127                         + ", level=" + batteryLevel);
128             }
129 
130             updateBatteryLevel(batteryLevel);
131 
132             if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
133                 mBatteryViewHandler.showBatteryView();
134             }
135         } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
136             int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
137 
138             if (Log.isLoggable(TAG, Log.DEBUG)) {
139                 int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
140                 Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
141                         + oldState + " -> " + newState);
142 
143             }
144             BluetoothDevice device =
145                     (BluetoothDevice) intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
146             updateBatteryIcon(device, newState);
147         }
148     }
149 
150     /**
151      * Verifies battery level is a valid percentage and notifies
152      * any {@link BatteryStateChangeCallback}s of this.
153      */
updateBatteryLevel(int batteryLevel)154     private void updateBatteryLevel(int batteryLevel) {
155         // Valid battery level is from 0 to 100, inclusive.
156         if (batteryLevel < 0 || batteryLevel > 100) {
157             if (Log.isLoggable(TAG, Log.DEBUG)) {
158                 Log.d(TAG, "Battery level invalid. Ignoring.");
159             }
160             return;
161         }
162 
163         mLevel = batteryLevel;
164 
165         if (Log.isLoggable(TAG, Log.DEBUG)) {
166             Log.d(TAG, "Battery level: " + mLevel);
167         }
168 
169         notifyBatteryLevelChanged();
170     }
171 
172     /**
173      * Updates the display of the battery icon depending on the given connection state from the
174      * given {@link BluetoothDevice}.
175      */
updateBatteryIcon(BluetoothDevice device, int newState)176     private void updateBatteryIcon(BluetoothDevice device, int newState) {
177         if (newState == BluetoothProfile.STATE_CONNECTED) {
178             if (Log.isLoggable(TAG, Log.DEBUG)) {
179                 Log.d(TAG, "Device connected");
180             }
181 
182             if (mBatteryViewHandler != null) {
183                 mBatteryViewHandler.showBatteryView();
184             }
185 
186             if (device == null) {
187                 return;
188             }
189 
190             int batteryLevel = device.getBatteryLevel();
191             updateBatteryLevel(batteryLevel);
192         } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
193             if (Log.isLoggable(TAG, Log.DEBUG)) {
194                 Log.d(TAG, "Device disconnected");
195             }
196 
197             if (mBatteryViewHandler != null) {
198                 mBatteryViewHandler.hideBatteryView();
199             }
200         }
201     }
202 
203     @Override
dispatchDemoCommand(String command, Bundle args)204     public void dispatchDemoCommand(String command, Bundle args) {
205         // TODO: Car demo mode.
206     }
207 
208     @Override
isPluggedIn()209     public boolean isPluggedIn() {
210         return true;
211     }
212 
213     @Override
isPowerSave()214     public boolean isPowerSave() {
215         // Power save is not valid for the car, so always return false.
216         return false;
217     }
218 
219     @Override
isAodPowerSave()220     public boolean isAodPowerSave() {
221         return false;
222     }
223 
notifyBatteryLevelChanged()224     private void notifyBatteryLevelChanged() {
225         for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
226             mChangeCallbacks.get(i)
227                     .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
228         }
229     }
230 
231     /**
232      * An interface indicating the container of a View that will display what the information
233      * in the {@link CarBatteryController}.
234      */
235     public interface BatteryViewHandler {
236         /** Hides the battery view. */
hideBatteryView()237         void hideBatteryView();
238 
239         /** Shows the battery view. */
showBatteryView()240         void showBatteryView();
241     }
242 
243 }
244