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