diff options
author | Sascha Prueter <saschap@google.com> | 2013-01-11 10:53:49 -0800 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2013-01-11 10:53:49 -0800 |
commit | b70d9df780b50f2aab2ff006e5cbfbd1a43ba332 (patch) | |
tree | 89ab898ac8ccc2be4887f8f939050bae5b7c6799 /services/java/com/android/server/power | |
parent | 1d6a7bd97340eab85920b37c3c83ee66fbebfed1 (diff) | |
parent | 1ec3c2ccedde5cd8ff6e94c9b1566a913d5e8db0 (diff) | |
download | frameworks_base-b70d9df780b50f2aab2ff006e5cbfbd1a43ba332.zip frameworks_base-b70d9df780b50f2aab2ff006e5cbfbd1a43ba332.tar.gz frameworks_base-b70d9df780b50f2aab2ff006e5cbfbd1a43ba332.tar.bz2 |
am 1ec3c2cc: am 28608bbb: am d692ea6d: am b5e4835e: Merge "Improve heuristics for detecting wireless chargers." into jb-mr1.1-dev
* commit '1ec3c2ccedde5cd8ff6e94c9b1566a913d5e8db0':
Improve heuristics for detecting wireless chargers.
Diffstat (limited to 'services/java/com/android/server/power')
3 files changed, 355 insertions, 40 deletions
diff --git a/services/java/com/android/server/power/DisplayPowerController.java b/services/java/com/android/server/power/DisplayPowerController.java index 724e126..b5010f2 100644 --- a/services/java/com/android/server/power/DisplayPowerController.java +++ b/services/java/com/android/server/power/DisplayPowerController.java @@ -344,7 +344,7 @@ final class DisplayPowerController { * Creates the display power controller. */ public DisplayPowerController(Looper looper, Context context, Notifier notifier, - LightsService lights, TwilightService twilight, + LightsService lights, TwilightService twilight, SensorManager sensorManager, DisplayManagerService displayManager, DisplayBlanker displayBlanker, Callbacks callbacks, Handler callbackHandler) { @@ -356,7 +356,7 @@ final class DisplayPowerController { mLights = lights; mTwilight = twilight; - mSensorManager = new SystemSensorManager(mHandler.getLooper()); + mSensorManager = sensorManager; mDisplayManager = displayManager; final Resources resources = context.getResources(); diff --git a/services/java/com/android/server/power/PowerManagerService.java b/services/java/com/android/server/power/PowerManagerService.java index ec82290..2652739 100644 --- a/services/java/com/android/server/power/PowerManagerService.java +++ b/services/java/com/android/server/power/PowerManagerService.java @@ -35,6 +35,8 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; +import android.hardware.SensorManager; +import android.hardware.SystemSensorManager; import android.net.Uri; import android.os.BatteryManager; import android.os.Binder; @@ -153,11 +155,6 @@ public final class PowerManagerService extends IPowerManager.Stub // Otherwise the user won't get much screen on time before dimming occurs. private static final float MAXIMUM_SCREEN_DIM_RATIO = 0.2f; - // Upper bound on the battery charge percentage in order to consider turning - // the screen on when the device starts charging wirelessly. - // See point of use for more details. - private static final int WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT = 95; - // The name of the boot animation service in init.rc. private static final String BOOT_ANIMATION_SERVICE = "bootanim"; @@ -179,6 +176,7 @@ public final class PowerManagerService extends IPowerManager.Stub private WindowManagerPolicy mPolicy; private Notifier mNotifier; private DisplayPowerController mDisplayPowerController; + private WirelessChargerDetector mWirelessChargerDetector; private SettingsObserver mSettingsObserver; private DreamManagerService mDreamManager; private LightsService.Light mAttentionLight; @@ -434,6 +432,8 @@ public final class PowerManagerService extends IPowerManager.Stub mScreenBrightnessSettingMaximum = pm.getMaximumScreenBrightnessSetting(); mScreenBrightnessSettingDefault = pm.getDefaultScreenBrightnessSetting(); + SensorManager sensorManager = new SystemSensorManager(mHandler.getLooper()); + // The notifier runs on the system server's main looper so as not to interfere // with the animations and other critical functions of the power manager. mNotifier = new Notifier(Looper.getMainLooper(), mContext, mBatteryStats, @@ -441,11 +441,14 @@ public final class PowerManagerService extends IPowerManager.Stub mScreenOnBlocker, mPolicy); // The display power controller runs on the power manager service's - // own handler thread. + // own handler thread to ensure timely operation. mDisplayPowerController = new DisplayPowerController(mHandler.getLooper(), - mContext, mNotifier, mLightsService, twilight, mDisplayManagerService, - mDisplayBlanker, mDisplayPowerControllerCallbacks, mHandler); + mContext, mNotifier, mLightsService, twilight, sensorManager, + mDisplayManagerService, mDisplayBlanker, + mDisplayPowerControllerCallbacks, mHandler); + mWirelessChargerDetector = new WirelessChargerDetector(sensorManager, + createSuspendBlockerLocked("PowerManagerService.WirelessChargerDetector")); mSettingsObserver = new SettingsObserver(mHandler); mAttentionLight = mLightsService.getLight(LightsService.LIGHT_ID_ATTENTION); @@ -1164,65 +1167,51 @@ public final class PowerManagerService extends IPowerManager.Stub if (wasPowered != mIsPowered || oldPlugType != mPlugType) { mDirty |= DIRTY_IS_POWERED; + // Update wireless dock detection state. + final boolean dockedOnWirelessCharger = mWirelessChargerDetector.update( + mIsPowered, mPlugType, mBatteryLevel); + // Treat plugging and unplugging the devices as a user activity. // Users find it disconcerting when they plug or unplug the device // and it shuts off right away. // Some devices also wake the device when plugged or unplugged because // they don't have a charging LED. final long now = SystemClock.uptimeMillis(); - if (shouldWakeUpWhenPluggedOrUnpluggedLocked(wasPowered, oldPlugType)) { + if (shouldWakeUpWhenPluggedOrUnpluggedLocked(wasPowered, oldPlugType, + dockedOnWirelessCharger)) { wakeUpNoUpdateLocked(now); } userActivityNoUpdateLocked( now, PowerManager.USER_ACTIVITY_EVENT_OTHER, 0, Process.SYSTEM_UID); // Tell the notifier whether wireless charging has started so that - // it can provide feedback to the user. Refer to - // shouldWakeUpWhenPluggedOrUnpluggedLocked for justification of the - // heuristics used here. - if (!wasPowered && mIsPowered - && mPlugType == BatteryManager.BATTERY_PLUGGED_WIRELESS - && mBatteryLevel < WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT) { + // it can provide feedback to the user. + if (dockedOnWirelessCharger) { mNotifier.onWirelessChargingStarted(); } } } } - private boolean shouldWakeUpWhenPluggedOrUnpluggedLocked(boolean wasPowered, int oldPlugType) { + private boolean shouldWakeUpWhenPluggedOrUnpluggedLocked( + boolean wasPowered, int oldPlugType, boolean dockedOnWirelessCharger) { // Don't wake when powered unless configured to do so. if (!mWakeUpWhenPluggedOrUnpluggedConfig) { return false; } - // FIXME: Need more accurate detection of wireless chargers. - // - // We are unable to accurately detect whether the device is resting on the - // charger unless it is actually receiving power. This causes us some grief - // because the device might not appear to be plugged into the wireless charger - // unless it actually charging. - // - // To avoid spuriously waking the screen, we apply a special policy to - // wireless chargers. - // - // 1. Don't wake the device when unplugged from wireless charger because - // it might be that the device is still resting on the wireless charger - // but is not receiving power anymore because the battery is full. - // - // 2. Don't wake the device when plugged into a wireless charger if the - // battery already appears to be mostly full. This situation may indicate - // that the device was resting on the charger the whole time and simply - // wasn't receiving power because the battery was full. We can't tell - // whether the device was just placed on the charger or whether it has - // been there for half of the night slowly discharging until it hit - // the point where it needed to start charging again. + // Don't wake when undocked from wireless charger. + // See WirelessChargerDetector for justification. if (wasPowered && !mIsPowered && oldPlugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) { return false; } + + // Don't wake when docked on wireless charger unless we are certain of it. + // See WirelessChargerDetector for justification. if (!wasPowered && mIsPowered && mPlugType == BatteryManager.BATTERY_PLUGGED_WIRELESS - && mBatteryLevel >= WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT) { + && !dockedOnWirelessCharger) { return false; } @@ -2209,6 +2198,7 @@ public final class PowerManagerService extends IPowerManager.Stub pw.println("POWER MANAGER (dumpsys power)\n"); final DisplayPowerController dpc; + final WirelessChargerDetector wcd; synchronized (mLock) { pw.println("Power Manager State:"); pw.println(" mDirty=0x" + Integer.toHexString(mDirty)); @@ -2291,11 +2281,16 @@ public final class PowerManagerService extends IPowerManager.Stub pw.println("Display Blanker: " + mDisplayBlanker); dpc = mDisplayPowerController; + wcd = mWirelessChargerDetector; } if (dpc != null) { dpc.dump(pw); } + + if (wcd != null) { + wcd.dump(pw); + } } private SuspendBlocker createSuspendBlockerLocked(String name) { diff --git a/services/java/com/android/server/power/WirelessChargerDetector.java b/services/java/com/android/server/power/WirelessChargerDetector.java new file mode 100644 index 0000000..ac6dc3e --- /dev/null +++ b/services/java/com/android/server/power/WirelessChargerDetector.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2013 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.server.power; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.BatteryManager; +import android.util.Slog; + +import java.io.PrintWriter; + +/** + * Implements heuristics to detect docking or undocking from a wireless charger. + * <p> + * Some devices have wireless charging circuits that are unable to detect when the + * device is resting on a wireless charger except when the device is actually + * receiving power from the charger. The device may stop receiving power + * if the battery is already nearly full or if it is too hot. As a result, we cannot + * always rely on the battery service wireless plug signal to accurately indicate + * whether the device has been docked or undocked from a wireless charger. + * </p><p> + * This is a problem because the power manager typically wakes up the screen and + * plays a tone when the device is docked in a wireless charger. It is important + * for the system to suppress spurious docking and undocking signals because they + * can be intrusive for the user (especially if they cause a tone to be played + * late at night for no apparent reason). + * </p><p> + * To avoid spurious signals, we apply some special policies to wireless chargers. + * </p><p> + * 1. Don't wake the device when undocked from the wireless charger because + * it might be that the device is still resting on the wireless charger + * but is not receiving power anymore because the battery is full. + * Ideally we would wake the device if we could be certain that the user had + * picked it up from the wireless charger but due to hardware limitations we + * must be more conservative. + * </p><p> + * 2. Don't wake the device when docked on a wireless charger if the + * battery already appears to be mostly full. This situation may indicate + * that the device was resting on the charger the whole time and simply + * wasn't receiving power because the battery was already full. We can't tell + * whether the device was just placed on the charger or whether it has + * been there for half of the night slowly discharging until it reached + * the point where it needed to start charging again. So we suppress docking + * signals that occur when the battery level is above a given threshold. + * </p><p> + * 3. Don't wake the device when docked on a wireless charger if it does + * not appear to have moved since it was last undocked because it may + * be that the prior undocking signal was spurious. We use the gravity + * sensor to detect this case. + * </p> + */ +final class WirelessChargerDetector { + private static final String TAG = "WirelessChargerDetector"; + private static final boolean DEBUG = false; + + // Number of nanoseconds per millisecond. + private static final long NANOS_PER_MS = 1000000; + + // The minimum amount of time to spend watching the sensor before making + // a determination of whether movement occurred. + private static final long SETTLE_TIME_NANOS = 500 * NANOS_PER_MS; + + // The minimum number of samples that must be collected. + private static final int MIN_SAMPLES = 3; + + // Upper bound on the battery charge percentage in order to consider turning + // the screen on when the device starts charging wirelessly. + private static final int WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT = 95; + + // To detect movement, we compute the angle between the gravity vector + // at rest and the current gravity vector. This field specifies the + // cosine of the maximum angle variance that we tolerate while at rest. + private static final double MOVEMENT_ANGLE_COS_THRESHOLD = Math.cos(5 * Math.PI / 180); + + // Sanity thresholds for the gravity vector. + private static final double MIN_GRAVITY = SensorManager.GRAVITY_EARTH - 1.0f; + private static final double MAX_GRAVITY = SensorManager.GRAVITY_EARTH + 1.0f; + + private final Object mLock = new Object(); + + private final SensorManager mSensorManager; + private final SuspendBlocker mSuspendBlocker; + + // The gravity sensor, or null if none. + private Sensor mGravitySensor; + + // Previously observed wireless power state. + private boolean mPoweredWirelessly; + + // True if the device is thought to be at rest on a wireless charger. + private boolean mAtRest; + + // The gravity vector most recently observed while at rest. + private float mRestX, mRestY, mRestZ; + + /* These properties are only meaningful while detection is in progress. */ + + // True if detection is in progress. + // The suspend blocker is held while this is the case. + private boolean mDetectionInProgress; + + // True if the rest position should be updated if at rest. + // Otherwise, the current rest position is simply checked and cleared if movement + // is detected but no new rest position is stored. + private boolean mMustUpdateRestPosition; + + // The total number of samples collected. + private int mTotalSamples; + + // The number of samples collected that showed evidence of not being at rest. + private int mMovingSamples; + + // The time and value of the first sample that was collected. + private long mFirstSampleTime; + private float mFirstSampleX, mFirstSampleY, mFirstSampleZ; + + public WirelessChargerDetector(SensorManager sensorManager, + SuspendBlocker suspendBlocker) { + mSensorManager = sensorManager; + mSuspendBlocker = suspendBlocker; + + mGravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); + } + + public void dump(PrintWriter pw) { + synchronized (mLock) { + pw.println(); + pw.println("Wireless Charger Detector State:"); + pw.println(" mGravitySensor=" + mGravitySensor); + pw.println(" mPoweredWirelessly=" + mPoweredWirelessly); + pw.println(" mAtRest=" + mAtRest); + pw.println(" mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ); + pw.println(" mDetectionInProgress=" + mDetectionInProgress); + pw.println(" mMustUpdateRestPosition=" + mMustUpdateRestPosition); + pw.println(" mTotalSamples=" + mTotalSamples); + pw.println(" mMovingSamples=" + mMovingSamples); + pw.println(" mFirstSampleTime=" + mFirstSampleTime); + pw.println(" mFirstSampleX=" + mFirstSampleX + + ", mFirstSampleY=" + mFirstSampleY + ", mFirstSampleZ=" + mFirstSampleZ); + } + } + + /** + * Updates the charging state and returns true if docking was detected. + * + * @param isPowered True if the device is powered. + * @param plugType The current plug type. + * @param batteryLevel The current battery level. + * @return True if the device is determined to have just been docked on a wireless + * charger, after suppressing spurious docking or undocking signals. + */ + public boolean update(boolean isPowered, int plugType, int batteryLevel) { + synchronized (mLock) { + final boolean wasPoweredWirelessly = mPoweredWirelessly; + + if (isPowered && plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) { + // The device is receiving power from the wireless charger. + // Update the rest position asynchronously. + mPoweredWirelessly = true; + mMustUpdateRestPosition = true; + startDetectionLocked(); + } else { + // The device may or may not be on the wireless charger depending on whether + // the unplug signal that we received was spurious. + mPoweredWirelessly = false; + if (mAtRest) { + if (plugType != 0 && plugType != BatteryManager.BATTERY_PLUGGED_WIRELESS) { + // The device was plugged into a new non-wireless power source. + // It's safe to assume that it is no longer on the wireless charger. + mMustUpdateRestPosition = false; + clearAtRestLocked(); + } else { + // The device may still be on the wireless charger but we don't know. + // Check whether the device has remained at rest on the charger + // so that we will know to ignore the next wireless plug event + // if needed. + startDetectionLocked(); + } + } + } + + // Report that the device has been docked only if the device just started + // receiving power wirelessly, has a high enough battery level that we + // can be assured that charging was not delayed due to the battery previously + // having been full, and the device is not known to already be at rest + // on the wireless charger from earlier. + return mPoweredWirelessly && !wasPoweredWirelessly + && batteryLevel < WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT + && !mAtRest; + } + } + + private void startDetectionLocked() { + if (!mDetectionInProgress && mGravitySensor != null) { + if (mSensorManager.registerListener(mListener, mGravitySensor, + SensorManager.SENSOR_DELAY_UI)) { + mSuspendBlocker.acquire(); + mDetectionInProgress = true; + mTotalSamples = 0; + mMovingSamples = 0; + } + } + } + + private void processSample(long timeNanos, float x, float y, float z) { + synchronized (mLock) { + if (!mDetectionInProgress) { + return; + } + + mTotalSamples += 1; + if (mTotalSamples == 1) { + // Save information about the first sample collected. + mFirstSampleTime = timeNanos; + mFirstSampleX = x; + mFirstSampleY = y; + mFirstSampleZ = z; + } else { + // Determine whether movement has occurred relative to the first sample. + if (hasMoved(mFirstSampleX, mFirstSampleY, mFirstSampleZ, x, y, z)) { + mMovingSamples += 1; + } + } + + // Clear the at rest flag if movement has occurred relative to the rest sample. + if (mAtRest && hasMoved(mRestX, mRestY, mRestZ, x, y, z)) { + if (DEBUG) { + Slog.d(TAG, "No longer at rest: " + + "mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ + + ", x=" + x + ", y=" + y + ", z=" + z); + } + clearAtRestLocked(); + } + + // Save the result when done. + if (timeNanos - mFirstSampleTime >= SETTLE_TIME_NANOS + && mTotalSamples >= MIN_SAMPLES) { + mSensorManager.unregisterListener(mListener); + if (mMustUpdateRestPosition) { + if (mMovingSamples == 0) { + mAtRest = true; + mRestX = x; + mRestY = y; + mRestZ = z; + } else { + clearAtRestLocked(); + } + mMustUpdateRestPosition = false; + } + mDetectionInProgress = false; + mSuspendBlocker.release(); + + if (DEBUG) { + Slog.d(TAG, "New state: mAtRest=" + mAtRest + + ", mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ + + ", mTotalSamples=" + mTotalSamples + + ", mMovingSamples=" + mMovingSamples); + } + } + } + } + + private void clearAtRestLocked() { + mAtRest = false; + mRestX = 0; + mRestY = 0; + mRestZ = 0; + } + + private static boolean hasMoved(float x1, float y1, float z1, + float x2, float y2, float z2) { + final double dotProduct = (x1 * x2) + (y1 * y2) + (z1 * z2); + final double mag1 = Math.sqrt((x1 * x1) + (y1 * y1) + (z1 * z1)); + final double mag2 = Math.sqrt((x2 * x2) + (y2 * y2) + (z2 * z2)); + if (mag1 < MIN_GRAVITY || mag1 > MAX_GRAVITY + || mag2 < MIN_GRAVITY || mag2 > MAX_GRAVITY) { + if (DEBUG) { + Slog.d(TAG, "Weird gravity vector: mag1=" + mag1 + ", mag2=" + mag2); + } + return true; + } + final boolean moved = (dotProduct < mag1 * mag2 * MOVEMENT_ANGLE_COS_THRESHOLD); + if (DEBUG) { + Slog.d(TAG, "Check: moved=" + moved + + ", x1=" + x1 + ", y1=" + y1 + ", z1=" + z1 + + ", x2=" + x2 + ", y2=" + y2 + ", z2=" + z2 + + ", angle=" + (Math.acos(dotProduct / mag1 / mag2) * 180 / Math.PI) + + ", dotProduct=" + dotProduct + + ", mag1=" + mag1 + ", mag2=" + mag2); + } + return moved; + } + + private final SensorEventListener mListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent event) { + processSample(event.timestamp, event.values[0], event.values[1], event.values[2]); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + }; +} |