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