Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit da62fdcd authored by Anthony Chen's avatar Anthony Chen
Browse files

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
parent 0e89f980
Loading
Loading
Loading
Loading
+271 −0
Original line number Diff line number Diff line
/*
 * 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;
            }
        }
    };

}
+65 −5
Original line number Diff line number Diff line
@@ -22,37 +22,75 @@ import android.content.Context;
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 @@ public class CarStatusBar extends PhoneStatusBar {

    }

    @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) {
+6 −3
Original line number Diff line number Diff line
@@ -21,9 +21,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManager.StackId;
import android.app.ActivityManagerNative;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.app.Notification;
import android.app.PendingIntent;
@@ -147,6 +145,7 @@ import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChan
import com.android.systemui.statusbar.policy.AccessibilityController;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
import com.android.systemui.statusbar.policy.BatteryControllerImpl;
import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.CastControllerImpl;
@@ -826,7 +825,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
        // Other icons
        mLocationController = new LocationControllerImpl(mContext,
                mHandlerThread.getLooper()); // will post a notification
        mBatteryController = new BatteryController(mContext);
        mBatteryController = createBatteryController();
        mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() {
            @Override
            public void onPowerSaveChanged(boolean isPowerSave) {
@@ -943,6 +942,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
        return mStatusBarView;
    }

    protected BatteryController createBatteryController() {
        return new BatteryControllerImpl(mContext);
    }

    @Override
    protected void reInflateViews() {
        super.reInflateViews();
+24 −149
Original line number Diff line number Diff line
@@ -16,158 +16,33 @@

package com.android.systemui.statusbar.policy;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Handler;
import android.os.PowerManager;
import android.util.Log;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;

public class BatteryController extends BroadcastReceiver {
    private static final String TAG = "BatteryController";

    public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";

    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final ArrayList<BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>();
    private final PowerManager mPowerManager;
    private final Handler mHandler;

    private int mLevel;
    private boolean mPluggedIn;
    private boolean mCharging;
    private boolean mCharged;
    private boolean mPowerSave;
    private boolean mTestmode = false;

    public BatteryController(Context context) {
        mHandler = new Handler();
        mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
        filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
        filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING);
        filter.addAction(ACTION_LEVEL_TEST);
        context.registerReceiver(this, filter);

        updatePowerSave();
    }

    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("BatteryController state:");
        pw.print("  mLevel="); pw.println(mLevel);
        pw.print("  mPluggedIn="); pw.println(mPluggedIn);
        pw.print("  mCharging="); pw.println(mCharging);
        pw.print("  mCharged="); pw.println(mCharged);
        pw.print("  mPowerSave="); pw.println(mPowerSave);
    }

    public void setPowerSaveMode(boolean powerSave) {
        mPowerManager.setPowerSaveMode(powerSave);
    }

    public void addStateChangedCallback(BatteryStateChangeCallback cb) {
        mChangeCallbacks.add(cb);
        cb.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
        cb.onPowerSaveChanged(mPowerSave);
    }

    public void removeStateChangedCallback(BatteryStateChangeCallback cb) {
        mChangeCallbacks.remove(cb);
    }

    public void onReceive(final Context context, Intent intent) {
        final String action = intent.getAction();
        if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
            if (mTestmode && !intent.getBooleanExtra("testmode", false)) return;
            mLevel = (int)(100f
                    * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
                    / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
            mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;

            final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
                    BatteryManager.BATTERY_STATUS_UNKNOWN);
            mCharged = status == BatteryManager.BATTERY_STATUS_FULL;
            mCharging = mCharged || status == BatteryManager.BATTERY_STATUS_CHARGING;

            fireBatteryLevelChanged();
        } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
            updatePowerSave();
        } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)) {
            setPowerSave(intent.getBooleanExtra(PowerManager.EXTRA_POWER_SAVE_MODE, false));
        } else if (action.equals(ACTION_LEVEL_TEST)) {
            mTestmode = true;
            mHandler.post(new Runnable() {
                int curLevel = 0;
                int incr = 1;
                int saveLevel = mLevel;
                boolean savePlugged = mPluggedIn;
                Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED);
                @Override
                public void run() {
                    if (curLevel < 0) {
                        mTestmode = false;
                        dummy.putExtra("level", saveLevel);
                        dummy.putExtra("plugged", savePlugged);
                        dummy.putExtra("testmode", false);
                    } else {
                        dummy.putExtra("level", curLevel);
                        dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC
                                : 0);
                        dummy.putExtra("testmode", true);
                    }
                    context.sendBroadcast(dummy);

                    if (!mTestmode) return;

                    curLevel += incr;
                    if (curLevel == 100) {
                        incr *= -1;
                    }
                    mHandler.postDelayed(this, 200);
                }
            });
        }
    }

    public boolean isPowerSave() {
        return mPowerSave;
    }

    private void updatePowerSave() {
        setPowerSave(mPowerManager.isPowerSaveMode());
    }
public interface BatteryController {
    /**
     * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}.
     */
    void dump(FileDescriptor fd, PrintWriter pw, String[] args);

    private void setPowerSave(boolean powerSave) {
        if (powerSave == mPowerSave) return;
        mPowerSave = powerSave;
        if (DEBUG) Log.d(TAG, "Power save is " + (mPowerSave ? "on" : "off"));
        firePowerSaveChanged();
    }
    /**
     * Sets if the current device is in power save mode.
     */
    void setPowerSaveMode(boolean powerSave);

    private void fireBatteryLevelChanged() {
        final int N = mChangeCallbacks.size();
        for (int i = 0; i < N; i++) {
            mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging);
        }
    }
    /**
     * Returns {@code true} if the device is currently in power save mode.
     */
    boolean isPowerSave();

    private void firePowerSaveChanged() {
        final int N = mChangeCallbacks.size();
        for (int i = 0; i < N; i++) {
            mChangeCallbacks.get(i).onPowerSaveChanged(mPowerSave);
        }
    }
    void addStateChangedCallback(BatteryStateChangeCallback cb);
    void removeStateChangedCallback(BatteryStateChangeCallback cb);

    public interface BatteryStateChangeCallback {
    /**
     * A listener that will be notified whenever a change in battery level or power save mode
     * has occurred.
     */
    interface BatteryStateChangeCallback {
        void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging);
        void onPowerSaveChanged(boolean isPowerSave);
    }
+179 −0

File added.

Preview size limit exceeded, changes collapsed.