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