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