1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.keyguard;
18 
19 import android.content.res.ColorStateList;
20 import android.content.res.Configuration;
21 import android.hardware.biometrics.BiometricSourceType;
22 import android.os.SystemClock;
23 import android.text.Editable;
24 import android.text.TextUtils;
25 import android.text.TextWatcher;
26 import android.util.Log;
27 import android.util.Pair;
28 import android.view.View;
29 
30 import androidx.annotation.Nullable;
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.systemui.statusbar.policy.ConfigurationController;
34 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
35 import com.android.systemui.util.ViewController;
36 
37 import java.lang.ref.WeakReference;
38 
39 import javax.inject.Inject;
40 
41 /**
42  * Controller for a {@link KeyguardMessageAreaController}.
43  * @param <T> A subclass of KeyguardMessageArea.
44  */
45 public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
46         extends ViewController<T> {
47     /**
48      * Pair representing:
49      *   first - BiometricSource the currently displayed message is associated with.
50      *   second - Timestamp the biometric message came in uptimeMillis.
51      * This Pair can be null if the message is not associated with a biometric.
52      */
53     @Nullable
54     private Pair<BiometricSourceType, Long> mMessageBiometricSource = null;
55     private static final Long SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS = 3500L;
56 
57     /**
58      * Delay before speaking an accessibility announcement. Used to prevent
59      * lift-to-type from interrupting itself.
60      */
61     private static final long ANNOUNCEMENT_DELAY = 250;
62     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
63     private final ConfigurationController mConfigurationController;
64     private final AnnounceRunnable mAnnounceRunnable;
65     private final TextWatcher mTextWatcher = new TextWatcher() {
66         @Override
67         public void afterTextChanged(Editable editable) {
68             CharSequence msg = editable;
69             if (!TextUtils.isEmpty(msg)) {
70                 mView.removeCallbacks(mAnnounceRunnable);
71                 mAnnounceRunnable.setTextToAnnounce(msg);
72                 mView.postDelayed(() -> {
73                     if (msg == mView.getText()) {
74                         mAnnounceRunnable.run();
75                     }
76                 }, ANNOUNCEMENT_DELAY);
77             }
78         }
79 
80         @Override
81         public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
82             /* no-op */
83         }
84 
85         @Override
86         public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
87             /* no-op */
88         }
89     };
90 
91     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
92         public void onFinishedGoingToSleep(int why) {
93             mView.setSelected(false);
94         }
95 
96         public void onStartedWakingUp() {
97             mView.setSelected(true);
98         }
99     };
100 
101     private ConfigurationListener mConfigurationListener = new ConfigurationListener() {
102         @Override
103         public void onConfigChanged(Configuration newConfig) {
104             mView.onConfigChanged();
105         }
106 
107         @Override
108         public void onThemeChanged() {
109             mView.onThemeChanged();
110         }
111 
112         @Override
113         public void onDensityOrFontScaleChanged() {
114             mView.onDensityOrFontScaleChanged();
115         }
116     };
117 
KeyguardMessageAreaController(T view, KeyguardUpdateMonitor keyguardUpdateMonitor, ConfigurationController configurationController)118     protected KeyguardMessageAreaController(T view,
119             KeyguardUpdateMonitor keyguardUpdateMonitor,
120             ConfigurationController configurationController) {
121         super(view);
122 
123         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
124         mConfigurationController = configurationController;
125         mAnnounceRunnable = new AnnounceRunnable(mView);
126     }
127 
128     @Override
onViewAttached()129     protected void onViewAttached() {
130         mConfigurationController.addCallback(mConfigurationListener);
131         mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
132         mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive());
133         mView.onThemeChanged();
134         mView.addTextChangedListener(mTextWatcher);
135     }
136 
137     @Override
onViewDetached()138     protected void onViewDetached() {
139         mConfigurationController.removeCallback(mConfigurationListener);
140         mKeyguardUpdateMonitor.removeCallback(mInfoCallback);
141         mView.removeTextChangedListener(mTextWatcher);
142     }
143 
144     /**
145      * Indicate that view is visible and can display messages.
146      */
setIsVisible(boolean isVisible)147     public void setIsVisible(boolean isVisible) {
148         mView.setIsVisible(isVisible);
149     }
150 
151     /**
152      * Mark this view with {@link View#GONE} visibility to remove this from the layout of the view.
153      * Any calls to {@link #setIsVisible(boolean)} after this will be a no-op.
154      */
disable()155     public void disable() {
156         mView.disable();
157     }
158 
setMessage(CharSequence s)159     public void setMessage(CharSequence s) {
160         setMessage(s, true);
161     }
162 
163     /**
164      * Sets a message to the underlying text view.
165      */
setMessage(CharSequence s, boolean animate)166     public void setMessage(CharSequence s, boolean animate) {
167         setMessage(s, animate, null);
168     }
169 
170     /**
171      * Sets a message to the underlying text view.
172      */
setMessage(CharSequence s, BiometricSourceType biometricSourceType)173     public void setMessage(CharSequence s, BiometricSourceType biometricSourceType) {
174         setMessage(s, true, biometricSourceType);
175     }
176 
setMessage( CharSequence s, boolean animate, BiometricSourceType biometricSourceType)177     private void setMessage(
178             CharSequence s,
179             boolean animate,
180             BiometricSourceType biometricSourceType) {
181         final long uptimeMillis = SystemClock.uptimeMillis();
182         if (skipShowingFaceMessage(biometricSourceType, uptimeMillis)) {
183             Log.d("KeyguardMessageAreaController", "Skip showing face message \"" + s + "\"");
184             return;
185         }
186         mMessageBiometricSource =  new Pair<>(biometricSourceType, uptimeMillis);
187         if (mView.isDisabled()) {
188             return;
189         }
190         mView.setMessage(s, animate);
191     }
192 
skipShowingFaceMessage( BiometricSourceType biometricSourceType, Long currentUptimeMillis )193     private boolean skipShowingFaceMessage(
194             BiometricSourceType biometricSourceType, Long currentUptimeMillis
195     ) {
196         return mMessageBiometricSource != null
197                 && biometricSourceType == BiometricSourceType.FACE
198                 && mMessageBiometricSource.first == BiometricSourceType.FINGERPRINT
199                 && (currentUptimeMillis - mMessageBiometricSource.second)
200                     < SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS;
201     }
202 
setMessage(int resId)203     public void setMessage(int resId) {
204         String message = resId != 0 ? mView.getResources().getString(resId) : null;
205         setMessage(message);
206     }
207 
setNextMessageColor(ColorStateList colorState)208     public void setNextMessageColor(ColorStateList colorState) {
209         mView.setNextMessageColor(colorState);
210     }
211 
212     /** Returns the message of the underlying TextView. */
getMessage()213     public CharSequence getMessage() {
214         return mView.getText();
215     }
216 
217     /** Factory for creating {@link com.android.keyguard.KeyguardMessageAreaController}. */
218     public static class Factory {
219         private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
220         private final ConfigurationController mConfigurationController;
221 
222         @Inject
Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, ConfigurationController configurationController)223         public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
224                 ConfigurationController configurationController) {
225             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
226             mConfigurationController = configurationController;
227         }
228 
229         /** Build a new {@link KeyguardMessageAreaController}. */
create(KeyguardMessageArea view)230         public KeyguardMessageAreaController create(KeyguardMessageArea view) {
231             return new KeyguardMessageAreaController(
232                     view, mKeyguardUpdateMonitor, mConfigurationController);
233         }
234     }
235 
236     /**
237      * Runnable used to delay accessibility announcements.
238      */
239     @VisibleForTesting
240     public static class AnnounceRunnable implements Runnable {
241         private final WeakReference<View> mHost;
242         private CharSequence mTextToAnnounce;
243 
AnnounceRunnable(View host)244         AnnounceRunnable(View host) {
245             mHost = new WeakReference<>(host);
246         }
247 
248         /** Sets the text to announce. */
setTextToAnnounce(CharSequence textToAnnounce)249         public void setTextToAnnounce(CharSequence textToAnnounce) {
250             mTextToAnnounce = textToAnnounce;
251         }
252 
253         @Override
run()254         public void run() {
255             final View host = mHost.get();
256             if (host != null && host.isVisibleToUser()) {
257                 host.announceForAccessibility(mTextToAnnounce);
258             }
259         }
260     }
261 }
262