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