WirelessChargerDetector.java revision 20e287534396655af0d5912d8b272070ad24a93a
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 17package com.android.server.power; 18 19import android.hardware.Sensor; 20import android.hardware.SensorEvent; 21import android.hardware.SensorEventListener; 22import android.hardware.SensorManager; 23import android.os.BatteryManager; 24import android.os.SystemClock; 25import android.util.Slog; 26 27import java.io.PrintWriter; 28 29/** 30 * Implements heuristics to detect docking or undocking from a wireless charger. 31 * <p> 32 * Some devices have wireless charging circuits that are unable to detect when the 33 * device is resting on a wireless charger except when the device is actually 34 * receiving power from the charger. The device may stop receiving power 35 * if the battery is already nearly full or if it is too hot. As a result, we cannot 36 * always rely on the battery service wireless plug signal to accurately indicate 37 * whether the device has been docked or undocked from a wireless charger. 38 * </p><p> 39 * This is a problem because the power manager typically wakes up the screen and 40 * plays a tone when the device is docked in a wireless charger. It is important 41 * for the system to suppress spurious docking and undocking signals because they 42 * can be intrusive for the user (especially if they cause a tone to be played 43 * late at night for no apparent reason). 44 * </p><p> 45 * To avoid spurious signals, we apply some special policies to wireless chargers. 46 * </p><p> 47 * 1. Don't wake the device when undocked from the wireless charger because 48 * it might be that the device is still resting on the wireless charger 49 * but is not receiving power anymore because the battery is full. 50 * Ideally we would wake the device if we could be certain that the user had 51 * picked it up from the wireless charger but due to hardware limitations we 52 * must be more conservative. 53 * </p><p> 54 * 2. Don't wake the device when docked on a wireless charger if the 55 * battery already appears to be mostly full. This situation may indicate 56 * that the device was resting on the charger the whole time and simply 57 * wasn't receiving power because the battery was already full. We can't tell 58 * whether the device was just placed on the charger or whether it has 59 * been there for half of the night slowly discharging until it reached 60 * the point where it needed to start charging again. So we suppress docking 61 * signals that occur when the battery level is above a given threshold. 62 * </p><p> 63 * 3. Don't wake the device when docked on a wireless charger if it does 64 * not appear to have moved since it was last undocked because it may 65 * be that the prior undocking signal was spurious. We use the gravity 66 * sensor to detect this case. 67 * </p> 68 */ 69final class WirelessChargerDetector { 70 private static final String TAG = "WirelessChargerDetector"; 71 private static final boolean DEBUG = false; 72 73 // Number of nanoseconds per millisecond. 74 private static final long NANOS_PER_MS = 1000000; 75 76 // The minimum amount of time to spend watching the sensor before making 77 // a determination of whether movement occurred. 78 private static final long SETTLE_TIME_NANOS = 500 * NANOS_PER_MS; 79 80 // The minimum number of samples that must be collected. 81 private static final int MIN_SAMPLES = 3; 82 83 // Upper bound on the battery charge percentage in order to consider turning 84 // the screen on when the device starts charging wirelessly. 85 private static final int WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT = 95; 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 101 // The gravity sensor, or null if none. 102 private Sensor mGravitySensor; 103 104 // Previously observed wireless power state. 105 private boolean mPoweredWirelessly; 106 107 // True if the device is thought to be at rest on a wireless charger. 108 private boolean mAtRest; 109 110 // The gravity vector most recently observed while at rest. 111 private float mRestX, mRestY, mRestZ; 112 113 /* These properties are only meaningful while detection is in progress. */ 114 115 // True if detection is in progress. 116 // The suspend blocker is held while this is the case. 117 private boolean mDetectionInProgress; 118 119 // True if the rest position should be updated if at rest. 120 // Otherwise, the current rest position is simply checked and cleared if movement 121 // is detected but no new rest position is stored. 122 private boolean mMustUpdateRestPosition; 123 124 // The total number of samples collected. 125 private int mTotalSamples; 126 127 // The number of samples collected that showed evidence of not being at rest. 128 private int mMovingSamples; 129 130 // The time and value of the first sample that was collected. 131 private long mFirstSampleTime; 132 private float mFirstSampleX, mFirstSampleY, mFirstSampleZ; 133 134 // The time and value of the last sample that was collected (for debugging only). 135 private long mLastSampleTime; 136 private float mLastSampleX, mLastSampleY, mLastSampleZ; 137 138 public WirelessChargerDetector(SensorManager sensorManager, 139 SuspendBlocker suspendBlocker) { 140 mSensorManager = sensorManager; 141 mSuspendBlocker = suspendBlocker; 142 143 mGravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); 144 } 145 146 public void dump(PrintWriter pw) { 147 synchronized (mLock) { 148 pw.println(); 149 pw.println("Wireless Charger Detector State:"); 150 pw.println(" mGravitySensor=" + mGravitySensor); 151 pw.println(" mPoweredWirelessly=" + mPoweredWirelessly); 152 pw.println(" mAtRest=" + mAtRest); 153 pw.println(" mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ); 154 pw.println(" mDetectionInProgress=" + mDetectionInProgress); 155 pw.println(" mMustUpdateRestPosition=" + mMustUpdateRestPosition); 156 pw.println(" mTotalSamples=" + mTotalSamples); 157 pw.println(" mMovingSamples=" + mMovingSamples); 158 pw.println(" mFirstSampleTime=" + mFirstSampleTime); 159 pw.println(" mFirstSampleX=" + mFirstSampleX 160 + ", mFirstSampleY=" + mFirstSampleY + ", mFirstSampleZ=" + mFirstSampleZ); 161 pw.println(" mLastSampleTime=" + mLastSampleTime); 162 pw.println(" mLastSampleX=" + mLastSampleX 163 + ", mLastSampleY=" + mLastSampleY + ", mLastSampleZ=" + mLastSampleZ); 164 } 165 } 166 167 /** 168 * Updates the charging state and returns true if docking was detected. 169 * 170 * @param isPowered True if the device is powered. 171 * @param plugType The current plug type. 172 * @param batteryLevel The current battery level. 173 * @return True if the device is determined to have just been docked on a wireless 174 * charger, after suppressing spurious docking or undocking signals. 175 */ 176 public boolean update(boolean isPowered, int plugType, int batteryLevel) { 177 synchronized (mLock) { 178 final boolean wasPoweredWirelessly = mPoweredWirelessly; 179 180 if (isPowered && plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) { 181 // The device is receiving power from the wireless charger. 182 // Update the rest position asynchronously. 183 mPoweredWirelessly = true; 184 mMustUpdateRestPosition = true; 185 startDetectionLocked(); 186 } else { 187 // The device may or may not be on the wireless charger depending on whether 188 // the unplug signal that we received was spurious. 189 mPoweredWirelessly = false; 190 if (mAtRest) { 191 if (plugType != 0 && plugType != BatteryManager.BATTERY_PLUGGED_WIRELESS) { 192 // The device was plugged into a new non-wireless power source. 193 // It's safe to assume that it is no longer on the wireless charger. 194 mMustUpdateRestPosition = false; 195 clearAtRestLocked(); 196 } else { 197 // The device may still be on the wireless charger but we don't know. 198 // Check whether the device has remained at rest on the charger 199 // so that we will know to ignore the next wireless plug event 200 // if needed. 201 startDetectionLocked(); 202 } 203 } 204 } 205 206 // Report that the device has been docked only if the device just started 207 // receiving power wirelessly, has a high enough battery level that we 208 // can be assured that charging was not delayed due to the battery previously 209 // having been full, and the device is not known to already be at rest 210 // on the wireless charger from earlier. 211 return mPoweredWirelessly && !wasPoweredWirelessly 212 && batteryLevel < WIRELESS_CHARGER_TURN_ON_BATTERY_LEVEL_LIMIT 213 && !mAtRest; 214 } 215 } 216 217 private void startDetectionLocked() { 218 if (!mDetectionInProgress && mGravitySensor != null) { 219 if (mSensorManager.registerListener(mListener, mGravitySensor, 220 SensorManager.SENSOR_DELAY_UI)) { 221 mSuspendBlocker.acquire(); 222 mDetectionInProgress = true; 223 mTotalSamples = 0; 224 mMovingSamples = 0; 225 } 226 } 227 } 228 229 private void processSample(long timeNanos, float x, float y, float z) { 230 synchronized (mLock) { 231 if (!mDetectionInProgress) { 232 return; 233 } 234 235 mLastSampleTime = timeNanos; 236 mLastSampleX = x; 237 mLastSampleY = y; 238 mLastSampleZ = z; 239 240 mTotalSamples += 1; 241 if (mTotalSamples == 1) { 242 // Save information about the first sample collected. 243 mFirstSampleTime = timeNanos; 244 mFirstSampleX = x; 245 mFirstSampleY = y; 246 mFirstSampleZ = z; 247 } else { 248 // Determine whether movement has occurred relative to the first sample. 249 if (hasMoved(mFirstSampleX, mFirstSampleY, mFirstSampleZ, x, y, z)) { 250 mMovingSamples += 1; 251 } 252 } 253 254 // Clear the at rest flag if movement has occurred relative to the rest sample. 255 if (mAtRest && hasMoved(mRestX, mRestY, mRestZ, x, y, z)) { 256 if (DEBUG) { 257 Slog.d(TAG, "No longer at rest: " 258 + "mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ 259 + ", x=" + x + ", y=" + y + ", z=" + z); 260 } 261 clearAtRestLocked(); 262 } 263 264 // Save the result when done. 265 if (timeNanos - mFirstSampleTime >= SETTLE_TIME_NANOS 266 && mTotalSamples >= MIN_SAMPLES) { 267 mSensorManager.unregisterListener(mListener); 268 if (mMustUpdateRestPosition) { 269 if (mMovingSamples == 0) { 270 mAtRest = true; 271 mRestX = x; 272 mRestY = y; 273 mRestZ = z; 274 } else { 275 clearAtRestLocked(); 276 } 277 mMustUpdateRestPosition = false; 278 } 279 mDetectionInProgress = false; 280 mSuspendBlocker.release(); 281 282 if (DEBUG) { 283 Slog.d(TAG, "New state: mAtRest=" + mAtRest 284 + ", mRestX=" + mRestX + ", mRestY=" + mRestY + ", mRestZ=" + mRestZ 285 + ", mTotalSamples=" + mTotalSamples 286 + ", mMovingSamples=" + mMovingSamples); 287 } 288 } 289 } 290 } 291 292 private void clearAtRestLocked() { 293 mAtRest = false; 294 mRestX = 0; 295 mRestY = 0; 296 mRestZ = 0; 297 } 298 299 private static boolean hasMoved(float x1, float y1, float z1, 300 float x2, float y2, float z2) { 301 final double dotProduct = (x1 * x2) + (y1 * y2) + (z1 * z2); 302 final double mag1 = Math.sqrt((x1 * x1) + (y1 * y1) + (z1 * z1)); 303 final double mag2 = Math.sqrt((x2 * x2) + (y2 * y2) + (z2 * z2)); 304 if (mag1 < MIN_GRAVITY || mag1 > MAX_GRAVITY 305 || mag2 < MIN_GRAVITY || mag2 > MAX_GRAVITY) { 306 if (DEBUG) { 307 Slog.d(TAG, "Weird gravity vector: mag1=" + mag1 + ", mag2=" + mag2); 308 } 309 return true; 310 } 311 final boolean moved = (dotProduct < mag1 * mag2 * MOVEMENT_ANGLE_COS_THRESHOLD); 312 if (DEBUG) { 313 Slog.d(TAG, "Check: moved=" + moved 314 + ", x1=" + x1 + ", y1=" + y1 + ", z1=" + z1 315 + ", x2=" + x2 + ", y2=" + y2 + ", z2=" + z2 316 + ", angle=" + (Math.acos(dotProduct / mag1 / mag2) * 180 / Math.PI) 317 + ", dotProduct=" + dotProduct 318 + ", mag1=" + mag1 + ", mag2=" + mag2); 319 } 320 return moved; 321 } 322 323 private final SensorEventListener mListener = new SensorEventListener() { 324 @Override 325 public void onSensorChanged(SensorEvent event) { 326 // We use SystemClock.elapsedRealtimeNanos() instead of event.timestamp because 327 // on some devices the sensor HAL may produce timestamps that are not monotonic. 328 processSample(SystemClock.elapsedRealtimeNanos(), 329 event.values[0], event.values[1], event.values[2]); 330 } 331 332 @Override 333 public void onAccuracyChanged(Sensor sensor, int accuracy) { 334 } 335 }; 336} 337