WirelessChargerDetector.java revision 958d0a503699840f8f4b85cafa62f3793c89ac73
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.Handler;
25import android.os.Message;
26import android.os.SystemClock;
27import android.service.power.WirelessChargerDetectorProto;
28import android.util.Slog;
29import android.util.TimeUtils;
30import android.util.proto.ProtoOutputStream;
31
32import java.io.PrintWriter;
33
34/**
35 * Implements heuristics to detect docking or undocking from a wireless charger.
36 * <p>
37 * Some devices have wireless charging circuits that are unable to detect when the
38 * device is resting on a wireless charger except when the device is actually
39 * receiving power from the charger.  The device may stop receiving power
40 * if the battery is already nearly full or if it is too hot.  As a result, we cannot
41 * always rely on the battery service wireless plug signal to accurately indicate
42 * whether the device has been docked or undocked from a wireless charger.
43 * </p><p>
44 * This is a problem because the power manager typically wakes up the screen and
45 * plays a tone when the device is docked in a wireless charger.  It is important
46 * for the system to suppress spurious docking and undocking signals because they
47 * can be intrusive for the user (especially if they cause a tone to be played
48 * late at night for no apparent reason).
49 * </p><p>
50 * To avoid spurious signals, we apply some special policies to wireless chargers.
51 * </p><p>
52 * 1. Don't wake the device when undocked from the wireless charger because
53 * it might be that the device is still resting on the wireless charger
54 * but is not receiving power anymore because the battery is full.
55 * Ideally we would wake the device if we could be certain that the user had
56 * picked it up from the wireless charger but due to hardware limitations we
57 * must be more conservative.
58 * </p><p>
59 * 2. Don't wake the device when docked on a wireless charger if the
60 * battery already appears to be mostly full.  This situation may indicate
61 * that the device was resting on the charger the whole time and simply
62 * wasn't receiving power because the battery was already full.  We can't tell
63 * whether the device was just placed on the charger or whether it has
64 * been there for half of the night slowly discharging until it reached
65 * the point where it needed to start charging again.  So we suppress docking
66 * signals that occur when the battery level is above a given threshold.
67 * </p><p>
68 * 3. Don't wake the device when docked on a wireless charger if it does
69 * not appear to have moved since it was last undocked because it may
70 * be that the prior undocking signal was spurious.  We use the gravity
71 * sensor to detect this case.
72 * </p>
73 */
74final class WirelessChargerDetector {
75    private static final String TAG = "WirelessChargerDetector";
76    private static final boolean DEBUG = false;
77
78    // The minimum amount of time to spend watching the sensor before making
79    // a determination of whether movement occurred.
80    private static final long SETTLE_TIME_MILLIS = 800;
81
82    // The sensor sampling interval.
83    private static final int SAMPLING_INTERVAL_MILLIS = 50;
84
85    // The minimum number of samples that must be collected.
86    private static final int MIN_SAMPLES = 3;
87
88    // Upper bound on the battery charge percentage in order to consider turning
89    // the screen on when the device starts charging wirelessly.
90    private static final int WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT = 95;
91
92    // To detect movement, we compute the angle between the gravity vector
93    // at rest and the current gravity vector.  This field specifies the
94    // cosine of the maximum angle variance that we tolerate while at rest.
95    private static final double MOVEMENT_ANGLE_COS_THRESHOLD = Math.cos(5 * Math.PI / 180);
96
97    // Sanity thresholds for the gravity vector.
98    private static final double MIN_GRAVITY = SensorManager.GRAVITY_EARTH - 1.0f;
99    private static final double MAX_GRAVITY = SensorManager.GRAVITY_EARTH + 1.0f;
100
101    private final Object mLock = new Object();
102
103    private final SensorManager mSensorManager;
104    private final SuspendBlocker mSuspendBlocker;
105    private final Handler mHandler;
106
107    // The gravity sensor, or null if none.
108    private Sensor mGravitySensor;
109
110    // Previously observed wireless power state.
111    private boolean mPoweredWirelessly;
112
113    // True if the device is thought to be at rest on a wireless charger.
114    private boolean mAtRest;
115
116    // The gravity vector most recently observed while at rest.
117    private float mRestX, mRestY, mRestZ;
118
119    /* These properties are only meaningful while detection is in progress. */
120
121    // True if detection is in progress.
122    // The suspend blocker is held while this is the case.
123    private boolean mDetectionInProgress;
124
125    // The time when detection was last performed.
126    private long mDetectionStartTime;
127
128    // True if the rest position should be updated if at rest.
129    // Otherwise, the current rest position is simply checked and cleared if movement
130    // is detected but no new rest position is stored.
131    private boolean mMustUpdateRestPosition;
132
133    // The total number of samples collected.
134    private int mTotalSamples;
135
136    // The number of samples collected that showed evidence of not being at rest.
137    private int mMovingSamples;
138
139    // The value of the first sample that was collected.
140    private float mFirstSampleX, mFirstSampleY, mFirstSampleZ;
141
142    // The value of the last sample that was collected.
143    private float mLastSampleX, mLastSampleY, mLastSampleZ;
144
145    public WirelessChargerDetector(SensorManager sensorManager,
146            SuspendBlocker suspendBlocker, Handler handler) {
147        mSensorManager = sensorManager;
148        mSuspendBlocker = suspendBlocker;
149        mHandler = handler;
150
151        mGravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
152    }
153
154    public void dump(PrintWriter pw) {
155        synchronized (mLock) {
156            pw.println();
157            pw.println("Wireless Charger Detector State:");
158            pw.println("  mGravitySensor=" + mGravitySensor);
159            pw.println("  mPoweredWirelessly=" + mPoweredWirelessly);
160            pw.println("  mAtRest=" + mAtRest);
161            pw.println("  mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ);
162            pw.println("  mDetectionInProgress=" + mDetectionInProgress);
163            pw.println("  mDetectionStartTime=" + (mDetectionStartTime == 0 ? "0 (never)"
164                    : TimeUtils.formatUptime(mDetectionStartTime)));
165            pw.println("  mMustUpdateRestPosition=" + mMustUpdateRestPosition);
166            pw.println("  mTotalSamples=" + mTotalSamples);
167            pw.println("  mMovingSamples=" + mMovingSamples);
168            pw.println("  mFirstSampleX=" + mFirstSampleX
169                    + ", mFirstSampleY=" + mFirstSampleY + ", mFirstSampleZ=" + mFirstSampleZ);
170            pw.println("  mLastSampleX=" + mLastSampleX
171                    + ", mLastSampleY=" + mLastSampleY + ", mLastSampleZ=" + mLastSampleZ);
172        }
173    }
174
175    public void writeToProto(ProtoOutputStream proto, long fieldId) {
176        final long wcdToken = proto.start(fieldId);
177        synchronized (mLock) {
178            proto.write(WirelessChargerDetectorProto.IS_POWERED_WIRELESSLY, mPoweredWirelessly);
179            proto.write(WirelessChargerDetectorProto.IS_AT_REST, mAtRest);
180
181            final long restVectorToken = proto.start(WirelessChargerDetectorProto.REST);
182            proto.write(WirelessChargerDetectorProto.VectorProto.X, mRestX);
183            proto.write(WirelessChargerDetectorProto.VectorProto.Y, mRestY);
184            proto.write(WirelessChargerDetectorProto.VectorProto.Z, mRestZ);
185            proto.end(restVectorToken);
186
187            proto.write(
188                    WirelessChargerDetectorProto.IS_DETECTION_IN_PROGRESS, mDetectionInProgress);
189            proto.write(WirelessChargerDetectorProto.DETECTION_START_TIME_MS, mDetectionStartTime);
190            proto.write(
191                    WirelessChargerDetectorProto.IS_MUST_UPDATE_REST_POSITION,
192                    mMustUpdateRestPosition);
193            proto.write(WirelessChargerDetectorProto.TOTAL_SAMPLES, mTotalSamples);
194            proto.write(WirelessChargerDetectorProto.MOVING_SAMPLES, mMovingSamples);
195
196            final long firstSampleVectorToken =
197                    proto.start(WirelessChargerDetectorProto.FIRST_SAMPLE);
198            proto.write(WirelessChargerDetectorProto.VectorProto.X, mFirstSampleX);
199            proto.write(WirelessChargerDetectorProto.VectorProto.Y, mFirstSampleY);
200            proto.write(WirelessChargerDetectorProto.VectorProto.Z, mFirstSampleZ);
201            proto.end(firstSampleVectorToken);
202
203            final long lastSampleVectorToken =
204                    proto.start(WirelessChargerDetectorProto.LAST_SAMPLE);
205            proto.write(WirelessChargerDetectorProto.VectorProto.X, mLastSampleX);
206            proto.write(WirelessChargerDetectorProto.VectorProto.Y, mLastSampleY);
207            proto.write(WirelessChargerDetectorProto.VectorProto.Z, mLastSampleZ);
208            proto.end(lastSampleVectorToken);
209        }
210        proto.end(wcdToken);
211    }
212
213    /**
214     * Updates the charging state and returns true if docking was detected.
215     *
216     * @param isPowered True if the device is powered.
217     * @param plugType The current plug type.
218     * @param batteryLevel The current battery level.
219     * @return True if the device is determined to have just been docked on a wireless
220     * charger, after suppressing spurious docking or undocking signals.
221     */
222    public boolean update(boolean isPowered, int plugType, int batteryLevel) {
223        synchronized (mLock) {
224            final boolean wasPoweredWirelessly = mPoweredWirelessly;
225
226            if (isPowered && plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
227                // The device is receiving power from the wireless charger.
228                // Update the rest position asynchronously.
229                mPoweredWirelessly = true;
230                mMustUpdateRestPosition = true;
231                startDetectionLocked();
232            } else {
233                // The device may or may not be on the wireless charger depending on whether
234                // the unplug signal that we received was spurious.
235                mPoweredWirelessly = false;
236                if (mAtRest) {
237                    if (plugType != 0 && plugType != BatteryManager.BATTERY_PLUGGED_WIRELESS) {
238                        // The device was plugged into a new non-wireless power source.
239                        // It's safe to assume that it is no longer on the wireless charger.
240                        mMustUpdateRestPosition = false;
241                        clearAtRestLocked();
242                    } else {
243                        // The device may still be on the wireless charger but we don't know.
244                        // Check whether the device has remained at rest on the charger
245                        // so that we will know to ignore the next wireless plug event
246                        // if needed.
247                        startDetectionLocked();
248                    }
249                }
250            }
251
252            // Report that the device has been docked only if the device just started
253            // receiving power wirelessly, has a high enough battery level that we
254            // can be assured that charging was not delayed due to the battery previously
255            // having been full, and the device is not known to already be at rest
256            // on the wireless charger from earlier.
257            return mPoweredWirelessly && !wasPoweredWirelessly
258                    && batteryLevel < WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT
259                    && !mAtRest;
260        }
261    }
262
263    private void startDetectionLocked() {
264        if (!mDetectionInProgress && mGravitySensor != null) {
265            if (mSensorManager.registerListener(mListener, mGravitySensor,
266                    SAMPLING_INTERVAL_MILLIS * 1000)) {
267                mSuspendBlocker.acquire();
268                mDetectionInProgress = true;
269                mDetectionStartTime = SystemClock.uptimeMillis();
270                mTotalSamples = 0;
271                mMovingSamples = 0;
272
273                Message msg = Message.obtain(mHandler, mSensorTimeout);
274                msg.setAsynchronous(true);
275                mHandler.sendMessageDelayed(msg, SETTLE_TIME_MILLIS);
276            }
277        }
278    }
279
280    private void finishDetectionLocked() {
281        if (mDetectionInProgress) {
282            mSensorManager.unregisterListener(mListener);
283            mHandler.removeCallbacks(mSensorTimeout);
284
285            if (mMustUpdateRestPosition) {
286                clearAtRestLocked();
287                if (mTotalSamples < MIN_SAMPLES) {
288                    Slog.w(TAG, "Wireless charger detector is broken.  Only received "
289                            + mTotalSamples + " samples from the gravity sensor but we "
290                            + "need at least " + MIN_SAMPLES + " and we expect to see "
291                            + "about " + SETTLE_TIME_MILLIS / SAMPLING_INTERVAL_MILLIS
292                            + " on average.");
293                } else if (mMovingSamples == 0) {
294                    mAtRest = true;
295                    mRestX = mLastSampleX;
296                    mRestY = mLastSampleY;
297                    mRestZ = mLastSampleZ;
298                }
299                mMustUpdateRestPosition = false;
300            }
301
302            if (DEBUG) {
303                Slog.d(TAG, "New state: mAtRest=" + mAtRest
304                        + ", mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ
305                        + ", mTotalSamples=" + mTotalSamples
306                        + ", mMovingSamples=" + mMovingSamples);
307            }
308
309            mDetectionInProgress = false;
310            mSuspendBlocker.release();
311        }
312    }
313
314    private void processSampleLocked(float x, float y, float z) {
315        if (mDetectionInProgress) {
316            mLastSampleX = x;
317            mLastSampleY = y;
318            mLastSampleZ = z;
319
320            mTotalSamples += 1;
321            if (mTotalSamples == 1) {
322                // Save information about the first sample collected.
323                mFirstSampleX = x;
324                mFirstSampleY = y;
325                mFirstSampleZ = z;
326            } else {
327                // Determine whether movement has occurred relative to the first sample.
328                if (hasMoved(mFirstSampleX, mFirstSampleY, mFirstSampleZ, x, y, z)) {
329                    mMovingSamples += 1;
330                }
331            }
332
333            // Clear the at rest flag if movement has occurred relative to the rest sample.
334            if (mAtRest && hasMoved(mRestX, mRestY, mRestZ, x, y, z)) {
335                if (DEBUG) {
336                    Slog.d(TAG, "No longer at rest: "
337                            + "mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ
338                            + ", x=" + x + ", y=" + y + ", z=" + z);
339                }
340                clearAtRestLocked();
341            }
342        }
343    }
344
345    private void clearAtRestLocked() {
346        mAtRest = false;
347        mRestX = 0;
348        mRestY = 0;
349        mRestZ = 0;
350    }
351
352    private static boolean hasMoved(float x1, float y1, float z1,
353            float x2, float y2, float z2) {
354        final double dotProduct = (x1 * x2) + (y1 * y2) + (z1 * z2);
355        final double mag1 = Math.sqrt((x1 * x1) + (y1 * y1) + (z1 * z1));
356        final double mag2 = Math.sqrt((x2 * x2) + (y2 * y2) + (z2 * z2));
357        if (mag1 < MIN_GRAVITY || mag1 > MAX_GRAVITY
358                || mag2 < MIN_GRAVITY || mag2 > MAX_GRAVITY) {
359            if (DEBUG) {
360                Slog.d(TAG, "Weird gravity vector: mag1=" + mag1 + ", mag2=" + mag2);
361            }
362            return true;
363        }
364        final boolean moved = (dotProduct < mag1 * mag2 * MOVEMENT_ANGLE_COS_THRESHOLD);
365        if (DEBUG) {
366            Slog.d(TAG, "Check: moved=" + moved
367                    + ", x1=" + x1 + ", y1=" + y1 + ", z1=" + z1
368                    + ", x2=" + x2 + ", y2=" + y2 + ", z2=" + z2
369                    + ", angle=" + (Math.acos(dotProduct / mag1 / mag2) * 180 / Math.PI)
370                    + ", dotProduct=" + dotProduct
371                    + ", mag1=" + mag1 + ", mag2=" + mag2);
372        }
373        return moved;
374    }
375
376    private final SensorEventListener mListener = new SensorEventListener() {
377        @Override
378        public void onSensorChanged(SensorEvent event) {
379            synchronized (mLock) {
380                processSampleLocked(event.values[0], event.values[1], event.values[2]);
381            }
382        }
383
384        @Override
385        public void onAccuracyChanged(Sensor sensor, int accuracy) {
386        }
387    };
388
389    private final Runnable mSensorTimeout = new Runnable() {
390        @Override
391        public void run() {
392            synchronized (mLock) {
393                finishDetectionLocked();
394            }
395        }
396    };
397}
398