| /* |
| * 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.annotation.MainThread; |
| import android.hardware.display.AmbientDisplayConfiguration; |
| import android.os.Trace; |
| import android.os.UserHandle; |
| import android.util.Log; |
| import android.view.Display; |
| |
| import com.android.internal.util.Preconditions; |
| import com.android.systemui.dock.DockManager; |
| import com.android.systemui.keyguard.WakefulnessLifecycle; |
| import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness; |
| import com.android.systemui.statusbar.phone.DozeParameters; |
| import com.android.systemui.statusbar.policy.BatteryController; |
| import com.android.systemui.util.Assert; |
| import com.android.systemui.util.wakelock.WakeLock; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| |
| /** |
| * Orchestrates all things doze. |
| * |
| * DozeMachine implements a state machine that orchestrates how the UI and triggers work and |
| * interfaces with the power and screen states. |
| * |
| * During state transitions and in certain states, DozeMachine holds a wake lock. |
| */ |
| public class DozeMachine { |
| |
| static final String TAG = "DozeMachine"; |
| static final boolean DEBUG = DozeService.DEBUG; |
| private final DozeLog mDozeLog; |
| private static final String REASON_CHANGE_STATE = "DozeMachine#requestState"; |
| private static final String REASON_HELD_FOR_STATE = "DozeMachine#heldForState"; |
| |
| public enum State { |
| /** Default state. Transition to INITIALIZED to get Doze going. */ |
| UNINITIALIZED, |
| /** Doze components are set up. Followed by transition to DOZE or DOZE_AOD. */ |
| INITIALIZED, |
| /** Regular doze. Device is asleep and listening for pulse triggers. */ |
| DOZE, |
| /** Always-on doze. Device is asleep, showing UI and listening for pulse triggers. */ |
| DOZE_AOD, |
| /** Pulse has been requested. Device is awake and preparing UI */ |
| DOZE_REQUEST_PULSE, |
| /** Pulse is showing. Device is awake and showing UI. */ |
| DOZE_PULSING, |
| /** Pulse is showing with bright wallpaper. Device is awake and showing UI. */ |
| DOZE_PULSING_BRIGHT, |
| /** Pulse is done showing. Followed by transition to DOZE or DOZE_AOD. */ |
| DOZE_PULSE_DONE, |
| /** Doze is done. DozeService is finished. */ |
| FINISH, |
| /** AOD, but the display is temporarily off. */ |
| DOZE_AOD_PAUSED, |
| /** AOD, prox is near, transitions to DOZE_AOD_PAUSED after a timeout. */ |
| DOZE_AOD_PAUSING, |
| /** Always-on doze. Device is awake, showing docking UI and listening for pulse triggers. */ |
| DOZE_AOD_DOCKED; |
| |
| boolean canPulse() { |
| switch (this) { |
| case DOZE: |
| case DOZE_AOD: |
| case DOZE_AOD_PAUSED: |
| case DOZE_AOD_PAUSING: |
| case DOZE_AOD_DOCKED: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| boolean staysAwake() { |
| switch (this) { |
| case DOZE_REQUEST_PULSE: |
| case DOZE_PULSING: |
| case DOZE_PULSING_BRIGHT: |
| case DOZE_AOD_DOCKED: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| boolean isAlwaysOn() { |
| return this == DOZE_AOD || this == DOZE_AOD_DOCKED; |
| } |
| |
| int screenState(DozeParameters parameters) { |
| switch (this) { |
| case UNINITIALIZED: |
| case INITIALIZED: |
| case DOZE_REQUEST_PULSE: |
| return parameters.shouldControlScreenOff() ? Display.STATE_ON |
| : Display.STATE_OFF; |
| case DOZE_AOD_PAUSED: |
| case DOZE: |
| return Display.STATE_OFF; |
| case DOZE_PULSING: |
| case DOZE_PULSING_BRIGHT: |
| case DOZE_AOD_DOCKED: |
| return Display.STATE_ON; |
| case DOZE_AOD: |
| case DOZE_AOD_PAUSING: |
| return Display.STATE_DOZE_SUSPEND; |
| default: |
| return Display.STATE_UNKNOWN; |
| } |
| } |
| } |
| |
| private final Service mDozeService; |
| private final WakeLock mWakeLock; |
| private final AmbientDisplayConfiguration mConfig; |
| private final WakefulnessLifecycle mWakefulnessLifecycle; |
| private final BatteryController mBatteryController; |
| private Part[] mParts; |
| |
| private final ArrayList<State> mQueuedRequests = new ArrayList<>(); |
| private State mState = State.UNINITIALIZED; |
| private int mPulseReason; |
| private boolean mWakeLockHeldForCurrentState = false; |
| private DockManager mDockManager; |
| |
| public DozeMachine(Service service, AmbientDisplayConfiguration config, WakeLock wakeLock, |
| WakefulnessLifecycle wakefulnessLifecycle, BatteryController batteryController, |
| DozeLog dozeLog, DockManager dockManager) { |
| mDozeService = service; |
| mConfig = config; |
| mWakefulnessLifecycle = wakefulnessLifecycle; |
| mWakeLock = wakeLock; |
| mBatteryController = batteryController; |
| mDozeLog = dozeLog; |
| mDockManager = dockManager; |
| } |
| |
| /** Initializes the set of {@link Part}s. Must be called exactly once after construction. */ |
| public void setParts(Part[] parts) { |
| Preconditions.checkState(mParts == null); |
| mParts = parts; |
| } |
| |
| /** |
| * Requests transitioning to {@code requestedState}. |
| * |
| * This can be called during a state transition, in which case it will be queued until all |
| * queued state transitions are done. |
| * |
| * A wake lock is held while the transition is happening. |
| * |
| * Note that {@link #transitionPolicy} can modify what state will be transitioned to. |
| */ |
| @MainThread |
| public void requestState(State requestedState) { |
| Preconditions.checkArgument(requestedState != State.DOZE_REQUEST_PULSE); |
| requestState(requestedState, DozeLog.PULSE_REASON_NONE); |
| } |
| |
| @MainThread |
| public void requestPulse(int pulseReason) { |
| // Must not be called during a transition. There's no inherent problem with that, |
| // but there's currently no need to execute from a transition and it simplifies the |
| // code to not have to worry about keeping the pulseReason in mQueuedRequests. |
| Preconditions.checkState(!isExecutingTransition()); |
| requestState(State.DOZE_REQUEST_PULSE, pulseReason); |
| } |
| |
| private void requestState(State requestedState, int pulseReason) { |
| Assert.isMainThread(); |
| if (DEBUG) { |
| Log.i(TAG, "request: current=" + mState + " req=" + requestedState, |
| new Throwable("here")); |
| } |
| |
| boolean runNow = !isExecutingTransition(); |
| mQueuedRequests.add(requestedState); |
| if (runNow) { |
| mWakeLock.acquire(REASON_CHANGE_STATE); |
| for (int i = 0; i < mQueuedRequests.size(); i++) { |
| // Transitions in Parts can call back into requestState, which will |
| // cause mQueuedRequests to grow. |
| transitionTo(mQueuedRequests.get(i), pulseReason); |
| } |
| mQueuedRequests.clear(); |
| mWakeLock.release(REASON_CHANGE_STATE); |
| } |
| } |
| |
| /** |
| * @return the current state. |
| * |
| * This must not be called during a transition. |
| */ |
| @MainThread |
| public State getState() { |
| Assert.isMainThread(); |
| if (isExecutingTransition()) { |
| throw new IllegalStateException("Cannot get state because there were pending " |
| + "transitions: " + mQueuedRequests.toString()); |
| } |
| return mState; |
| } |
| |
| /** |
| * @return the current pulse reason. |
| * |
| * This is only valid if the machine is currently in one of the pulse states. |
| */ |
| @MainThread |
| public int getPulseReason() { |
| Assert.isMainThread(); |
| Preconditions.checkState(mState == State.DOZE_REQUEST_PULSE |
| || mState == State.DOZE_PULSING |
| || mState == State.DOZE_PULSING_BRIGHT |
| || mState == State.DOZE_PULSE_DONE, "must be in pulsing state, but is " + mState); |
| return mPulseReason; |
| } |
| |
| /** Requests the PowerManager to wake up now. */ |
| public void wakeUp() { |
| mDozeService.requestWakeUp(); |
| } |
| |
| public boolean isExecutingTransition() { |
| return !mQueuedRequests.isEmpty(); |
| } |
| |
| private void transitionTo(State requestedState, int pulseReason) { |
| State newState = transitionPolicy(requestedState); |
| |
| if (DEBUG) { |
| Log.i(TAG, "transition: old=" + mState + " req=" + requestedState + " new=" + newState); |
| } |
| |
| if (newState == mState) { |
| return; |
| } |
| |
| validateTransition(newState); |
| |
| State oldState = mState; |
| mState = newState; |
| |
| mDozeLog.traceState(newState); |
| Trace.traceCounter(Trace.TRACE_TAG_APP, "doze_machine_state", newState.ordinal()); |
| |
| updatePulseReason(newState, oldState, pulseReason); |
| performTransitionOnComponents(oldState, newState); |
| updateWakeLockState(newState); |
| |
| resolveIntermediateState(newState); |
| } |
| |
| private void updatePulseReason(State newState, State oldState, int pulseReason) { |
| if (newState == State.DOZE_REQUEST_PULSE) { |
| mPulseReason = pulseReason; |
| } else if (oldState == State.DOZE_PULSE_DONE) { |
| mPulseReason = DozeLog.PULSE_REASON_NONE; |
| } |
| } |
| |
| private void performTransitionOnComponents(State oldState, State newState) { |
| for (Part p : mParts) { |
| p.transitionTo(oldState, newState); |
| } |
| |
| switch (newState) { |
| case FINISH: |
| mDozeService.finish(); |
| break; |
| default: |
| } |
| } |
| |
| private void validateTransition(State newState) { |
| try { |
| switch (mState) { |
| case FINISH: |
| Preconditions.checkState(newState == State.FINISH); |
| break; |
| case UNINITIALIZED: |
| Preconditions.checkState(newState == State.INITIALIZED); |
| break; |
| } |
| switch (newState) { |
| case UNINITIALIZED: |
| throw new IllegalArgumentException("can't transition to UNINITIALIZED"); |
| case INITIALIZED: |
| Preconditions.checkState(mState == State.UNINITIALIZED); |
| break; |
| case DOZE_PULSING: |
| Preconditions.checkState(mState == State.DOZE_REQUEST_PULSE); |
| break; |
| case DOZE_PULSE_DONE: |
| Preconditions.checkState( |
| mState == State.DOZE_REQUEST_PULSE || mState == State.DOZE_PULSING |
| || mState == State.DOZE_PULSING_BRIGHT); |
| break; |
| default: |
| break; |
| } |
| } catch (RuntimeException e) { |
| throw new IllegalStateException("Illegal Transition: " + mState + " -> " + newState, e); |
| } |
| } |
| |
| private State transitionPolicy(State requestedState) { |
| if (mState == State.FINISH) { |
| return State.FINISH; |
| } |
| if (mConfig.dozeSuppressed(UserHandle.USER_CURRENT) && requestedState.isAlwaysOn()) { |
| Log.i(TAG, "Doze is suppressed. Suppressing state: " + requestedState); |
| mDozeLog.traceDozeSuppressed(requestedState); |
| return State.DOZE; |
| } |
| if ((mState == State.DOZE_AOD_PAUSED || mState == State.DOZE_AOD_PAUSING |
| || mState == State.DOZE_AOD || mState == State.DOZE) |
| && requestedState == State.DOZE_PULSE_DONE) { |
| Log.i(TAG, "Dropping pulse done because current state is already done: " + mState); |
| return mState; |
| } |
| if (requestedState == State.DOZE_AOD && mBatteryController.isAodPowerSave()) { |
| return State.DOZE; |
| } |
| if (requestedState == State.DOZE_REQUEST_PULSE && !mState.canPulse()) { |
| Log.i(TAG, "Dropping pulse request because current state can't pulse: " + mState); |
| return mState; |
| } |
| return requestedState; |
| } |
| |
| private void updateWakeLockState(State newState) { |
| boolean staysAwake = newState.staysAwake(); |
| if (mWakeLockHeldForCurrentState && !staysAwake) { |
| mWakeLock.release(REASON_HELD_FOR_STATE); |
| mWakeLockHeldForCurrentState = false; |
| } else if (!mWakeLockHeldForCurrentState && staysAwake) { |
| mWakeLock.acquire(REASON_HELD_FOR_STATE); |
| mWakeLockHeldForCurrentState = true; |
| } |
| } |
| |
| private void resolveIntermediateState(State state) { |
| switch (state) { |
| case INITIALIZED: |
| case DOZE_PULSE_DONE: |
| final State nextState; |
| @Wakefulness int wakefulness = mWakefulnessLifecycle.getWakefulness(); |
| if (wakefulness == WakefulnessLifecycle.WAKEFULNESS_AWAKE |
| || wakefulness == WakefulnessLifecycle.WAKEFULNESS_WAKING) { |
| nextState = State.FINISH; |
| } else if (mDockManager.isDocked()) { |
| nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED; |
| } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { |
| nextState = State.DOZE_AOD; |
| } else { |
| nextState = State.DOZE; |
| } |
| |
| transitionTo(nextState, DozeLog.PULSE_REASON_NONE); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** Dumps the current state */ |
| public void dump(PrintWriter pw) { |
| pw.print(" state="); pw.println(mState); |
| pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState); |
| pw.print(" wakeLock="); pw.println(mWakeLock); |
| pw.println("Parts:"); |
| for (Part p : mParts) { |
| p.dump(pw); |
| } |
| } |
| |
| /** A part of the DozeMachine that needs to be notified about state changes. */ |
| public interface Part { |
| /** |
| * Transition from {@code oldState} to {@code newState}. |
| * |
| * This method is guaranteed to only be called while a wake lock is held. |
| */ |
| void transitionTo(State oldState, State newState); |
| |
| /** Dump current state. For debugging only. */ |
| default void dump(PrintWriter pw) {} |
| } |
| |
| /** A wrapper interface for {@link android.service.dreams.DreamService} */ |
| public interface Service { |
| /** Finish dreaming. */ |
| void finish(); |
| |
| /** Request a display state. See {@link android.view.Display#STATE_DOZE}. */ |
| void setDozeScreenState(int state); |
| |
| /** Request waking up. */ |
| void requestWakeUp(); |
| |
| /** Set screen brightness */ |
| void setDozeScreenBrightness(int brightness); |
| |
| class Delegate implements Service { |
| private final Service mDelegate; |
| |
| public Delegate(Service delegate) { |
| mDelegate = delegate; |
| } |
| |
| @Override |
| public void finish() { |
| mDelegate.finish(); |
| } |
| |
| @Override |
| public void setDozeScreenState(int state) { |
| mDelegate.setDozeScreenState(state); |
| } |
| |
| @Override |
| public void requestWakeUp() { |
| mDelegate.requestWakeUp(); |
| } |
| |
| @Override |
| public void setDozeScreenBrightness(int brightness) { |
| mDelegate.setDozeScreenBrightness(brightness); |
| } |
| } |
| } |
| } |