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