/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; import android.os.Build; import android.os.SystemClock; import android.util.Log; import org.mozilla.gecko.annotation.WrapForJNI; public class GeckoBatteryManager extends BroadcastReceiver { private static final String LOGTAG = "GeckoBatteryManager"; // Those constants should be keep in sync with the ones in: // dom/battery/Constants.h private static final double kDefaultLevel = 1.0; private static final boolean kDefaultCharging = true; private static final double kDefaultRemainingTime = 0.0; private static final double kUnknownRemainingTime = -1.0; private static long sLastLevelChange; private static boolean sNotificationsEnabled; private static double sLevel = kDefaultLevel; private static boolean sCharging = kDefaultCharging; private static double sRemainingTime = kDefaultRemainingTime; private static final GeckoBatteryManager sInstance = new GeckoBatteryManager(); private final IntentFilter mFilter; private Context mApplicationContext; private boolean mIsEnabled; public static GeckoBatteryManager getInstance() { return sInstance; } private GeckoBatteryManager() { mFilter = new IntentFilter(); mFilter.addAction(Intent.ACTION_BATTERY_CHANGED); } public synchronized void start(final Context context) { if (mIsEnabled) { Log.w(LOGTAG, "Already started!"); return; } mApplicationContext = context.getApplicationContext(); // registerReceiver will return null if registering fails. final Intent intent = mApplicationContext.registerReceiver(this, mFilter); if (intent == null) { Log.e(LOGTAG, "Registering receiver failed"); return; } mIsEnabled = true; final double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); final double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); if (current == -1 || max == -1) { Log.e(LOGTAG, "Failed to get battery level!"); sLevel = kDefaultLevel; } else { sLevel = current / max; } } public synchronized void stop() { if (!mIsEnabled) { Log.w(LOGTAG, "Already stopped!"); return; } mApplicationContext.unregisterReceiver(this); mApplicationContext = null; mIsEnabled = false; } @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") private static native void onBatteryChange(double level, boolean charging, double remainingTime); @Override public void onReceive(final Context context, final Intent intent) { if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) { Log.e(LOGTAG, "Got an unexpected intent!"); return; } final boolean previousCharging = isCharging(); final double previousLevel = getLevel(); // NOTE: it might not be common (in 2012) but technically, Android can run // on a device that has no battery so we want to make sure it's not the case // before bothering checking for battery state. // However, the Galaxy Nexus phone advertises itself as battery-less which // force us to special-case the logic. // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035 if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) || Build.MODEL.equals("Galaxy Nexus")) { final int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); if (plugged == -1) { sCharging = kDefaultCharging; Log.e(LOGTAG, "Failed to get the plugged status!"); } else { // Likely, if plugged > 0, it's likely plugged and charging but the doc // isn't clear about that. sCharging = plugged != 0; } if (sCharging != previousCharging) { sRemainingTime = kUnknownRemainingTime; // The new remaining time is going to take some time to show up but // it's the best way to show a not too wrong value. sLastLevelChange = 0; } // We need two doubles because sLevel is a double. final double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); final double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); if (current == -1 || max == -1) { Log.e(LOGTAG, "Failed to get battery level!"); sLevel = kDefaultLevel; } else { sLevel = current / max; } if (sLevel == 1.0 && sCharging) { sRemainingTime = kDefaultRemainingTime; } else if (sLevel != previousLevel) { // Estimate remaining time. if (sLastLevelChange != 0) { // Use elapsedRealtime() because we want to track time across device sleeps. final long currentTime = SystemClock.elapsedRealtime(); final long dt = (currentTime - sLastLevelChange) / 1000; final double dLevel = sLevel - previousLevel; if (sCharging) { if (dLevel < 0) { sRemainingTime = kUnknownRemainingTime; } else { sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel)); } } else { if (dLevel > 0) { Log.w(LOGTAG, "When discharging, level should decrease!"); sRemainingTime = kUnknownRemainingTime; } else { sRemainingTime = Math.round(dt / -dLevel * sLevel); } } sLastLevelChange = currentTime; } else { // That's the first time we got an update, we can't do anything. sLastLevelChange = SystemClock.elapsedRealtime(); } } } else { sLevel = kDefaultLevel; sCharging = kDefaultCharging; sRemainingTime = kDefaultRemainingTime; } /* * We want to inform listeners if the following conditions are fulfilled: * - we have at least one observer; * - the charging state or the level has changed. * * Note: no need to check for a remaining time change given that it's only * updated if there is a level change or a charging change. * * The idea is to prevent doing all the way to the DOM code in the child * process to finally not send an event. */ if (sNotificationsEnabled && (previousCharging != isCharging() || previousLevel != getLevel())) { onBatteryChange(getLevel(), isCharging(), getRemainingTime()); } } public static boolean isCharging() { return sCharging; } public static double getLevel() { return sLevel; } public static double getRemainingTime() { return sRemainingTime; } public static void enableNotifications() { sNotificationsEnabled = true; } public static void disableNotifications() { sNotificationsEnabled = false; } public static double[] getCurrentInformation(final Context context) { if (!getInstance().mIsEnabled) { getInstance().start(context); } return new double[] {getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime()}; } }