2 * Copyright (C) 2016 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.systemui.doze;
19 import android.annotation.Nullable;
20 import android.app.AlarmManager;
21 import android.app.UiModeManager;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.res.Configuration;
27 import android.hardware.Sensor;
28 import android.hardware.SensorEvent;
29 import android.hardware.SensorEventListener;
30 import android.hardware.SensorManager;
31 import android.hardware.display.AmbientDisplayConfiguration;
32 import android.metrics.LogMaker;
33 import android.os.Handler;
34 import android.os.SystemClock;
35 import android.os.UserHandle;
36 import android.text.format.Formatter;
37 import android.util.Log;
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.logging.MetricsLogger;
41 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
42 import com.android.internal.util.Preconditions;
43 import com.android.systemui.Dependency;
44 import com.android.systemui.R;
45 import com.android.systemui.dock.DockManager;
46 import com.android.systemui.statusbar.phone.DozeParameters;
47 import com.android.systemui.util.Assert;
48 import com.android.systemui.util.wakelock.WakeLock;
50 import java.io.PrintWriter;
51 import java.util.function.IntConsumer;
54 * Handles triggers for ambient state changes.
56 public class DozeTriggers implements DozeMachine.Part {
58 private static final String TAG = "DozeTriggers";
59 private static final boolean DEBUG = DozeService.DEBUG;
61 /** adb shell am broadcast -a com.android.systemui.doze.pulse com.android.systemui */
62 private static final String PULSE_ACTION = "com.android.systemui.doze.pulse";
65 * Last value sent by the wake-display sensor.
66 * Assuming that the screen should start on.
68 private static boolean sWakeDisplaySensorState = true;
70 private final Context mContext;
71 private final DozeMachine mMachine;
72 private final DozeSensors mDozeSensors;
73 private final DozeHost mDozeHost;
74 private final AmbientDisplayConfiguration mConfig;
75 private final DozeParameters mDozeParameters;
76 private final SensorManager mSensorManager;
77 private final Handler mHandler;
78 private final WakeLock mWakeLock;
79 private final boolean mAllowPulseTriggers;
80 private final UiModeManager mUiModeManager;
81 private final TriggerReceiver mBroadcastReceiver = new TriggerReceiver();
82 private final DockEventListener mDockEventListener = new DockEventListener();
83 private final DockManager mDockManager;
85 private long mNotificationPulseTime;
86 private boolean mPulsePending;
88 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
90 public DozeTriggers(Context context, DozeMachine machine, DozeHost dozeHost,
91 AlarmManager alarmManager, AmbientDisplayConfiguration config,
92 DozeParameters dozeParameters, SensorManager sensorManager, Handler handler,
93 WakeLock wakeLock, boolean allowPulseTriggers, DockManager dockManager) {
98 mDozeParameters = dozeParameters;
99 mSensorManager = sensorManager;
101 mWakeLock = wakeLock;
102 mAllowPulseTriggers = allowPulseTriggers;
103 mDozeSensors = new DozeSensors(context, alarmManager, mSensorManager, dozeParameters,
104 config, wakeLock, this::onSensor, this::onProximityFar,
105 dozeParameters.getPolicy());
106 mUiModeManager = mContext.getSystemService(UiModeManager.class);
107 mDockManager = dockManager;
110 private void onNotification(Runnable onPulseSuppressedListener) {
111 if (DozeMachine.DEBUG) {
112 Log.d(TAG, "requestNotificationPulse");
114 if (!sWakeDisplaySensorState) {
115 Log.d(TAG, "Wake display false. Pulse denied.");
116 runIfNotNull(onPulseSuppressedListener);
117 DozeLog.tracePulseDropped(mContext, "wakeDisplaySensor");
120 mNotificationPulseTime = SystemClock.elapsedRealtime();
121 if (!mConfig.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
122 runIfNotNull(onPulseSuppressedListener);
123 DozeLog.tracePulseDropped(mContext, "pulseOnNotificationsDisabled");
126 requestPulse(DozeLog.PULSE_REASON_NOTIFICATION, false /* performedProxCheck */,
127 onPulseSuppressedListener);
128 DozeLog.traceNotificationPulse(mContext);
131 private static void runIfNotNull(Runnable runnable) {
132 if (runnable != null) {
137 private void proximityCheckThenCall(IntConsumer callback,
138 boolean alreadyPerformedProxCheck,
140 Boolean cachedProxFar = mDozeSensors.isProximityCurrentlyFar();
141 if (alreadyPerformedProxCheck) {
142 callback.accept(ProximityCheck.RESULT_NOT_CHECKED);
143 } else if (cachedProxFar != null) {
144 callback.accept(cachedProxFar ? ProximityCheck.RESULT_FAR : ProximityCheck.RESULT_NEAR);
146 final long start = SystemClock.uptimeMillis();
147 new ProximityCheck() {
149 public void onProximityResult(int result) {
150 final long end = SystemClock.uptimeMillis();
151 DozeLog.traceProximityResult(mContext, result == RESULT_NEAR,
152 end - start, reason);
153 callback.accept(result);
160 void onSensor(int pulseReason, float screenX, float screenY, float[] rawValues) {
161 boolean isDoubleTap = pulseReason == DozeLog.REASON_SENSOR_DOUBLE_TAP;
162 boolean isTap = pulseReason == DozeLog.REASON_SENSOR_TAP;
163 boolean isPickup = pulseReason == DozeLog.REASON_SENSOR_PICKUP;
164 boolean isLongPress = pulseReason == DozeLog.PULSE_REASON_SENSOR_LONG_PRESS;
165 boolean isWakeDisplay = pulseReason == DozeLog.REASON_SENSOR_WAKE_UP;
166 boolean isWakeLockScreen = pulseReason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN;
167 boolean wakeEvent = rawValues != null && rawValues.length > 0 && rawValues[0] != 0;
170 onWakeScreen(wakeEvent, mMachine.isExecutingTransition() ? null : mMachine.getState());
171 } else if (isLongPress) {
172 requestPulse(pulseReason, true /* alreadyPerformedProxCheck */,
173 null /* onPulseSupressedListener */);
174 } else if (isWakeLockScreen) {
176 requestPulse(pulseReason, true /* alreadyPerformedProxCheck */,
177 null /* onPulseSupressedListener */);
180 proximityCheckThenCall((result) -> {
181 if (result == ProximityCheck.RESULT_NEAR) {
182 // In pocket, drop event.
185 if (isDoubleTap || isTap) {
186 if (screenX != -1 && screenY != -1) {
187 mDozeHost.onSlpiTap(screenX, screenY);
189 gentleWakeUp(pulseReason);
190 } else if (isPickup) {
191 gentleWakeUp(pulseReason);
193 mDozeHost.extendPulse(pulseReason);
195 }, true /* alreadyPerformedProxCheck */, pulseReason);
199 final long timeSinceNotification =
200 SystemClock.elapsedRealtime() - mNotificationPulseTime;
201 final boolean withinVibrationThreshold =
202 timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
203 DozeLog.tracePickupWakeUp(mContext, withinVibrationThreshold);
207 private void gentleWakeUp(int reason) {
208 // Log screen wake up reason (lift/pickup, tap, double-tap)
209 mMetricsLogger.write(new LogMaker(MetricsEvent.DOZING)
210 .setType(MetricsEvent.TYPE_UPDATE)
211 .setSubtype(reason));
212 if (mDozeParameters.getDisplayNeedsBlanking()) {
213 // Let's prepare the display to wake-up by drawing black.
214 // This will cover the hardware wake-up sequence, where the display
215 // becomes black for a few frames.
216 mDozeHost.setAodDimmingScrim(1f);
221 private void onProximityFar(boolean far) {
222 // Proximity checks are asynchronous and the user might have interacted with the phone
223 // when a new event is arriving. This means that a state transition might have happened
224 // and the proximity check is now obsolete.
225 if (mMachine.isExecutingTransition()) {
226 Log.w(TAG, "onProximityFar called during transition. Ignoring sensor response.");
230 final boolean near = !far;
231 final DozeMachine.State state = mMachine.getState();
232 final boolean paused = (state == DozeMachine.State.DOZE_AOD_PAUSED);
233 final boolean pausing = (state == DozeMachine.State.DOZE_AOD_PAUSING);
234 final boolean aod = (state == DozeMachine.State.DOZE_AOD);
236 if (state == DozeMachine.State.DOZE_PULSING
237 || state == DozeMachine.State.DOZE_PULSING_BRIGHT) {
238 boolean ignoreTouch = near;
240 Log.i(TAG, "Prox changed, ignore touch = " + ignoreTouch);
242 mDozeHost.onIgnoreTouchWhilePulsing(ignoreTouch);
245 if (far && (paused || pausing)) {
247 Log.i(TAG, "Prox FAR, unpausing AOD");
249 mMachine.requestState(DozeMachine.State.DOZE_AOD);
250 } else if (near && aod) {
252 Log.i(TAG, "Prox NEAR, pausing AOD");
254 mMachine.requestState(DozeMachine.State.DOZE_AOD_PAUSING);
259 * When a wake screen event is received from a sensor
260 * @param wake {@code true} when it's time to wake up, {@code false} when we should sleep.
261 * @param state The current state, or null if the state could not be determined due to enqueued
264 private void onWakeScreen(boolean wake, @Nullable DozeMachine.State state) {
265 DozeLog.traceWakeDisplay(wake);
266 sWakeDisplaySensorState = wake;
269 proximityCheckThenCall((result) -> {
270 if (result == ProximityCheck.RESULT_NEAR) {
271 // In pocket, drop event.
274 if (state == DozeMachine.State.DOZE) {
275 mMachine.requestState(DozeMachine.State.DOZE_AOD);
276 // Logs AOD open due to sensor wake up.
277 mMetricsLogger.write(new LogMaker(MetricsEvent.DOZING)
278 .setType(MetricsEvent.TYPE_OPEN)
279 .setSubtype(DozeLog.REASON_SENSOR_WAKE_UP));
281 }, true /* alreadyPerformedProxCheck */, DozeLog.REASON_SENSOR_WAKE_UP);
283 boolean paused = (state == DozeMachine.State.DOZE_AOD_PAUSED);
284 boolean pausing = (state == DozeMachine.State.DOZE_AOD_PAUSING);
285 if (!pausing && !paused) {
286 mMachine.requestState(DozeMachine.State.DOZE);
287 // Logs AOD close due to sensor wake up.
288 mMetricsLogger.write(new LogMaker(MetricsEvent.DOZING)
289 .setType(MetricsEvent.TYPE_CLOSE)
290 .setSubtype(DozeLog.REASON_SENSOR_WAKE_UP));
296 public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
299 mBroadcastReceiver.register(mContext);
300 mDozeHost.addCallback(mHostCallback);
301 if (mDockManager != null) {
302 mDockManager.addListener(mDockEventListener);
304 mDozeSensors.requestTemporaryDisable();
305 checkTriggersAtInit();
309 mDozeSensors.setProxListening(newState != DozeMachine.State.DOZE);
310 mDozeSensors.setListening(true);
311 mDozeSensors.setPaused(false);
312 if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) {
313 onWakeScreen(false, newState);
316 case DOZE_AOD_PAUSED:
317 case DOZE_AOD_PAUSING:
318 mDozeSensors.setProxListening(true);
319 mDozeSensors.setPaused(true);
322 case DOZE_PULSING_BRIGHT:
323 mDozeSensors.setTouchscreenSensorsListening(false);
324 mDozeSensors.setProxListening(true);
325 mDozeSensors.setPaused(false);
327 case DOZE_PULSE_DONE:
328 mDozeSensors.requestTemporaryDisable();
329 // A pulse will temporarily disable sensors that require a touch screen.
330 // Let's make sure that they are re-enabled when the pulse is over.
331 mDozeSensors.updateListening();
334 mBroadcastReceiver.unregister(mContext);
335 mDozeHost.removeCallback(mHostCallback);
336 if (mDockManager != null) {
337 mDockManager.removeListener(mDockEventListener);
339 mDozeSensors.setListening(false);
340 mDozeSensors.setProxListening(false);
346 private void checkTriggersAtInit() {
347 if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
348 || mDozeHost.isPowerSaveActive()
349 || mDozeHost.isBlockingDoze()
350 || !mDozeHost.isProvisioned()) {
351 mMachine.requestState(DozeMachine.State.FINISH);
355 private void requestPulse(final int reason, boolean performedProxCheck,
356 Runnable onPulseSuppressedListener) {
357 Assert.isMainThread();
358 mDozeHost.extendPulse(reason);
360 // When already pulsing we're allowed to show the wallpaper directly without
361 // requesting a new pulse.
362 if (mMachine.getState() == DozeMachine.State.DOZE_PULSING
363 && reason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
364 mMachine.requestState(DozeMachine.State.DOZE_PULSING_BRIGHT);
368 if (mPulsePending || !mAllowPulseTriggers || !canPulse()) {
369 if (mAllowPulseTriggers) {
370 DozeLog.tracePulseDropped(mContext, mPulsePending, mMachine.getState(),
371 mDozeHost.isPulsingBlocked());
373 runIfNotNull(onPulseSuppressedListener);
377 mPulsePending = true;
378 proximityCheckThenCall((result) -> {
379 if (result == ProximityCheck.RESULT_NEAR) {
380 // in pocket, abort pulse
381 DozeLog.tracePulseDropped(mContext, "inPocket");
382 mPulsePending = false;
383 runIfNotNull(onPulseSuppressedListener);
385 // not in pocket, continue pulsing
386 continuePulseRequest(reason);
388 }, !mDozeParameters.getProxCheckBeforePulse() || performedProxCheck, reason);
390 // Logs request pulse reason on AOD screen.
391 mMetricsLogger.write(new LogMaker(MetricsEvent.DOZING)
392 .setType(MetricsEvent.TYPE_UPDATE).setSubtype(reason));
395 private boolean canPulse() {
396 return mMachine.getState() == DozeMachine.State.DOZE
397 || mMachine.getState() == DozeMachine.State.DOZE_AOD;
400 private void continuePulseRequest(int reason) {
401 mPulsePending = false;
402 if (mDozeHost.isPulsingBlocked() || !canPulse()) {
403 DozeLog.tracePulseDropped(mContext, mPulsePending, mMachine.getState(),
404 mDozeHost.isPulsingBlocked());
407 mMachine.requestPulse(reason);
411 public void dump(PrintWriter pw) {
412 pw.print(" notificationPulseTime=");
413 pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime));
415 pw.print(" pulsePending="); pw.println(mPulsePending);
416 pw.println("DozeSensors:");
417 mDozeSensors.dump(pw);
421 * @see DozeSensors.ProxSensor
423 private abstract class ProximityCheck implements SensorEventListener, Runnable {
424 private static final int TIMEOUT_DELAY_MS = 500;
426 protected static final int RESULT_UNKNOWN = 0;
427 protected static final int RESULT_NEAR = 1;
428 protected static final int RESULT_FAR = 2;
429 protected static final int RESULT_NOT_CHECKED = 3;
431 private boolean mRegistered;
432 private boolean mFinished;
433 private float mMaxRange;
434 private boolean mUsingBrightnessSensor;
436 protected abstract void onProximityResult(int result);
438 public void check() {
439 Preconditions.checkState(!mFinished && !mRegistered);
440 Sensor sensor = DozeSensors.findSensorWithType(mSensorManager,
441 mContext.getString(R.string.doze_brightness_sensor_type));
442 mUsingBrightnessSensor = sensor != null;
443 if (sensor == null) {
444 sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
446 if (sensor == null) {
447 if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No sensor found");
448 finishWithResult(RESULT_UNKNOWN);
451 mDozeSensors.setDisableSensorsInterferingWithProximity(true);
453 mMaxRange = sensor.getMaximumRange();
454 mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0,
456 mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
457 mWakeLock.acquire(TAG);
462 * @see DozeSensors.ProxSensor#onSensorChanged(SensorEvent)
465 public void onSensorChanged(SensorEvent event) {
466 if (event.values.length == 0) {
467 if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: Event has no values!");
468 finishWithResult(RESULT_UNKNOWN);
470 if (DozeMachine.DEBUG) {
471 Log.d(TAG, "ProxCheck: Event: value=" + event.values[0] + " max=" + mMaxRange);
473 final boolean isNear;
474 if (mUsingBrightnessSensor) {
475 // The custom brightness sensor is gated by the proximity sensor and will
476 // return 0 whenever prox is covered.
477 isNear = event.values[0] == 0;
479 isNear = event.values[0] < mMaxRange;
481 finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
487 if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No event received before timeout");
488 finishWithResult(RESULT_UNKNOWN);
491 private void finishWithResult(int result) {
492 if (mFinished) return;
493 boolean wasRegistered = mRegistered;
495 mHandler.removeCallbacks(this);
496 mSensorManager.unregisterListener(this);
497 mDozeSensors.setDisableSensorsInterferingWithProximity(false);
500 onProximityResult(result);
502 mWakeLock.release(TAG);
508 public void onAccuracyChanged(Sensor sensor, int accuracy) {
513 private class TriggerReceiver extends BroadcastReceiver {
514 private boolean mRegistered;
517 public void onReceive(Context context, Intent intent) {
518 if (PULSE_ACTION.equals(intent.getAction())) {
519 if (DozeMachine.DEBUG) Log.d(TAG, "Received pulse intent");
520 requestPulse(DozeLog.PULSE_REASON_INTENT, false, /* performedProxCheck */
521 null /* onPulseSupressedListener */);
523 if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
524 mMachine.requestState(DozeMachine.State.FINISH);
526 if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
527 mDozeSensors.onUserSwitched();
531 public void register(Context context) {
535 IntentFilter filter = new IntentFilter(PULSE_ACTION);
536 filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
537 filter.addAction(Intent.ACTION_USER_SWITCHED);
538 context.registerReceiver(this, filter);
542 public void unregister(Context context) {
546 context.unregisterReceiver(this);
551 private class DockEventListener implements DockManager.DockEventListener {
553 public void onEvent(int event) {
554 if (DEBUG) Log.d(TAG, "dock event = " + event);
556 case DockManager.STATE_DOCKED:
557 case DockManager.STATE_DOCKED_HIDE:
558 mDozeSensors.ignoreTouchScreenSensorsSettingInterferingWithDocking(true);
560 case DockManager.STATE_NONE:
561 mDozeSensors.ignoreTouchScreenSensorsSettingInterferingWithDocking(false);
569 private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
571 public void onNotificationAlerted(Runnable onPulseSuppressedListener) {
572 onNotification(onPulseSuppressedListener);
576 public void onPowerSaveChanged(boolean active) {
578 mMachine.requestState(DozeMachine.State.FINISH);