blob: d1f5337b80b0541d1d267d024be6788e4fdf3efa [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.doze;
import android.app.AlarmManager;
import android.app.UiModeManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.Formatter;
import android.util.Log;
import com.android.internal.hardware.AmbientDisplayConfiguration;
import com.android.internal.util.Preconditions;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.util.Assert;
import com.android.systemui.util.wakelock.WakeLock;
import java.io.PrintWriter;
import java.util.function.IntConsumer;
/**
* Handles triggers for ambient state changes.
*/
public class DozeTriggers implements DozeMachine.Part {
private static final String TAG = "DozeTriggers";
private static final boolean DEBUG = DozeService.DEBUG;
/** adb shell am broadcast -a com.android.systemui.doze.pulse com.android.systemui */
private static final String PULSE_ACTION = "com.android.systemui.doze.pulse";
private final Context mContext;
private final DozeMachine mMachine;
private final DozeSensors mDozeSensors;
private final DozeHost mDozeHost;
private final AmbientDisplayConfiguration mConfig;
private final DozeParameters mDozeParameters;
private final SensorManager mSensorManager;
private final Handler mHandler;
private final WakeLock mWakeLock;
private final boolean mAllowPulseTriggers;
private final UiModeManager mUiModeManager;
private final TriggerReceiver mBroadcastReceiver = new TriggerReceiver();
private long mNotificationPulseTime;
private boolean mPulsePending;
public DozeTriggers(Context context, DozeMachine machine, DozeHost dozeHost,
AlarmManager alarmManager, AmbientDisplayConfiguration config,
DozeParameters dozeParameters, SensorManager sensorManager, Handler handler,
WakeLock wakeLock, boolean allowPulseTriggers) {
mContext = context;
mMachine = machine;
mDozeHost = dozeHost;
mConfig = config;
mDozeParameters = dozeParameters;
mSensorManager = sensorManager;
mHandler = handler;
mWakeLock = wakeLock;
mAllowPulseTriggers = allowPulseTriggers;
mDozeSensors = new DozeSensors(context, alarmManager, mSensorManager, dozeParameters,
config, wakeLock, this::onSensor, this::onProximityFar);
mUiModeManager = mContext.getSystemService(UiModeManager.class);
}
private void onNotification() {
if (DozeMachine.DEBUG) Log.d(TAG, "requestNotificationPulse");
mNotificationPulseTime = SystemClock.elapsedRealtime();
if (!mConfig.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) return;
requestPulse(DozeLog.PULSE_REASON_NOTIFICATION, false /* performedProxCheck */);
DozeLog.traceNotificationPulse(mContext);
}
private void onWhisper() {
requestPulse(DozeLog.PULSE_REASON_NOTIFICATION, false /* performedProxCheck */);
}
private void proximityCheckThenCall(IntConsumer callback,
boolean alreadyPerformedProxCheck,
int pulseReason) {
if (alreadyPerformedProxCheck) {
callback.accept(ProximityCheck.RESULT_NOT_CHECKED);
} else {
final long start = SystemClock.uptimeMillis();
new ProximityCheck() {
@Override
public void onProximityResult(int result) {
final long end = SystemClock.uptimeMillis();
DozeLog.traceProximityResult(mContext, result == RESULT_NEAR,
end - start, pulseReason);
callback.accept(result);
}
}.check();
}
}
private void onSensor(int pulseReason, boolean sensorPerformedProxCheck,
float screenX, float screenY) {
boolean isDoubleTap = pulseReason == DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP;
boolean isPickup = pulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP;
boolean isLongPress = pulseReason == DozeLog.PULSE_REASON_SENSOR_LONG_PRESS;
if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) && !isLongPress) {
proximityCheckThenCall((result) -> {
if (result == ProximityCheck.RESULT_NEAR) {
// In pocket, drop event.
return;
}
if (isDoubleTap) {
mDozeHost.onDoubleTap(screenX, screenY);
mMachine.wakeUp();
} else {
mDozeHost.extendPulse();
}
}, sensorPerformedProxCheck, pulseReason);
return;
} else {
requestPulse(pulseReason, sensorPerformedProxCheck);
}
if (isPickup) {
final long timeSinceNotification =
SystemClock.elapsedRealtime() - mNotificationPulseTime;
final boolean withinVibrationThreshold =
timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
DozeLog.tracePickupPulse(mContext, withinVibrationThreshold);
}
}
private void onProximityFar(boolean far) {
final boolean near = !far;
final DozeMachine.State state = mMachine.getState();
final boolean paused = (state == DozeMachine.State.DOZE_AOD_PAUSED);
final boolean pausing = (state == DozeMachine.State.DOZE_AOD_PAUSING);
final boolean aod = (state == DozeMachine.State.DOZE_AOD);
if (near && state == DozeMachine.State.DOZE_PULSING) {
if (DEBUG) Log.i(TAG, "Prox NEAR, ending pulse");
DozeLog.tracePulseCanceledByProx(mContext);
mMachine.requestState(DozeMachine.State.DOZE_PULSE_DONE);
}
if (far && (paused || pausing)) {
if (DEBUG) Log.i(TAG, "Prox FAR, unpausing AOD");
mMachine.requestState(DozeMachine.State.DOZE_AOD);
} else if (near && aod) {
if (DEBUG) Log.i(TAG, "Prox NEAR, pausing AOD");
mMachine.requestState(DozeMachine.State.DOZE_AOD_PAUSING);
}
}
@Override
public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
switch (newState) {
case INITIALIZED:
mBroadcastReceiver.register(mContext);
mDozeHost.addCallback(mHostCallback);
checkTriggersAtInit();
break;
case DOZE:
case DOZE_AOD:
mDozeSensors.setProxListening(newState != DozeMachine.State.DOZE);
if (oldState != DozeMachine.State.INITIALIZED) {
mDozeSensors.reregisterAllSensors();
}
mDozeSensors.setListening(true);
break;
case DOZE_AOD_PAUSED:
case DOZE_AOD_PAUSING:
mDozeSensors.setProxListening(true);
mDozeSensors.setListening(false);
break;
case DOZE_PULSING:
mDozeSensors.setProxListening(true);
break;
case FINISH:
mBroadcastReceiver.unregister(mContext);
mDozeHost.removeCallback(mHostCallback);
mDozeSensors.setListening(false);
mDozeSensors.setProxListening(false);
break;
default:
}
}
private void checkTriggersAtInit() {
if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
|| mDozeHost.isPowerSaveActive()
|| mDozeHost.isBlockingDoze()
|| !mDozeHost.isProvisioned()) {
mMachine.requestState(DozeMachine.State.FINISH);
}
}
private void requestPulse(final int reason, boolean performedProxCheck) {
Assert.isMainThread();
mDozeHost.extendPulse();
if (mPulsePending || !mAllowPulseTriggers || !canPulse()) {
if (mAllowPulseTriggers) {
DozeLog.tracePulseDropped(mContext, mPulsePending, mMachine.getState(),
mDozeHost.isPulsingBlocked());
}
return;
}
mPulsePending = true;
proximityCheckThenCall((result) -> {
if (result == ProximityCheck.RESULT_NEAR) {
// in pocket, abort pulse
mPulsePending = false;
} else {
// not in pocket, continue pulsing
continuePulseRequest(reason);
}
}, !mDozeParameters.getProxCheckBeforePulse() || performedProxCheck, reason);
}
private boolean canPulse() {
return mMachine.getState() == DozeMachine.State.DOZE
|| mMachine.getState() == DozeMachine.State.DOZE_AOD;
}
private void continuePulseRequest(int reason) {
mPulsePending = false;
if (mDozeHost.isPulsingBlocked() || !canPulse()) {
DozeLog.tracePulseDropped(mContext, mPulsePending, mMachine.getState(),
mDozeHost.isPulsingBlocked());
return;
}
mMachine.requestPulse(reason);
}
@Override
public void dump(PrintWriter pw) {
pw.print(" notificationPulseTime=");
pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime));
pw.print(" pulsePending="); pw.println(mPulsePending);
pw.println("DozeSensors:");
mDozeSensors.dump(pw);
}
private abstract class ProximityCheck implements SensorEventListener, Runnable {
private static final int TIMEOUT_DELAY_MS = 500;
protected static final int RESULT_UNKNOWN = 0;
protected static final int RESULT_NEAR = 1;
protected static final int RESULT_FAR = 2;
protected static final int RESULT_NOT_CHECKED = 3;
private boolean mRegistered;
private boolean mFinished;
private float mMaxRange;
protected abstract void onProximityResult(int result);
public void check() {
Preconditions.checkState(!mFinished && !mRegistered);
final Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if (sensor == null) {
if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No sensor found");
finishWithResult(RESULT_UNKNOWN);
return;
}
mDozeSensors.setDisableSensorsInterferingWithProximity(true);
mMaxRange = sensor.getMaximumRange();
mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0,
mHandler);
mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
mWakeLock.acquire();
mRegistered = true;
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.values.length == 0) {
if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: Event has no values!");
finishWithResult(RESULT_UNKNOWN);
} else {
if (DozeMachine.DEBUG) {
Log.d(TAG, "ProxCheck: Event: value=" + event.values[0] + " max=" + mMaxRange);
}
final boolean isNear = event.values[0] < mMaxRange;
finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
}
}
@Override
public void run() {
if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No event received before timeout");
finishWithResult(RESULT_UNKNOWN);
}
private void finishWithResult(int result) {
if (mFinished) return;
boolean wasRegistered = mRegistered;
if (mRegistered) {
mHandler.removeCallbacks(this);
mSensorManager.unregisterListener(this);
mDozeSensors.setDisableSensorsInterferingWithProximity(false);
mRegistered = false;
}
onProximityResult(result);
if (wasRegistered) {
mWakeLock.release();
}
mFinished = true;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// noop
}
}
private class TriggerReceiver extends BroadcastReceiver {
private boolean mRegistered;
@Override
public void onReceive(Context context, Intent intent) {
if (PULSE_ACTION.equals(intent.getAction())) {
if (DozeMachine.DEBUG) Log.d(TAG, "Received pulse intent");
requestPulse(DozeLog.PULSE_REASON_INTENT, false /* performedProxCheck */);
}
if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
mMachine.requestState(DozeMachine.State.FINISH);
}
if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
mDozeSensors.onUserSwitched();
}
}
public void register(Context context) {
if (mRegistered) {
return;
}
IntentFilter filter = new IntentFilter(PULSE_ACTION);
filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
filter.addAction(Intent.ACTION_USER_SWITCHED);
context.registerReceiver(this, filter);
mRegistered = true;
}
public void unregister(Context context) {
if (!mRegistered) {
return;
}
context.unregisterReceiver(this);
mRegistered = false;
}
}
private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
@Override
public void onNotificationHeadsUp() {
onNotification();
}
@Override
public void onPowerSaveChanged(boolean active) {
if (active) {
mMachine.requestState(DozeMachine.State.FINISH);
}
}
};
}