• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.server.power;
18 
19 import android.hardware.Sensor;
20 import android.hardware.SensorEvent;
21 import android.hardware.SensorEventListener;
22 import android.hardware.SensorManager;
23 import android.os.BatteryManager;
24 import android.os.Handler;
25 import android.os.Message;
26 import android.os.SystemClock;
27 import android.util.Slog;
28 import android.util.TimeUtils;
29 import android.util.proto.ProtoOutputStream;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import java.io.PrintWriter;
34 
35 /**
36  * Implements heuristics to detect docking or undocking from a wireless charger.
37  * <p>
38  * Some devices have wireless charging circuits that are unable to detect when the
39  * device is resting on a wireless charger except when the device is actually
40  * receiving power from the charger.  The device may stop receiving power
41  * if the battery is already nearly full or if it is too hot.  As a result, we cannot
42  * always rely on the battery service wireless plug signal to accurately indicate
43  * whether the device has been docked or undocked from a wireless charger.
44  * </p><p>
45  * This is a problem because the power manager typically wakes up the screen and
46  * plays a tone when the device is docked in a wireless charger.  It is important
47  * for the system to suppress spurious docking and undocking signals because they
48  * can be intrusive for the user (especially if they cause a tone to be played
49  * late at night for no apparent reason).
50  * </p><p>
51  * To avoid spurious signals, we apply some special policies to wireless chargers.
52  * </p><p>
53  * 1. Don't wake the device when undocked from the wireless charger because
54  * it might be that the device is still resting on the wireless charger
55  * but is not receiving power anymore because the battery is full.
56  * Ideally we would wake the device if we could be certain that the user had
57  * picked it up from the wireless charger but due to hardware limitations we
58  * must be more conservative.
59  * </p><p>
60  * 2. Don't wake the device when docked on a wireless charger if the
61  * battery already appears to be mostly full.  This situation may indicate
62  * that the device was resting on the charger the whole time and simply
63  * wasn't receiving power because the battery was already full.  We can't tell
64  * whether the device was just placed on the charger or whether it has
65  * been there for half of the night slowly discharging until it reached
66  * the point where it needed to start charging again.  So we suppress docking
67  * signals that occur when the battery level is above a given threshold.
68  * </p><p>
69  * 3. Don't wake the device when docked on a wireless charger if it does
70  * not appear to have moved since it was last undocked because it may
71  * be that the prior undocking signal was spurious.  We use the gravity
72  * sensor to detect this case.
73  * </p>
74  */
75 @VisibleForTesting
76 public class WirelessChargerDetector {
77     private static final String TAG = "WirelessChargerDetector";
78     private static final boolean DEBUG = false;
79 
80     // The minimum amount of time to spend watching the sensor before making
81     // a determination of whether movement occurred.
82     private static final long SETTLE_TIME_MILLIS = 800;
83 
84     // The sensor sampling interval.
85     private static final int SAMPLING_INTERVAL_MILLIS = 50;
86 
87     // The minimum number of samples that must be collected.
88     private static final int MIN_SAMPLES = 3;
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     // Validity 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 
WirelessChargerDetector(SensorManager sensorManager, SuspendBlocker suspendBlocker, Handler handler)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 
dump(PrintWriter pw)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 
dumpDebug(ProtoOutputStream proto, long fieldId)173     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
174         final long wcdToken = proto.start(fieldId);
175         synchronized (mLock) {
176             proto.write(WirelessChargerDetectorProto.IS_POWERED_WIRELESSLY, mPoweredWirelessly);
177             proto.write(WirelessChargerDetectorProto.IS_AT_REST, mAtRest);
178 
179             final long restVectorToken = proto.start(WirelessChargerDetectorProto.REST);
180             proto.write(WirelessChargerDetectorProto.VectorProto.X, mRestX);
181             proto.write(WirelessChargerDetectorProto.VectorProto.Y, mRestY);
182             proto.write(WirelessChargerDetectorProto.VectorProto.Z, mRestZ);
183             proto.end(restVectorToken);
184 
185             proto.write(
186                     WirelessChargerDetectorProto.IS_DETECTION_IN_PROGRESS, mDetectionInProgress);
187             proto.write(WirelessChargerDetectorProto.DETECTION_START_TIME_MS, mDetectionStartTime);
188             proto.write(
189                     WirelessChargerDetectorProto.IS_MUST_UPDATE_REST_POSITION,
190                     mMustUpdateRestPosition);
191             proto.write(WirelessChargerDetectorProto.TOTAL_SAMPLES, mTotalSamples);
192             proto.write(WirelessChargerDetectorProto.MOVING_SAMPLES, mMovingSamples);
193 
194             final long firstSampleVectorToken =
195                     proto.start(WirelessChargerDetectorProto.FIRST_SAMPLE);
196             proto.write(WirelessChargerDetectorProto.VectorProto.X, mFirstSampleX);
197             proto.write(WirelessChargerDetectorProto.VectorProto.Y, mFirstSampleY);
198             proto.write(WirelessChargerDetectorProto.VectorProto.Z, mFirstSampleZ);
199             proto.end(firstSampleVectorToken);
200 
201             final long lastSampleVectorToken =
202                     proto.start(WirelessChargerDetectorProto.LAST_SAMPLE);
203             proto.write(WirelessChargerDetectorProto.VectorProto.X, mLastSampleX);
204             proto.write(WirelessChargerDetectorProto.VectorProto.Y, mLastSampleY);
205             proto.write(WirelessChargerDetectorProto.VectorProto.Z, mLastSampleZ);
206             proto.end(lastSampleVectorToken);
207         }
208         proto.end(wcdToken);
209     }
210 
211     /**
212      * Updates the charging state and returns true if docking was detected.
213      *
214      * @param isPowered True if the device is powered.
215      * @param plugType The current plug type.
216      * @return True if the device is determined to have just been docked on a wireless
217      * charger, after suppressing spurious docking or undocking signals.
218      */
update(boolean isPowered, int plugType)219     public boolean update(boolean isPowered, int plugType) {
220         synchronized (mLock) {
221             final boolean wasPoweredWirelessly = mPoweredWirelessly;
222 
223             if (isPowered && plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
224                 // The device is receiving power from the wireless charger.
225                 // Update the rest position asynchronously.
226                 mPoweredWirelessly = true;
227                 mMustUpdateRestPosition = true;
228                 startDetectionLocked();
229             } else {
230                 // The device may or may not be on the wireless charger depending on whether
231                 // the unplug signal that we received was spurious.
232                 mPoweredWirelessly = false;
233                 if (mAtRest) {
234                     if (plugType != 0 && plugType != BatteryManager.BATTERY_PLUGGED_WIRELESS) {
235                         // The device was plugged into a new non-wireless power source.
236                         // It's safe to assume that it is no longer on the wireless charger.
237                         mMustUpdateRestPosition = false;
238                         clearAtRestLocked();
239                     } else {
240                         // The device may still be on the wireless charger but we don't know.
241                         // Check whether the device has remained at rest on the charger
242                         // so that we will know to ignore the next wireless plug event
243                         // if needed.
244                         startDetectionLocked();
245                     }
246                 }
247             }
248 
249             // Report that the device has been docked only if the device just started
250             // receiving power wirelessly and the device is not known to already be at rest
251             // on the wireless charger from earlier.
252             return mPoweredWirelessly && !wasPoweredWirelessly && !mAtRest;
253         }
254     }
255 
startDetectionLocked()256     private void startDetectionLocked() {
257         if (!mDetectionInProgress && mGravitySensor != null) {
258             if (mSensorManager.registerListener(mListener, mGravitySensor,
259                     SAMPLING_INTERVAL_MILLIS * 1000)) {
260                 mSuspendBlocker.acquire();
261                 mDetectionInProgress = true;
262                 mDetectionStartTime = SystemClock.uptimeMillis();
263                 mTotalSamples = 0;
264                 mMovingSamples = 0;
265 
266                 Message msg = Message.obtain(mHandler, mSensorTimeout);
267                 msg.setAsynchronous(true);
268                 mHandler.sendMessageDelayed(msg, SETTLE_TIME_MILLIS);
269             }
270         }
271     }
272 
finishDetectionLocked()273     private void finishDetectionLocked() {
274         if (mDetectionInProgress) {
275             mSensorManager.unregisterListener(mListener);
276             mHandler.removeCallbacks(mSensorTimeout);
277 
278             if (mMustUpdateRestPosition) {
279                 clearAtRestLocked();
280                 if (mTotalSamples < MIN_SAMPLES) {
281                     Slog.w(TAG, "Wireless charger detector is broken.  Only received "
282                             + mTotalSamples + " samples from the gravity sensor but we "
283                             + "need at least " + MIN_SAMPLES + " and we expect to see "
284                             + "about " + SETTLE_TIME_MILLIS / SAMPLING_INTERVAL_MILLIS
285                             + " on average.");
286                 } else if (mMovingSamples == 0) {
287                     mAtRest = true;
288                     mRestX = mLastSampleX;
289                     mRestY = mLastSampleY;
290                     mRestZ = mLastSampleZ;
291                 }
292                 mMustUpdateRestPosition = false;
293             }
294 
295             if (DEBUG) {
296                 Slog.d(TAG, "New state: mAtRest=" + mAtRest
297                         + ", mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ
298                         + ", mTotalSamples=" + mTotalSamples
299                         + ", mMovingSamples=" + mMovingSamples);
300             }
301 
302             mDetectionInProgress = false;
303             mSuspendBlocker.release();
304         }
305     }
306 
processSampleLocked(float x, float y, float z)307     private void processSampleLocked(float x, float y, float z) {
308         if (mDetectionInProgress) {
309             mLastSampleX = x;
310             mLastSampleY = y;
311             mLastSampleZ = z;
312 
313             mTotalSamples += 1;
314             if (mTotalSamples == 1) {
315                 // Save information about the first sample collected.
316                 mFirstSampleX = x;
317                 mFirstSampleY = y;
318                 mFirstSampleZ = z;
319             } else {
320                 // Determine whether movement has occurred relative to the first sample.
321                 if (hasMoved(mFirstSampleX, mFirstSampleY, mFirstSampleZ, x, y, z)) {
322                     mMovingSamples += 1;
323                 }
324             }
325 
326             // Clear the at rest flag if movement has occurred relative to the rest sample.
327             if (mAtRest && hasMoved(mRestX, mRestY, mRestZ, x, y, z)) {
328                 if (DEBUG) {
329                     Slog.d(TAG, "No longer at rest: "
330                             + "mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ
331                             + ", x=" + x + ", y=" + y + ", z=" + z);
332                 }
333                 clearAtRestLocked();
334             }
335         }
336     }
337 
clearAtRestLocked()338     private void clearAtRestLocked() {
339         mAtRest = false;
340         mRestX = 0;
341         mRestY = 0;
342         mRestZ = 0;
343     }
344 
hasMoved(float x1, float y1, float z1, float x2, float y2, float z2)345     private static boolean hasMoved(float x1, float y1, float z1,
346             float x2, float y2, float z2) {
347         final double dotProduct = (x1 * x2) + (y1 * y2) + (z1 * z2);
348         final double mag1 = Math.sqrt((x1 * x1) + (y1 * y1) + (z1 * z1));
349         final double mag2 = Math.sqrt((x2 * x2) + (y2 * y2) + (z2 * z2));
350         if (mag1 < MIN_GRAVITY || mag1 > MAX_GRAVITY
351                 || mag2 < MIN_GRAVITY || mag2 > MAX_GRAVITY) {
352             if (DEBUG) {
353                 Slog.d(TAG, "Weird gravity vector: mag1=" + mag1 + ", mag2=" + mag2);
354             }
355             return true;
356         }
357         final boolean moved = (dotProduct < mag1 * mag2 * MOVEMENT_ANGLE_COS_THRESHOLD);
358         if (DEBUG) {
359             Slog.d(TAG, "Check: moved=" + moved
360                     + ", x1=" + x1 + ", y1=" + y1 + ", z1=" + z1
361                     + ", x2=" + x2 + ", y2=" + y2 + ", z2=" + z2
362                     + ", angle=" + (Math.acos(dotProduct / mag1 / mag2) * 180 / Math.PI)
363                     + ", dotProduct=" + dotProduct
364                     + ", mag1=" + mag1 + ", mag2=" + mag2);
365         }
366         return moved;
367     }
368 
369     private final SensorEventListener mListener = new SensorEventListener() {
370         @Override
371         public void onSensorChanged(SensorEvent event) {
372             synchronized (mLock) {
373                 processSampleLocked(event.values[0], event.values[1], event.values[2]);
374             }
375         }
376 
377         @Override
378         public void onAccuracyChanged(Sensor sensor, int accuracy) {
379         }
380     };
381 
382     private final Runnable mSensorTimeout = new Runnable() {
383         @Override
384         public void run() {
385             synchronized (mLock) {
386                 finishDetectionLocked();
387             }
388         }
389     };
390 }
391