summaryrefslogtreecommitdiffstats
path: root/services/java/com/android/server/power/WirelessChargerDetector.java
blob: 38f5d7740842b909143b8b728a14e8e3332d1763 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*
 * 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.
 * <p>
 * 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.
 * </p><p>
 * 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).
 * </p><p>
 * To avoid spurious signals, we apply some special policies to wireless chargers.
 * </p><p>
 * 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.
 * </p><p>
 * 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.
 * </p><p>
 * 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.
 * </p>
 */
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();
            }
        }
    };
}