Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d692ea6d authored by Sascha Prueter's avatar Sascha Prueter Committed by Android Git Automerger
Browse files

am b5e4835e: Merge "Improve heuristics for detecting wireless chargers." into jb-mr1.1-dev

* commit 'b5e4835e':
  Improve heuristics for detecting wireless chargers.
parents 2d255b7e b5e4835e
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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();
+33 −38
Original line number Diff line number Diff line
@@ -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;
@@ -423,6 +421,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,
@@ -430,11 +430,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);

@@ -1139,65 +1142,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;
        }

@@ -2183,6 +2172,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));
@@ -2264,11 +2254,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) {
+320 −0
Original line number Diff line number Diff line
/*
 * 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) {
        }
    };
}