| /* |
| * 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{} |
| } |