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