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