WirelessChargerDetector.java revision 20e287534396655af0d5912d8b272070ad24a93a
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.power;
18
19import android.hardware.Sensor;
20import android.hardware.SensorEvent;
21import android.hardware.SensorEventListener;
22import android.hardware.SensorManager;
23import android.os.BatteryManager;
24import android.os.SystemClock;
25import android.util.Slog;
26
27import java.io.PrintWriter;
28
29/**
30 * Implements heuristics to detect docking or undocking from a wireless charger.
31 * <p>
32 * Some devices have wireless charging circuits that are unable to detect when the
33 * device is resting on a wireless charger except when the device is actually
34 * receiving power from the charger.  The device may stop receiving power
35 * if the battery is already nearly full or if it is too hot.  As a result, we cannot
36 * always rely on the battery service wireless plug signal to accurately indicate
37 * whether the device has been docked or undocked from a wireless charger.
38 * </p><p>
39 * This is a problem because the power manager typically wakes up the screen and
40 * plays a tone when the device is docked in a wireless charger.  It is important
41 * for the system to suppress spurious docking and undocking signals because they
42 * can be intrusive for the user (especially if they cause a tone to be played
43 * late at night for no apparent reason).
44 * </p><p>
45 * To avoid spurious signals, we apply some special policies to wireless chargers.
46 * </p><p>
47 * 1. Don't wake the device when undocked from the wireless charger because
48 * it might be that the device is still resting on the wireless charger
49 * but is not receiving power anymore because the battery is full.
50 * Ideally we would wake the device if we could be certain that the user had
51 * picked it up from the wireless charger but due to hardware limitations we
52 * must be more conservative.
53 * </p><p>
54 * 2. Don't wake the device when docked on a wireless charger if the
55 * battery already appears to be mostly full.  This situation may indicate
56 * that the device was resting on the charger the whole time and simply
57 * wasn't receiving power because the battery was already full.  We can't tell
58 * whether the device was just placed on the charger or whether it has
59 * been there for half of the night slowly discharging until it reached
60 * the point where it needed to start charging again.  So we suppress docking
61 * signals that occur when the battery level is above a given threshold.
62 * </p><p>
63 * 3. Don't wake the device when docked on a wireless charger if it does
64 * not appear to have moved since it was last undocked because it may
65 * be that the prior undocking signal was spurious.  We use the gravity
66 * sensor to detect this case.
67 * </p>
68 */
69final class WirelessChargerDetector {
70    private static final String TAG = "WirelessChargerDetector";
71    private static final boolean DEBUG = false;
72
73    // Number of nanoseconds per millisecond.
74    private static final long NANOS_PER_MS = 1000000;
75
76    // The minimum amount of time to spend watching the sensor before making
77    // a determination of whether movement occurred.
78    private static final long SETTLE_TIME_NANOS = 500 * NANOS_PER_MS;
79
80    // The minimum number of samples that must be collected.
81    private static final int MIN_SAMPLES = 3;
82
83    // Upper bound on the battery charge percentage in order to consider turning
84    // the screen on when the device starts charging wirelessly.
85    private static final int WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT = 95;
86
87    // To detect movement, we compute the angle between the gravity vector
88    // at rest and the current gravity vector.  This field specifies the
89    // cosine of the maximum angle variance that we tolerate while at rest.
90    private static final double MOVEMENT_ANGLE_COS_THRESHOLD = Math.cos(5 * Math.PI / 180);
91
92    // Sanity thresholds for the gravity vector.
93    private static final double MIN_GRAVITY = SensorManager.GRAVITY_EARTH - 1.0f;
94    private static final double MAX_GRAVITY = SensorManager.GRAVITY_EARTH + 1.0f;
95
96    private final Object mLock = new Object();
97
98    private final SensorManager mSensorManager;
99    private final SuspendBlocker mSuspendBlocker;
100
101    // The gravity sensor, or null if none.
102    private Sensor mGravitySensor;
103
104    // Previously observed wireless power state.
105    private boolean mPoweredWirelessly;
106
107    // True if the device is thought to be at rest on a wireless charger.
108    private boolean mAtRest;
109
110    // The gravity vector most recently observed while at rest.
111    private float mRestX, mRestY, mRestZ;
112
113    /* These properties are only meaningful while detection is in progress. */
114
115    // True if detection is in progress.
116    // The suspend blocker is held while this is the case.
117    private boolean mDetectionInProgress;
118
119    // True if the rest position should be updated if at rest.
120    // Otherwise, the current rest position is simply checked and cleared if movement
121    // is detected but no new rest position is stored.
122    private boolean mMustUpdateRestPosition;
123
124    // The total number of samples collected.
125    private int mTotalSamples;
126
127    // The number of samples collected that showed evidence of not being at rest.
128    private int mMovingSamples;
129
130    // The time and value of the first sample that was collected.
131    private long mFirstSampleTime;
132    private float mFirstSampleX, mFirstSampleY, mFirstSampleZ;
133
134    // The time and value of the last sample that was collected (for debugging only).
135    private long mLastSampleTime;
136    private float mLastSampleX, mLastSampleY, mLastSampleZ;
137
138    public WirelessChargerDetector(SensorManager sensorManager,
139            SuspendBlocker suspendBlocker) {
140        mSensorManager = sensorManager;
141        mSuspendBlocker = suspendBlocker;
142
143        mGravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
144    }
145
146    public void dump(PrintWriter pw) {
147        synchronized (mLock) {
148            pw.println();
149            pw.println("Wireless Charger Detector State:");
150            pw.println("  mGravitySensor=" + mGravitySensor);
151            pw.println("  mPoweredWirelessly=" + mPoweredWirelessly);
152            pw.println("  mAtRest=" + mAtRest);
153            pw.println("  mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ);
154            pw.println("  mDetectionInProgress=" + mDetectionInProgress);
155            pw.println("  mMustUpdateRestPosition=" + mMustUpdateRestPosition);
156            pw.println("  mTotalSamples=" + mTotalSamples);
157            pw.println("  mMovingSamples=" + mMovingSamples);
158            pw.println("  mFirstSampleTime=" + mFirstSampleTime);
159            pw.println("  mFirstSampleX=" + mFirstSampleX
160                    + ", mFirstSampleY=" + mFirstSampleY + ", mFirstSampleZ=" + mFirstSampleZ);
161            pw.println("  mLastSampleTime=" + mLastSampleTime);
162            pw.println("  mLastSampleX=" + mLastSampleX
163                    + ", mLastSampleY=" + mLastSampleY + ", mLastSampleZ=" + mLastSampleZ);
164        }
165    }
166
167    /**
168     * Updates the charging state and returns true if docking was detected.
169     *
170     * @param isPowered True if the device is powered.
171     * @param plugType The current plug type.
172     * @param batteryLevel The current battery level.
173     * @return True if the device is determined to have just been docked on a wireless
174     * charger, after suppressing spurious docking or undocking signals.
175     */
176    public boolean update(boolean isPowered, int plugType, int batteryLevel) {
177        synchronized (mLock) {
178            final boolean wasPoweredWirelessly = mPoweredWirelessly;
179
180            if (isPowered && plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
181                // The device is receiving power from the wireless charger.
182                // Update the rest position asynchronously.
183                mPoweredWirelessly = true;
184                mMustUpdateRestPosition = true;
185                startDetectionLocked();
186            } else {
187                // The device may or may not be on the wireless charger depending on whether
188                // the unplug signal that we received was spurious.
189                mPoweredWirelessly = false;
190                if (mAtRest) {
191                    if (plugType != 0 && plugType != BatteryManager.BATTERY_PLUGGED_WIRELESS) {
192                        // The device was plugged into a new non-wireless power source.
193                        // It's safe to assume that it is no longer on the wireless charger.
194                        mMustUpdateRestPosition = false;
195                        clearAtRestLocked();
196                    } else {
197                        // The device may still be on the wireless charger but we don't know.
198                        // Check whether the device has remained at rest on the charger
199                        // so that we will know to ignore the next wireless plug event
200                        // if needed.
201                        startDetectionLocked();
202                    }
203                }
204            }
205
206            // Report that the device has been docked only if the device just started
207            // receiving power wirelessly, has a high enough battery level that we
208            // can be assured that charging was not delayed due to the battery previously
209            // having been full, and the device is not known to already be at rest
210            // on the wireless charger from earlier.
211            return mPoweredWirelessly && !wasPoweredWirelessly
212                    && batteryLevel < WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT
213                    && !mAtRest;
214        }
215    }
216
217    private void startDetectionLocked() {
218        if (!mDetectionInProgress && mGravitySensor != null) {
219            if (mSensorManager.registerListener(mListener, mGravitySensor,
220                    SensorManager.SENSOR_DELAY_UI)) {
221                mSuspendBlocker.acquire();
222                mDetectionInProgress = true;
223                mTotalSamples = 0;
224                mMovingSamples = 0;
225            }
226        }
227    }
228
229    private void processSample(long timeNanos, float x, float y, float z) {
230        synchronized (mLock) {
231            if (!mDetectionInProgress) {
232                return;
233            }
234
235            mLastSampleTime = timeNanos;
236            mLastSampleX = x;
237            mLastSampleY = y;
238            mLastSampleZ = z;
239
240            mTotalSamples += 1;
241            if (mTotalSamples == 1) {
242                // Save information about the first sample collected.
243                mFirstSampleTime = timeNanos;
244                mFirstSampleX = x;
245                mFirstSampleY = y;
246                mFirstSampleZ = z;
247            } else {
248                // Determine whether movement has occurred relative to the first sample.
249                if (hasMoved(mFirstSampleX, mFirstSampleY, mFirstSampleZ, x, y, z)) {
250                    mMovingSamples += 1;
251                }
252            }
253
254            // Clear the at rest flag if movement has occurred relative to the rest sample.
255            if (mAtRest && hasMoved(mRestX, mRestY, mRestZ, x, y, z)) {
256                if (DEBUG) {
257                    Slog.d(TAG, "No longer at rest: "
258                            + "mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ
259                            + ", x=" + x + ", y=" + y + ", z=" + z);
260                }
261                clearAtRestLocked();
262            }
263
264            // Save the result when done.
265            if (timeNanos - mFirstSampleTime >= SETTLE_TIME_NANOS
266                    && mTotalSamples >= MIN_SAMPLES) {
267                mSensorManager.unregisterListener(mListener);
268                if (mMustUpdateRestPosition) {
269                    if (mMovingSamples == 0) {
270                        mAtRest = true;
271                        mRestX = x;
272                        mRestY = y;
273                        mRestZ = z;
274                    } else {
275                        clearAtRestLocked();
276                    }
277                    mMustUpdateRestPosition = false;
278                }
279                mDetectionInProgress = false;
280                mSuspendBlocker.release();
281
282                if (DEBUG) {
283                    Slog.d(TAG, "New state: mAtRest=" + mAtRest
284                            + ", mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ
285                            + ", mTotalSamples=" + mTotalSamples
286                            + ", mMovingSamples=" + mMovingSamples);
287                }
288            }
289        }
290    }
291
292    private void clearAtRestLocked() {
293        mAtRest = false;
294        mRestX = 0;
295        mRestY = 0;
296        mRestZ = 0;
297    }
298
299    private static boolean hasMoved(float x1, float y1, float z1,
300            float x2, float y2, float z2) {
301        final double dotProduct = (x1 * x2) + (y1 * y2) + (z1 * z2);
302        final double mag1 = Math.sqrt((x1 * x1) + (y1 * y1) + (z1 * z1));
303        final double mag2 = Math.sqrt((x2 * x2) + (y2 * y2) + (z2 * z2));
304        if (mag1 < MIN_GRAVITY || mag1 > MAX_GRAVITY
305                || mag2 < MIN_GRAVITY || mag2 > MAX_GRAVITY) {
306            if (DEBUG) {
307                Slog.d(TAG, "Weird gravity vector: mag1=" + mag1 + ", mag2=" + mag2);
308            }
309            return true;
310        }
311        final boolean moved = (dotProduct < mag1 * mag2 * MOVEMENT_ANGLE_COS_THRESHOLD);
312        if (DEBUG) {
313            Slog.d(TAG, "Check: moved=" + moved
314                    + ", x1=" + x1 + ", y1=" + y1 + ", z1=" + z1
315                    + ", x2=" + x2 + ", y2=" + y2 + ", z2=" + z2
316                    + ", angle=" + (Math.acos(dotProduct / mag1 / mag2) * 180 / Math.PI)
317                    + ", dotProduct=" + dotProduct
318                    + ", mag1=" + mag1 + ", mag2=" + mag2);
319        }
320        return moved;
321    }
322
323    private final SensorEventListener mListener = new SensorEventListener() {
324        @Override
325        public void onSensorChanged(SensorEvent event) {
326            // We use SystemClock.elapsedRealtimeNanos() instead of event.timestamp because
327            // on some devices the sensor HAL may produce timestamps that are not monotonic.
328            processSample(SystemClock.elapsedRealtimeNanos(),
329                    event.values[0], event.values[1], event.values[2]);
330        }
331
332        @Override
333        public void onAccuracyChanged(Sensor sensor, int accuracy) {
334        }
335    };
336}
337