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