/* * 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.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.Slog; import android.util.TimeUtils; import java.io.PrintWriter; /** * Implements heuristics to detect docking or undocking from a wireless charger. *

* 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. *

* 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). *

* To avoid spurious signals, we apply some special policies to wireless chargers. *

* 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. *

* 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. *

* 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. *

*/ final class WirelessChargerDetector { private static final String TAG = "WirelessChargerDetector"; private static final boolean DEBUG = false; // The minimum amount of time to spend watching the sensor before making // a determination of whether movement occurred. private static final long SETTLE_TIME_MILLIS = 800; // The sensor sampling interval. private static final int SAMPLING_INTERVAL_MILLIS = 50; // 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; private final Handler mHandler; // 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; // The time when detection was last performed. private long mDetectionStartTime; // 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 value of the first sample that was collected. private float mFirstSampleX, mFirstSampleY, mFirstSampleZ; // The value of the last sample that was collected. private float mLastSampleX, mLastSampleY, mLastSampleZ; public WirelessChargerDetector(SensorManager sensorManager, SuspendBlocker suspendBlocker, Handler handler) { mSensorManager = sensorManager; mSuspendBlocker = suspendBlocker; mHandler = handler; 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(" mDetectionStartTime=" + (mDetectionStartTime == 0 ? "0 (never)" : TimeUtils.formatUptime(mDetectionStartTime))); pw.println(" mMustUpdateRestPosition=" + mMustUpdateRestPosition); pw.println(" mTotalSamples=" + mTotalSamples); pw.println(" mMovingSamples=" + mMovingSamples); pw.println(" mFirstSampleX=" + mFirstSampleX + ", mFirstSampleY=" + mFirstSampleY + ", mFirstSampleZ=" + mFirstSampleZ); pw.println(" mLastSampleX=" + mLastSampleX + ", mLastSampleY=" + mLastSampleY + ", mLastSampleZ=" + mLastSampleZ); } } /** * 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, SAMPLING_INTERVAL_MILLIS * 1000)) { mSuspendBlocker.acquire(); mDetectionInProgress = true; mDetectionStartTime = SystemClock.uptimeMillis(); mTotalSamples = 0; mMovingSamples = 0; Message msg = Message.obtain(mHandler, mSensorTimeout); msg.setAsynchronous(true); mHandler.sendMessageDelayed(msg, SETTLE_TIME_MILLIS); } } } private void finishDetectionLocked() { if (mDetectionInProgress) { mSensorManager.unregisterListener(mListener); mHandler.removeCallbacks(mSensorTimeout); if (mMustUpdateRestPosition) { clearAtRestLocked(); if (mTotalSamples < MIN_SAMPLES) { Slog.w(TAG, "Wireless charger detector is broken. Only received " + mTotalSamples + " samples from the gravity sensor but we " + "need at least " + MIN_SAMPLES + " and we expect to see " + "about " + SETTLE_TIME_MILLIS / SAMPLING_INTERVAL_MILLIS + " on average."); } else if (mMovingSamples == 0) { mAtRest = true; mRestX = mLastSampleX; mRestY = mLastSampleY; mRestZ = mLastSampleZ; } mMustUpdateRestPosition = false; } if (DEBUG) { Slog.d(TAG, "New state: mAtRest=" + mAtRest + ", mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ + ", mTotalSamples=" + mTotalSamples + ", mMovingSamples=" + mMovingSamples); } mDetectionInProgress = false; mSuspendBlocker.release(); } } private void processSampleLocked(float x, float y, float z) { if (mDetectionInProgress) { mLastSampleX = x; mLastSampleY = y; mLastSampleZ = z; mTotalSamples += 1; if (mTotalSamples == 1) { // Save information about the first sample collected. 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(); } } } 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) { synchronized (mLock) { processSampleLocked(event.values[0], event.values[1], event.values[2]); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } }; private final Runnable mSensorTimeout = new Runnable() { @Override public void run() { synchronized (mLock) { finishDetectionLocked(); } } }; }