GeckoBatteryManager.java (7398B)
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko; 7 8 import android.content.BroadcastReceiver; 9 import android.content.Context; 10 import android.content.Intent; 11 import android.content.IntentFilter; 12 import android.os.BatteryManager; 13 import android.os.Build; 14 import android.os.SystemClock; 15 import android.util.Log; 16 import org.mozilla.gecko.annotation.WrapForJNI; 17 18 public class GeckoBatteryManager extends BroadcastReceiver { 19 private static final String LOGTAG = "GeckoBatteryManager"; 20 21 // Those constants should be keep in sync with the ones in: 22 // dom/battery/Constants.h 23 private static final double kDefaultLevel = 1.0; 24 private static final boolean kDefaultCharging = true; 25 private static final double kDefaultRemainingTime = 0.0; 26 private static final double kUnknownRemainingTime = -1.0; 27 28 private static long sLastLevelChange; 29 private static boolean sNotificationsEnabled; 30 private static double sLevel = kDefaultLevel; 31 private static boolean sCharging = kDefaultCharging; 32 private static double sRemainingTime = kDefaultRemainingTime; 33 34 private static final GeckoBatteryManager sInstance = new GeckoBatteryManager(); 35 36 private final IntentFilter mFilter; 37 private Context mApplicationContext; 38 private boolean mIsEnabled; 39 40 public static GeckoBatteryManager getInstance() { 41 return sInstance; 42 } 43 44 private GeckoBatteryManager() { 45 mFilter = new IntentFilter(); 46 mFilter.addAction(Intent.ACTION_BATTERY_CHANGED); 47 } 48 49 public synchronized void start(final Context context) { 50 if (mIsEnabled) { 51 Log.w(LOGTAG, "Already started!"); 52 return; 53 } 54 55 mApplicationContext = context.getApplicationContext(); 56 // registerReceiver will return null if registering fails. 57 final Intent intent = mApplicationContext.registerReceiver(this, mFilter); 58 if (intent == null) { 59 Log.e(LOGTAG, "Registering receiver failed"); 60 return; 61 } 62 63 mIsEnabled = true; 64 final double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); 65 final double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); 66 if (current == -1 || max == -1) { 67 Log.e(LOGTAG, "Failed to get battery level!"); 68 sLevel = kDefaultLevel; 69 } else { 70 sLevel = current / max; 71 } 72 } 73 74 public synchronized void stop() { 75 if (!mIsEnabled) { 76 Log.w(LOGTAG, "Already stopped!"); 77 return; 78 } 79 80 mApplicationContext.unregisterReceiver(this); 81 mApplicationContext = null; 82 mIsEnabled = false; 83 } 84 85 @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") 86 private static native void onBatteryChange(double level, boolean charging, double remainingTime); 87 88 @Override 89 public void onReceive(final Context context, final Intent intent) { 90 if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) { 91 Log.e(LOGTAG, "Got an unexpected intent!"); 92 return; 93 } 94 95 final boolean previousCharging = isCharging(); 96 final double previousLevel = getLevel(); 97 98 // NOTE: it might not be common (in 2012) but technically, Android can run 99 // on a device that has no battery so we want to make sure it's not the case 100 // before bothering checking for battery state. 101 // However, the Galaxy Nexus phone advertises itself as battery-less which 102 // force us to special-case the logic. 103 // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035 104 if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) 105 || Build.MODEL.equals("Galaxy Nexus")) { 106 final int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); 107 if (plugged == -1) { 108 sCharging = kDefaultCharging; 109 Log.e(LOGTAG, "Failed to get the plugged status!"); 110 } else { 111 // Likely, if plugged > 0, it's likely plugged and charging but the doc 112 // isn't clear about that. 113 sCharging = plugged != 0; 114 } 115 116 if (sCharging != previousCharging) { 117 sRemainingTime = kUnknownRemainingTime; 118 // The new remaining time is going to take some time to show up but 119 // it's the best way to show a not too wrong value. 120 sLastLevelChange = 0; 121 } 122 123 // We need two doubles because sLevel is a double. 124 final double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); 125 final double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); 126 if (current == -1 || max == -1) { 127 Log.e(LOGTAG, "Failed to get battery level!"); 128 sLevel = kDefaultLevel; 129 } else { 130 sLevel = current / max; 131 } 132 133 if (sLevel == 1.0 && sCharging) { 134 sRemainingTime = kDefaultRemainingTime; 135 } else if (sLevel != previousLevel) { 136 // Estimate remaining time. 137 if (sLastLevelChange != 0) { 138 // Use elapsedRealtime() because we want to track time across device sleeps. 139 final long currentTime = SystemClock.elapsedRealtime(); 140 final long dt = (currentTime - sLastLevelChange) / 1000; 141 final double dLevel = sLevel - previousLevel; 142 143 if (sCharging) { 144 if (dLevel < 0) { 145 sRemainingTime = kUnknownRemainingTime; 146 } else { 147 sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel)); 148 } 149 } else { 150 if (dLevel > 0) { 151 Log.w(LOGTAG, "When discharging, level should decrease!"); 152 sRemainingTime = kUnknownRemainingTime; 153 } else { 154 sRemainingTime = Math.round(dt / -dLevel * sLevel); 155 } 156 } 157 158 sLastLevelChange = currentTime; 159 } else { 160 // That's the first time we got an update, we can't do anything. 161 sLastLevelChange = SystemClock.elapsedRealtime(); 162 } 163 } 164 } else { 165 sLevel = kDefaultLevel; 166 sCharging = kDefaultCharging; 167 sRemainingTime = kDefaultRemainingTime; 168 } 169 170 /* 171 * We want to inform listeners if the following conditions are fulfilled: 172 * - we have at least one observer; 173 * - the charging state or the level has changed. 174 * 175 * Note: no need to check for a remaining time change given that it's only 176 * updated if there is a level change or a charging change. 177 * 178 * The idea is to prevent doing all the way to the DOM code in the child 179 * process to finally not send an event. 180 */ 181 if (sNotificationsEnabled 182 && (previousCharging != isCharging() || previousLevel != getLevel())) { 183 onBatteryChange(getLevel(), isCharging(), getRemainingTime()); 184 } 185 } 186 187 public static boolean isCharging() { 188 return sCharging; 189 } 190 191 public static double getLevel() { 192 return sLevel; 193 } 194 195 public static double getRemainingTime() { 196 return sRemainingTime; 197 } 198 199 public static void enableNotifications() { 200 sNotificationsEnabled = true; 201 } 202 203 public static void disableNotifications() { 204 sNotificationsEnabled = false; 205 } 206 207 public static double[] getCurrentInformation(final Context context) { 208 if (!getInstance().mIsEnabled) { 209 getInstance().start(context); 210 } 211 return new double[] {getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime()}; 212 } 213 }