Home | History | Annotate | Download | only in car
      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 BluetoothHeadsetClient mBluetoothHeadsetClient;
     62 
     63     private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
     64 
     65     private int mLevel;
     66 
     67     /**
     68      * An interface indicating the container of a View that will display what the information
     69      * in the {@link CarBatteryController}.
     70      */
     71     public interface BatteryViewHandler {
     72         void hideBatteryView();
     73         void showBatteryView();
     74     }
     75 
     76     private BatteryViewHandler mBatteryViewHandler;
     77 
     78     public CarBatteryController(Context context) {
     79         mContext = context;
     80 
     81         if (mAdapter == null) {
     82            return;
     83         }
     84 
     85         mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
     86                 BluetoothProfile.HEADSET_CLIENT);
     87     }
     88 
     89     @Override
     90     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
     91         pw.println("CarBatteryController state:");
     92         pw.print("    mLevel=");
     93         pw.println(mLevel);
     94     }
     95 
     96     @Override
     97     public void setPowerSaveMode(boolean powerSave) {
     98         // No-op. No power save mode for the car.
     99     }
    100 
    101     @Override
    102     public void addCallback(BatteryController.BatteryStateChangeCallback cb) {
    103         mChangeCallbacks.add(cb);
    104 
    105         // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
    106         // false for these values.
    107         cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
    108         cb.onPowerSaveChanged(false /* isPowerSave */);
    109     }
    110 
    111     @Override
    112     public void removeCallback(BatteryController.BatteryStateChangeCallback cb) {
    113         mChangeCallbacks.remove(cb);
    114     }
    115 
    116     public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
    117         mBatteryViewHandler = batteryViewHandler;
    118     }
    119 
    120     public void startListening() {
    121         IntentFilter filter = new IntentFilter();
    122         filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
    123         filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
    124         mContext.registerReceiver(this, filter);
    125     }
    126 
    127     public void stopListening() {
    128         mContext.unregisterReceiver(this);
    129     }
    130 
    131     @Override
    132     public void onReceive(Context context, Intent intent) {
    133         String action = intent.getAction();
    134 
    135         if (Log.isLoggable(TAG, Log.DEBUG)) {
    136             Log.d(TAG, "onReceive(). action: " + action);
    137         }
    138 
    139         if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
    140             if (Log.isLoggable(TAG, Log.DEBUG)) {
    141                 Log.d(TAG, "Received ACTION_AG_EVENT");
    142             }
    143 
    144             int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
    145                     INVALID_BATTERY_LEVEL);
    146 
    147             updateBatteryLevel(batteryLevel);
    148 
    149             if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
    150                 mBatteryViewHandler.showBatteryView();
    151             }
    152         } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
    153             int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
    154 
    155             if (Log.isLoggable(TAG, Log.DEBUG)) {
    156                 int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
    157                 Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
    158                         + oldState + " -> " + newState);
    159 
    160             }
    161             BluetoothDevice device =
    162                     (BluetoothDevice)intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
    163             updateBatteryIcon(device, newState);
    164         }
    165     }
    166 
    167     /**
    168      * Converts the battery level to a percentage that can be displayed on-screen and notifies
    169      * any {@link BatteryStateChangeCallback}s of this.
    170      */
    171     private void updateBatteryLevel(int batteryLevel) {
    172         if (batteryLevel == INVALID_BATTERY_LEVEL) {
    173             if (Log.isLoggable(TAG, Log.DEBUG)) {
    174                 Log.d(TAG, "Battery level invalid. Ignoring.");
    175             }
    176             return;
    177         }
    178 
    179         // The battery level is a value between 0-5. Let the default battery level be 0.
    180         switch (batteryLevel) {
    181             case 5:
    182                 mLevel = BATTERY_LEVEL_FULL;
    183                 break;
    184             case 4:
    185                 mLevel = BATTERY_LEVEL_4;
    186                 break;
    187             case 3:
    188                 mLevel = BATTERY_LEVEL_3;
    189                 break;
    190             case 2:
    191                 mLevel = BATTERY_LEVEL_2;
    192                 break;
    193             case 1:
    194                 mLevel = BATTERY_LEVEL_1;
    195                 break;
    196             case 0:
    197             default:
    198                 mLevel = BATTERY_LEVEL_EMPTY;
    199         }
    200 
    201         if (Log.isLoggable(TAG, Log.DEBUG)) {
    202             Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel);
    203         }
    204 
    205         notifyBatteryLevelChanged();
    206     }
    207 
    208     /**
    209      * Updates the display of the battery icon depending on the given connection state from the
    210      * given {@link BluetoothDevice}.
    211      */
    212     private void updateBatteryIcon(BluetoothDevice device, int newState) {
    213         if (newState == BluetoothProfile.STATE_CONNECTED) {
    214             if (Log.isLoggable(TAG, Log.DEBUG)) {
    215                 Log.d(TAG, "Device connected");
    216             }
    217 
    218             if (mBatteryViewHandler != null) {
    219                 mBatteryViewHandler.showBatteryView();
    220             }
    221 
    222             if (mBluetoothHeadsetClient == null || device == null) {
    223                 return;
    224             }
    225 
    226             // Check if battery information is available and immediately update.
    227             Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
    228             if (featuresBundle == null) {
    229                 return;
    230             }
    231 
    232             int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
    233                     INVALID_BATTERY_LEVEL);
    234             updateBatteryLevel(batteryLevel);
    235         } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
    236             if (Log.isLoggable(TAG, Log.DEBUG)) {
    237                 Log.d(TAG, "Device disconnected");
    238             }
    239 
    240             if (mBatteryViewHandler != null) {
    241                 mBatteryViewHandler.hideBatteryView();
    242             }
    243         }
    244     }
    245 
    246     @Override
    247     public void dispatchDemoCommand(String command, Bundle args) {
    248         // TODO: Car demo mode.
    249     }
    250 
    251     @Override
    252     public boolean isPowerSave() {
    253         // Power save is not valid for the car, so always return false.
    254         return false;
    255     }
    256 
    257     private void notifyBatteryLevelChanged() {
    258         for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
    259             mChangeCallbacks.get(i)
    260                     .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
    261         }
    262     }
    263 
    264     private final ServiceListener mHfpServiceListener = new ServiceListener() {
    265         @Override
    266         public void onServiceConnected(int profile, BluetoothProfile proxy) {
    267             if (profile == BluetoothProfile.HEADSET_CLIENT) {
    268                 mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
    269             }
    270         }
    271 
    272         @Override
    273         public void onServiceDisconnected(int profile) {
    274             if (profile == BluetoothProfile.HEADSET_CLIENT) {
    275                 mBluetoothHeadsetClient = null;
    276             }
    277         }
    278     };
    279 
    280 }
    281