For Auto, display battery status as that of a connected device.

For Android Auto Embedded, we would like to display the battery status
of a device that is connected via Bluetooth and not the battery status
of the device itself (which would not make sense in Auto).

To accomplish this, introduce a new CarBatteryController that only
monitors the status of battery via Bluetooth. Note that AAE is not
explicitly handling the battery icon that appears in the quick settings
because this will be hidden for Auto.

This CarBatteryController implements a new BatteryController interface.
What used to be the BatteryController has been moved to the
BatteryControllerImpl class.

Bug: 28002775
Change-Id: I2285bcbd3d207cdcc1ac5a98ec3685d4fff0f0d9
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
new file mode 100644
index 0000000..03b51c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarBatteryController.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.car;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.BatteryController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * A {@link BatteryController} that is specific to the Auto use-case. For Auto, the battery icon
+ * displays the battery status of a device that is connected via bluetooth and not the system's
+ * battery.
+ */
+public class CarBatteryController extends BroadcastReceiver implements BatteryController {
+    private static final String TAG = "CarBatteryController";
+
+    // According to the Bluetooth HFP 1.5 specification, battery levels are indicated by a
+    // value from 1-5, where these values represent the following:
+    // 0%% - 0, 1-25%% - 1, 26-50%% - 2, 51-75%% - 3, 76-99%% - 4, 100%% - 5
+    // As a result, set the level as the average within that range.
+    private static final int BATTERY_LEVEL_EMPTY = 0;
+    private static final int BATTERY_LEVEL_1 = 12;
+    private static final int BATTERY_LEVEL_2 = 28;
+    private static final int BATTERY_LEVEL_3 = 63;
+    private static final int BATTERY_LEVEL_4 = 87;
+    private static final int BATTERY_LEVEL_FULL = 100;
+
+    private static final int INVALID_BATTERY_LEVEL = -1;
+
+    private final Context mContext;
+
+    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+    private BluetoothHeadsetClient mBluetoothHeadsetClient;
+
+    private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
+
+    private int mLevel;
+
+    /**
+     * An interface indicating the container of a View that will display what the information
+     * in the {@link CarBatteryController}.
+     */
+    public interface BatteryViewHandler {
+        void hideBatteryView();
+        void showBatteryView();
+    }
+
+    private BatteryViewHandler mBatteryViewHandler;
+
+    public CarBatteryController(Context context) {
+        mContext = context;
+
+        mAdapter.getProfileProxy(context.getApplicationContext(), mHfpServiceListener,
+                BluetoothProfile.HEADSET_CLIENT);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CarBatteryController state:");
+        pw.print("    mLevel=");
+        pw.println(mLevel);
+    }
+
+    @Override
+    public void setPowerSaveMode(boolean powerSave) {
+        // No-op. No power save mode for the car.
+    }
+
+    @Override
+    public void addStateChangedCallback(BatteryController.BatteryStateChangeCallback cb) {
+        mChangeCallbacks.add(cb);
+
+        // There is no way to know if the phone is plugged in or charging via bluetooth, so pass
+        // false for these values.
+        cb.onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+        cb.onPowerSaveChanged(false /* isPowerSave */);
+    }
+
+    @Override
+    public void removeStateChangedCallback(BatteryController.BatteryStateChangeCallback cb) {
+        mChangeCallbacks.remove(cb);
+    }
+
+    public void addBatteryViewHandler(BatteryViewHandler batteryViewHandler) {
+        mBatteryViewHandler = batteryViewHandler;
+    }
+
+    public void startListening() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
+        mContext.registerReceiver(this, filter);
+    }
+
+    public void stopListening() {
+        mContext.unregisterReceiver(this);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "onReceive(). action: " + action);
+        }
+
+        if (BluetoothHeadsetClient.ACTION_AG_EVENT.equals(action)) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Received ACTION_AG_EVENT");
+            }
+
+            int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+                    INVALID_BATTERY_LEVEL);
+
+            updateBatteryLevel(batteryLevel);
+
+            if (batteryLevel != INVALID_BATTERY_LEVEL && mBatteryViewHandler != null) {
+                mBatteryViewHandler.showBatteryView();
+            }
+        } else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+                Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED event: "
+                        + oldState + " -> " + newState);
+
+            }
+            BluetoothDevice device =
+                    (BluetoothDevice)intent.getExtra(BluetoothDevice.EXTRA_DEVICE);
+            updateBatteryIcon(device, newState);
+        }
+    }
+
+    /**
+     * Converts the battery level to a percentage that can be displayed on-screen and notifies
+     * any {@link BatteryStateChangeCallback}s of this.
+     */
+    private void updateBatteryLevel(int batteryLevel) {
+        if (batteryLevel == INVALID_BATTERY_LEVEL) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Battery level invalid. Ignoring.");
+            }
+            return;
+        }
+
+        // The battery level is a value between 0-5. Let the default battery level be 0.
+        switch (batteryLevel) {
+            case 5:
+                mLevel = BATTERY_LEVEL_FULL;
+                break;
+            case 4:
+                mLevel = BATTERY_LEVEL_4;
+                break;
+            case 3:
+                mLevel = BATTERY_LEVEL_3;
+                break;
+            case 2:
+                mLevel = BATTERY_LEVEL_2;
+                break;
+            case 1:
+                mLevel = BATTERY_LEVEL_1;
+                break;
+            case 0:
+            default:
+                mLevel = BATTERY_LEVEL_EMPTY;
+        }
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Battery level: " + batteryLevel + "; setting mLevel as: " + mLevel);
+        }
+
+        notifyBatteryLevelChanged();
+    }
+
+    /**
+     * Updates the display of the battery icon depending on the given connection state from the
+     * given {@link BluetoothDevice}.
+     */
+    private void updateBatteryIcon(BluetoothDevice device, int newState) {
+        if (newState == BluetoothProfile.STATE_CONNECTED) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Device connected");
+            }
+
+            if (mBatteryViewHandler != null) {
+                mBatteryViewHandler.showBatteryView();
+            }
+
+            if (mBluetoothHeadsetClient == null || device == null) {
+                return;
+            }
+
+            // Check if battery information is available and immediately update.
+            Bundle featuresBundle = mBluetoothHeadsetClient.getCurrentAgEvents(device);
+            if (featuresBundle == null) {
+                return;
+            }
+
+            int batteryLevel = featuresBundle.getInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
+                    INVALID_BATTERY_LEVEL);
+            updateBatteryLevel(batteryLevel);
+        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Device disconnected");
+            }
+
+            if (mBatteryViewHandler != null) {
+                mBatteryViewHandler.hideBatteryView();
+            }
+        }
+    }
+
+    @Override
+    public boolean isPowerSave() {
+        // Power save is not valid for the car, so always return false.
+        return false;
+    }
+
+    private void notifyBatteryLevelChanged() {
+        for (int i = 0, size = mChangeCallbacks.size(); i < size; i++) {
+            mChangeCallbacks.get(i)
+                    .onBatteryLevelChanged(mLevel, false /* pluggedIn */, false /* charging */);
+        }
+    }
+
+    private final ServiceListener mHfpServiceListener = new ServiceListener() {
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            if (profile == BluetoothProfile.HEADSET_CLIENT) {
+                mBluetoothHeadsetClient = null;
+            }
+        }
+    };
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index 4add3cb..811687c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -22,37 +22,75 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.PixelFormat;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.RemoteException;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.view.ViewStub;
 import android.view.WindowManager;
