chromium/services/device/battery/android/java/src/org/chromium/device/battery/BatteryStatusManager.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.device.battery;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.task.AsyncTask;
import org.chromium.device.mojom.BatteryStatus;

/**
 * Data source for battery status information. This class registers for battery status notifications
 * from the system and calls the callback passed on construction whenever a notification is
 * received.
 */
class BatteryStatusManager {
    private static final String TAG = "BatteryStatusManager";

    interface BatteryStatusCallback {
        void onBatteryStatusChanged(BatteryStatus batteryStatus);
    }

    private final BatteryStatusCallback mCallback;
    private final IntentFilter mFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    private final BroadcastReceiver mReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    BatteryStatusManager.this.onReceive(intent);
                }
            };
    private AndroidBatteryManagerWrapper mAndroidBatteryManager;
    private boolean mEnabled;

    @VisibleForTesting
    static class AndroidBatteryManagerWrapper {
        private final BatteryManager mBatteryManager;

        protected AndroidBatteryManagerWrapper(BatteryManager batteryManager) {
            mBatteryManager = batteryManager;
        }

        public int getIntProperty(int id) {
            return mBatteryManager.getIntProperty(id);
        }
    }

    private BatteryStatusManager(
            BatteryStatusCallback callback, @Nullable AndroidBatteryManagerWrapper batteryManager) {
        mCallback = callback;
        mAndroidBatteryManager = batteryManager;
    }

    BatteryStatusManager(BatteryStatusCallback callback) {
        this(
                callback,
                new AndroidBatteryManagerWrapper(
                        (BatteryManager)
                                ContextUtils.getApplicationContext()
                                        .getSystemService(Context.BATTERY_SERVICE)));
    }

    /**
     * Creates a BatteryStatusManager without the Galaxy Nexus workaround for consistency in
     * testing.
     */
    static BatteryStatusManager createBatteryStatusManagerForTesting(
            BatteryStatusCallback callback, @Nullable AndroidBatteryManagerWrapper batteryManager) {
        return new BatteryStatusManager(callback, batteryManager);
    }

    /**
     * Starts listening for intents.
     * @return True on success.
     */
    boolean start() {
        if (!mEnabled
                && ContextUtils.registerProtectedBroadcastReceiver(
                                ContextUtils.getApplicationContext(), mReceiver, mFilter)
                        != null) {
            // success
            mEnabled = true;
        }
        return mEnabled;
    }

    /** Stops listening to intents. */
    void stop() {
        if (mEnabled) {
            ContextUtils.getApplicationContext().unregisterReceiver(mReceiver);
            mEnabled = false;
        }
    }

    @VisibleForTesting
    void onReceive(Intent intent) {
        if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
            Log.e(TAG, "Unexpected intent.");
            return;
        }

        boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);
        int pluggedStatus = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);

        if (!present || pluggedStatus == -1) {
            // No battery or no plugged status: return default values.
            mCallback.onBatteryStatusChanged(new BatteryStatus());
            return;
        }

        int current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        int max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
        double level = (double) current / (double) max;
        if (level < 0 || level > 1) {
            // Sanity check, assume default value in this case.
            level = 1.0;
        }

        // Currently Android (below L) does not provide charging/discharging time, as a work-around
        // we could compute it manually based on the evolution of level delta.
        // TODO(timvolodine): add proper projection for chargingTime, dischargingTime
        // (see crbug.com/401553).
        boolean charging = pluggedStatus != 0;
        int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
        boolean batteryFull = status == BatteryManager.BATTERY_STATUS_FULL;
        double chargingTimeSeconds = (charging && batteryFull) ? 0 : Double.POSITIVE_INFINITY;
        double dischargingTimeSeconds = Double.POSITIVE_INFINITY;

        BatteryStatus batteryStatus = new BatteryStatus();
        batteryStatus.charging = charging;
        batteryStatus.chargingTime = chargingTimeSeconds;
        batteryStatus.dischargingTime = dischargingTimeSeconds;
        batteryStatus.level = level;

        if (mAndroidBatteryManager != null) {
            // Doing an AsyncTask since querying the BatteryManager might be slow. In the past, it
            // has caused ANRs when executed on the main thread - see crbug.com/1163401.
            new AsyncTask<BatteryStatus>() {
                @Override
                protected BatteryStatus doInBackground() {
                    updateBatteryStatus(batteryStatus);
                    return batteryStatus;
                }

                @Override
                protected void onPostExecute(BatteryStatus batteryStatus) {
                    mCallback.onBatteryStatusChanged(batteryStatus);
                }
            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        } else {
            mCallback.onBatteryStatusChanged(batteryStatus);
        }
    }

    private void updateBatteryStatus(BatteryStatus batteryStatus) {
        double remainingCapacityRatio =
                mAndroidBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
                        / 100.0;
        double batteryCapacityMicroAh =
                mAndroidBatteryManager.getIntProperty(
                        BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER);
        double averageCurrentMicroA =
                mAndroidBatteryManager.getIntProperty(
                        BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE);

        if (batteryStatus.charging) {
            if (batteryStatus.chargingTime == Double.POSITIVE_INFINITY
                    && averageCurrentMicroA > 0) {
                double chargeFromEmptyHours = batteryCapacityMicroAh / averageCurrentMicroA;
                batteryStatus.chargingTime =
                        Math.ceil((1 - remainingCapacityRatio) * chargeFromEmptyHours * 3600.0);
            }
        } else {
            if (averageCurrentMicroA < 0) {
                double dischargeFromFullHours = batteryCapacityMicroAh / -averageCurrentMicroA;
                batteryStatus.dischargingTime =
                        Math.floor(remainingCapacityRatio * dischargeFromFullHours * 3600.0);
            }
        }
    }
}