blob: d73d9cdb7d40f0ac1c837603106a688e7a7539f2 [file] [log] [blame]
/*
* Copyright (C) 2021 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.keyguard;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Animates through messages to show on the keyguard bottom area on the lock screen.
* Utilizes a {@link KeyguardIndicationTextView} for animations. This class handles the rotating
* nature of the messages including:
* - ensuring a message is shown for its minimum amount of time. Minimum time is determined by
* {@link KeyguardIndication#getMinVisibilityMillis()}
* - showing the next message after a default of 3.5 seconds before animating to the next
* - statically showing a single message if there is only one message to show
* - showing certain messages immediately, assuming te current message has been shown for
* at least {@link KeyguardIndication#getMinVisibilityMillis()}. For example, transient and
* biometric messages are meant to be shown immediately.
* - ending animations when dozing begins, and resuming when dozing ends. Rotating messages on
* AoD is undesirable since it wakes up the AP too often.
*/
public class KeyguardIndicationRotateTextViewController extends
ViewController<KeyguardIndicationTextView> implements Dumpable {
public static String TAG = "KgIndicationRotatingCtrl";
private static final long DEFAULT_INDICATION_SHOW_LENGTH = 3500; // milliseconds
public static final long IMPORTANT_MSG_MIN_DURATION = 2000L + 600L; // 2000ms + [Y in duration]
private final StatusBarStateController mStatusBarStateController;
private final float mMaxAlpha;
private final ColorStateList mInitialTextColorState;
// Stores @IndicationType => KeyguardIndication messages
private final Map<Integer, KeyguardIndication> mIndicationMessages = new HashMap<>();
// Executor that will show the next message after a delay
private final DelayableExecutor mExecutor;
@Nullable private ShowNextIndication mShowNextIndicationRunnable;
// List of indication types to show. The next indication to show is always at index 0
private final List<Integer> mIndicationQueue = new LinkedList<>();
private @IndicationType int mCurrIndicationType = INDICATION_TYPE_NONE;
private CharSequence mCurrMessage;
private long mLastIndicationSwitch;
private boolean mIsDozing;
public KeyguardIndicationRotateTextViewController(
KeyguardIndicationTextView view,
@Main DelayableExecutor executor,
StatusBarStateController statusBarStateController
) {
super(view);
mMaxAlpha = view.getAlpha();
mExecutor = executor;
mInitialTextColorState = mView != null
? mView.getTextColors() : ColorStateList.valueOf(Color.WHITE);
mStatusBarStateController = statusBarStateController;
init();
}
@Override
protected void onViewAttached() {
mStatusBarStateController.addCallback(mStatusBarStateListener);
}
@Override
protected void onViewDetached() {
mStatusBarStateController.removeCallback(mStatusBarStateListener);
cancelScheduledIndication();
}
/**
* Update the indication type with the given String.
* @param type of indication
* @param newIndication message to associate with this indication type
* @param showAsap if true: shows this indication message as soon as possible. If false,
* the text associated with this type is updated and will show when its turn
* in the IndicationQueue comes around.
*/
public void updateIndication(@IndicationType int type, KeyguardIndication newIndication,
boolean showAsap) {
if (type == INDICATION_TYPE_REVERSE_CHARGING) {
// temporarily don't show here, instead use AmbientContainer b/181049781
return;
}
long minShowDuration = getMinVisibilityMillis(mIndicationMessages.get(mCurrIndicationType));
final boolean hasPreviousIndication = mIndicationMessages.get(type) != null
&& !TextUtils.isEmpty(mIndicationMessages.get(type).getMessage());
final boolean hasNewIndication = newIndication != null;
if (!hasNewIndication) {
mIndicationMessages.remove(type);
mIndicationQueue.removeIf(x -> x == type);
} else {
if (!hasPreviousIndication) {
mIndicationQueue.add(type);
}
mIndicationMessages.put(type, newIndication);
}
if (mIsDozing) {
return;
}
long currTime = SystemClock.uptimeMillis();
long timeSinceLastIndicationSwitch = currTime - mLastIndicationSwitch;
boolean currMsgShownForMinTime = timeSinceLastIndicationSwitch >= minShowDuration;
if (hasNewIndication) {
if (mCurrIndicationType == INDICATION_TYPE_NONE || mCurrIndicationType == type) {
showIndication(type);
} else if (showAsap) {
if (currMsgShownForMinTime) {
showIndication(type);
} else {
mIndicationQueue.removeIf(x -> x == type);
mIndicationQueue.add(0 /* index */, type /* type */);
scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch);
}
} else if (!isNextIndicationScheduled()) {
long nextShowTime = Math.max(
getMinVisibilityMillis(mIndicationMessages.get(type)),
DEFAULT_INDICATION_SHOW_LENGTH);
if (timeSinceLastIndicationSwitch >= nextShowTime) {
showIndication(type);
} else {
scheduleShowNextIndication(
nextShowTime - timeSinceLastIndicationSwitch);
}
}
return;
}
// current indication is updated to empty
if (mCurrIndicationType == type
&& !hasNewIndication
&& showAsap) {
if (currMsgShownForMinTime) {
if (mShowNextIndicationRunnable != null) {
mShowNextIndicationRunnable.runImmediately();
} else {
showIndication(INDICATION_TYPE_NONE);
}
} else {
scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch);
}
}
}
/**
* Stop showing the following indication type.
*
* If the current indication is of this type, immediately stops showing the message.
*/
public void hideIndication(@IndicationType int type) {
if (!mIndicationMessages.containsKey(type)
|| TextUtils.isEmpty(mIndicationMessages.get(type).getMessage())) {
return;
}
updateIndication(type, null, true);
}
/**
* Show a transient message.
* Transient messages:
* - show immediately
* - will continue to be in the rotation of messages shown until hideTransient is called.
*/
public void showTransient(CharSequence newIndication) {
updateIndication(INDICATION_TYPE_TRANSIENT,
new KeyguardIndication.Builder()
.setMessage(newIndication)
.setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
.setTextColor(mInitialTextColorState)
.build(),
/* showImmediately */true);
}
/**
* Hide a transient message immediately.
*/
public void hideTransient() {
hideIndication(INDICATION_TYPE_TRANSIENT);
}
/**
* @return true if there are available indications to show
*/
public boolean hasIndications() {
return mIndicationMessages.keySet().size() > 0;
}
/**
* Clears all messages in the queue and sets the current message to an empty string.
*/
public void clearMessages() {
mCurrIndicationType = INDICATION_TYPE_NONE;
mIndicationQueue.clear();
mView.clearMessages();
}
/**
* Immediately show the passed indication type and schedule the next indication to show.
* Will re-add this indication to be re-shown after all other indications have been
* rotated through.
*/
private void showIndication(@IndicationType int type) {
cancelScheduledIndication();
final CharSequence previousMessage = mCurrMessage;
final @IndicationType int previousIndicationType = mCurrIndicationType;
mCurrIndicationType = type;
mCurrMessage = mIndicationMessages.get(type) != null
? mIndicationMessages.get(type).getMessage()
: null;
mIndicationQueue.removeIf(x -> x == type);
if (mCurrIndicationType != INDICATION_TYPE_NONE) {
mIndicationQueue.add(type); // re-add to show later
}
mLastIndicationSwitch = SystemClock.uptimeMillis();
if (!TextUtils.equals(previousMessage, mCurrMessage)
|| previousIndicationType != mCurrIndicationType) {
mView.switchIndication(mIndicationMessages.get(type));
}
// only schedule next indication if there's more than just this indication in the queue
if (mCurrIndicationType != INDICATION_TYPE_NONE && mIndicationQueue.size() > 1) {
scheduleShowNextIndication(Math.max(
getMinVisibilityMillis(mIndicationMessages.get(type)),
DEFAULT_INDICATION_SHOW_LENGTH));
}
}
private long getMinVisibilityMillis(KeyguardIndication indication) {
if (indication == null) {
return 0;
}
if (indication.getMinVisibilityMillis() == null) {
return 0;
}
return indication.getMinVisibilityMillis();
}
protected boolean isNextIndicationScheduled() {
return mShowNextIndicationRunnable != null;
}
private void scheduleShowNextIndication(long msUntilShowNextMsg) {
cancelScheduledIndication();
mShowNextIndicationRunnable = new ShowNextIndication(msUntilShowNextMsg);
}
private void cancelScheduledIndication() {
if (mShowNextIndicationRunnable != null) {
mShowNextIndicationRunnable.cancelDelayedExecution();
mShowNextIndicationRunnable = null;
}
}
private StatusBarStateController.StateListener mStatusBarStateListener =
new StatusBarStateController.StateListener() {
@Override
public void onDozeAmountChanged(float linear, float eased) {
mView.setAlpha((1 - linear) * mMaxAlpha);
}
@Override
public void onDozingChanged(boolean isDozing) {
if (isDozing == mIsDozing) return;
mIsDozing = isDozing;
if (mIsDozing) {
showIndication(INDICATION_TYPE_NONE);
} else if (mIndicationQueue.size() > 0) {
showIndication(mIndicationQueue.remove(0));
}
}
};
/**
* Shows the next indication in the IndicationQueue after an optional delay.
* This wrapper has the ability to cancel itself (remove runnable from DelayableExecutor) or
* immediately run itself (which also removes itself from the DelayableExecutor).
*/
class ShowNextIndication {
private final Runnable mShowIndicationRunnable;
private Runnable mCancelDelayedRunnable;
ShowNextIndication(long delay) {
mShowIndicationRunnable = () -> {
int type = mIndicationQueue.size() == 0
? INDICATION_TYPE_NONE : mIndicationQueue.remove(0);
showIndication(type);
};
mCancelDelayedRunnable = mExecutor.executeDelayed(mShowIndicationRunnable, delay);
}
public void runImmediately() {
cancelDelayedExecution();
mShowIndicationRunnable.run();
}
public void cancelDelayedExecution() {
if (mCancelDelayedRunnable != null) {
mCancelDelayedRunnable.run();
mCancelDelayedRunnable = null;
}
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("KeyguardIndicationRotatingTextViewController:");
pw.println(" currentMessage=" + mView.getText());
pw.println(" dozing:" + mIsDozing);
pw.println(" queue:" + mIndicationQueue.toString());
pw.println(" showNextIndicationRunnable:" + mShowNextIndicationRunnable);
if (hasIndications()) {
pw.println(" All messages:");
for (int type : mIndicationMessages.keySet()) {
pw.println(" type=" + type + " " + mIndicationMessages.get(type));
}
}
}
// only used locally to stop showing any messages & stop the rotating messages
static final int INDICATION_TYPE_NONE = -1;
public static final int INDICATION_TYPE_OWNER_INFO = 0;
public static final int INDICATION_TYPE_DISCLOSURE = 1;
public static final int INDICATION_TYPE_LOGOUT = 2;
public static final int INDICATION_TYPE_BATTERY = 3;
public static final int INDICATION_TYPE_ALIGNMENT = 4;
public static final int INDICATION_TYPE_TRANSIENT = 5;
public static final int INDICATION_TYPE_TRUST = 6;
public static final int INDICATION_TYPE_RESTING = 7;
public static final int INDICATION_TYPE_USER_LOCKED = 8;
public static final int INDICATION_TYPE_REVERSE_CHARGING = 10;
public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11;
@IntDef({
INDICATION_TYPE_NONE,
INDICATION_TYPE_DISCLOSURE,
INDICATION_TYPE_OWNER_INFO,
INDICATION_TYPE_LOGOUT,
INDICATION_TYPE_BATTERY,
INDICATION_TYPE_ALIGNMENT,
INDICATION_TYPE_TRANSIENT,
INDICATION_TYPE_TRUST,
INDICATION_TYPE_RESTING,
INDICATION_TYPE_USER_LOCKED,
INDICATION_TYPE_REVERSE_CHARGING,
INDICATION_TYPE_BIOMETRIC_MESSAGE
})
@Retention(RetentionPolicy.SOURCE)
public @interface IndicationType{}
}