-
+import com.android.systemui.BatteryMeterView;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.phone.PhoneStatusBarView;
+import com.android.systemui.statusbar.policy.BatteryController;
 
 /**
  * A status bar (and navigation bar) tailored for the automotive use case.
  */
-public class CarStatusBar extends PhoneStatusBar {
+public class CarStatusBar extends PhoneStatusBar implements
+        CarBatteryController.BatteryViewHandler {
+    private static final String TAG = "CarStatusBar";
+
     private TaskStackListenerImpl mTaskStackListener;
 
     private CarNavigationBarView mCarNavigationBar;
     private CarNavigationBarController mController;
     private FullscreenUserSwitcher mFullscreenUserSwitcher;
 
+    private CarBatteryController mCarBatteryController;
+    private BatteryMeterView mBatteryMeterView;
+
     @Override
     public void start() {
         super.start();
         mTaskStackListener = new TaskStackListenerImpl();
         SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskStackListener);
         registerPackageChangeReceivers();
+
+        mCarBatteryController.startListening();
+    }
+
+    @Override
+    public void destroy() {
+        mCarBatteryController.stopListening();
+        super.destroy();
+    }
+
+    @Override
+    protected PhoneStatusBarView makeStatusBarView() {
+        PhoneStatusBarView statusBarView = super.makeStatusBarView();
+
+        mBatteryMeterView = ((BatteryMeterView) statusBarView.findViewById(R.id.battery));
+
+        // By default, the BatteryMeterView should not be visible. It will be toggled visible
+        // when a device has connected by bluetooth.
+        mBatteryMeterView.setVisibility(View.GONE);
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "makeStatusBarView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        return statusBarView;
+    }
+
+    @Override
+    protected BatteryController createBatteryController() {
+        mCarBatteryController = new CarBatteryController(mContext);
+        mCarBatteryController.addBatteryViewHandler(this);
+        return mCarBatteryController;
     }
 
     @Override
@@ -85,6 +123,28 @@
 
     }
 
+    @Override
+    public void showBatteryView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "showBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        if (mBatteryMeterView != null) {
+            mBatteryMeterView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void hideBatteryView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "hideBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
+        }
+
+        if (mBatteryMeterView != null) {
+            mBatteryMeterView.setVisibility(View.GONE);
+        }
+    }
+
     private BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {