• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.systemui.car.privacy;
18 
19 import android.content.Context;
20 import android.util.AttributeSet;
21 import android.util.Log;
22 import android.view.View;
23 
24 import androidx.annotation.NonNull;
25 import androidx.annotation.Nullable;
26 import androidx.annotation.UiThread;
27 import androidx.constraintlayout.motion.widget.MotionLayout;
28 
29 import com.android.systemui.R;
30 
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * Car optimized Mic Privacy Chip View that is shown when microphone is being used.
37  *
38  * State flows:
39  * Base state:
40  * <ul>
41  * <li>INVISIBLE - Start Mic Use ->> Mic Status?</li>
42  * </ul>
43  * Mic On:
44  * <ul>
45  * <li>Mic Status? - On ->> ACTIVE_INIT</li>
46  * <li>ACTIVE_INIT - delay ->> ACTIVE</li>
47  * <li>ACTIVE - Stop Mic Use ->> INACTIVE</li>
48  * <li>INACTIVE - delay ->> INVISIBLE</li>
49  * </ul>
50  * Mic Off:
51  * <ul>
52  * <li>Mic Status? - Off ->> MICROPHONE_OFF</li>
53  * </ul>
54  */
55 public class MicPrivacyChip extends MotionLayout {
56     private final static boolean DEBUG = false;
57     private final static String TAG = "MicPrivacyChip";
58     private final static String TYPES_TEXT_MICROPHONE = "microphone";
59 
60     private final int mDelayPillToCircle;
61     private final int mDelayToNoMicUsage;
62 
63     private AnimationStates mCurrentTransitionState;
64     private boolean mIsInflated;
65     private boolean mIsMicrophoneEnabled;
66     private ScheduledExecutorService mExecutor;
67 
MicPrivacyChip(@onNull Context context)68     public MicPrivacyChip(@NonNull Context context) {
69         this(context, /* attrs= */ null);
70     }
71 
MicPrivacyChip(@onNull Context context, @Nullable AttributeSet attrs)72     public MicPrivacyChip(@NonNull Context context, @Nullable AttributeSet attrs) {
73         this(context, attrs, /* defStyleAttrs= */ 0);
74     }
75 
MicPrivacyChip(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttrs)76     public MicPrivacyChip(@NonNull Context context,
77             @Nullable AttributeSet attrs, int defStyleAttrs) {
78         super(context, attrs, defStyleAttrs);
79 
80         mDelayPillToCircle = getResources().getInteger(R.integer.privacy_chip_pill_to_circle_delay);
81         mDelayToNoMicUsage = getResources().getInteger(R.integer.privacy_chip_no_mic_usage_delay);
82 
83         mExecutor = Executors.newSingleThreadScheduledExecutor();
84         mIsInflated = false;
85 
86         // Microphone is enabled by default (invisible state).
87         mIsMicrophoneEnabled = true;
88     }
89 
90     @Override
onFinishInflate()91     protected void onFinishInflate() {
92         super.onFinishInflate();
93 
94         mCurrentTransitionState = AnimationStates.INVISIBLE;
95         mIsInflated = true;
96     }
97 
98     @Override
setOnClickListener(View.OnClickListener onClickListener)99     public void setOnClickListener(View.OnClickListener onClickListener) {
100         // required for CTS tests.
101         super.setOnClickListener(onClickListener);
102         // required for rotary.
103         requireViewById(R.id.focus_view).setOnClickListener(onClickListener);
104     }
105 
106     /**
107      * Sets whether microphone is enabled or disabled.
108      * If enabled, animates to {@link AnimationStates#INVISIBLE}.
109      * Otherwise, animates to {@link AnimationStates#MICROPHONE_OFF}.
110      */
111     @UiThread
setMicrophoneEnabled(boolean isMicrophoneEnabled)112     public void setMicrophoneEnabled(boolean isMicrophoneEnabled) {
113         if (DEBUG) Log.d(TAG, "Microphone enabled: " + isMicrophoneEnabled);
114 
115         if (mIsMicrophoneEnabled == isMicrophoneEnabled) {
116             if (isMicrophoneEnabled) {
117                 switch (mCurrentTransitionState) {
118                     case INVISIBLE:
119                     case ACTIVE:
120                     case INACTIVE:
121                     case ACTIVE_INIT:
122                         return;
123                 }
124             } else {
125                 if (mCurrentTransitionState == AnimationStates.MICROPHONE_OFF) return;
126             }
127         }
128 
129         mIsMicrophoneEnabled = isMicrophoneEnabled;
130 
131         if (!mIsInflated) {
132             if (DEBUG) Log.d(TAG, "Layout not inflated");
133 
134             return;
135         }
136 
137         if (mIsMicrophoneEnabled) {
138             if (DEBUG) Log.d(TAG, "setTransition: invisibleFromMicOff");
139             setTransition(R.id.invisibleFromMicOff);
140         } else {
141             switch (mCurrentTransitionState) {
142                 case INVISIBLE:
143                     if (DEBUG) Log.d(TAG, "setTransition: micOffFromInvisible");
144                     setTransition(R.id.micOffFromInvisible);
145                     break;
146                 case ACTIVE_INIT:
147                     if (DEBUG) Log.d(TAG, "setTransition: micOffFromActiveInit");
148                     setTransition(R.id.micOffFromActiveInit);
149                     break;
150                 case ACTIVE:
151                     if (DEBUG) Log.d(TAG, "setTransition: micOffFromActive");
152                     setTransition(R.id.micOffFromActive);
153                     break;
154                 case INACTIVE:
155                     if (DEBUG) Log.d(TAG, "setTransition: micOffFromInactive");
156                     setTransition(R.id.micOffFromInactive);
157                     break;
158                 default:
159                     return;
160             }
161         }
162 
163         mExecutor.shutdownNow();
164         mExecutor = Executors.newSingleThreadScheduledExecutor();
165 
166         // TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
167 
168         // When microphone is off, mic privacy chip is always visible.
169         if (!mIsMicrophoneEnabled) setVisibility(View.VISIBLE);
170         setContentDescription(!mIsMicrophoneEnabled);
171         mCurrentTransitionState = mIsMicrophoneEnabled ? MicPrivacyChip.AnimationStates.INVISIBLE
172                 : MicPrivacyChip.AnimationStates.MICROPHONE_OFF;
173         transitionToEnd();
174         // When microphone is on, after animation we hide mic privacy chip until mic is next used.
175         if (mIsMicrophoneEnabled) setVisibility(View.GONE);
176     }
177 
setContentDescription(boolean isMicOff)178     private void setContentDescription(boolean isMicOff) {
179         String contentDescription;
180         if (isMicOff) {
181             contentDescription = getResources().getString(R.string.mic_privacy_chip_off_content);
182         } else {
183             contentDescription = getResources().getString(
184                     R.string.ongoing_privacy_chip_content_multiple_apps, TYPES_TEXT_MICROPHONE);
185         }
186 
187         setContentDescription(contentDescription);
188     }
189 
190     /**
191      * Starts reveal animation for Mic Privacy Chip.
192      */
193     @UiThread
animateIn()194     public void animateIn() {
195         if (!mIsInflated) {
196             if (DEBUG) Log.d(TAG, "Layout not inflated");
197 
198             return;
199         }
200 
201         if (mCurrentTransitionState == null) {
202             if (DEBUG) Log.d(TAG, "Current transition state is null or empty.");
203 
204             return;
205         }
206 
207         switch (mCurrentTransitionState) {
208             case INVISIBLE:
209                 if (DEBUG) {
210                     Log.d(TAG, mIsMicrophoneEnabled ? "setTransition: activeInitFromInvisible"
211                             : "setTransition: micOffFromInvisible");
212                 }
213                 setTransition(mIsMicrophoneEnabled ? R.id.activeInitFromInvisible
214                         : R.id.micOffFromInvisible);
215                 break;
216             case INACTIVE:
217                 if (DEBUG) {
218                     Log.d(TAG, mIsMicrophoneEnabled ? "setTransition: activeInitFromInactive"
219                             : "setTransition: micOffFromInactive");
220                 }
221 
222                 setTransition(mIsMicrophoneEnabled ? R.id.activeInitFromInactive
223                         : R.id.micOffFromInactive);
224                 break;
225             case MICROPHONE_OFF:
226                 if (DEBUG) {
227                     Log.d(TAG, mIsMicrophoneEnabled ? "setTransition: activeInitFromMicOff"
228                             : "No Transition.");
229                 }
230 
231                 if (!mIsMicrophoneEnabled) {
232                     return;
233                 }
234 
235                 setTransition(R.id.activeInitFromMicOff);
236                 break;
237             default:
238                 if (DEBUG) {
239                     Log.d(TAG, "Early exit, mCurrentTransitionState= "
240                             + mCurrentTransitionState);
241                 }
242 
243                 return;
244         }
245 
246         mExecutor.shutdownNow();
247         mExecutor = Executors.newSingleThreadScheduledExecutor();
248 
249         // TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
250         setContentDescription(false);
251         setVisibility(View.VISIBLE);
252         transitionToEnd();
253         mCurrentTransitionState = AnimationStates.ACTIVE_INIT;
254         if (mIsMicrophoneEnabled) {
255             mExecutor.schedule(MicPrivacyChip.this::animateToOrangeCircle, mDelayPillToCircle,
256                     TimeUnit.MILLISECONDS);
257         }
258     }
259 
260     // TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
animateToOrangeCircle()261     private void animateToOrangeCircle() {
262         setTransition(R.id.activeFromActiveInit);
263 
264         // Since this is launched using a {@link ScheduledExecutorService}, its UI based elements
265         // need to execute on main executor.
266         getContext().getMainExecutor().execute(() -> {
267             mCurrentTransitionState = AnimationStates.ACTIVE;
268             transitionToEnd();
269         });
270     }
271 
272     /**
273      * Starts conceal animation for Mic Privacy Chip.
274      */
275     @UiThread
animateOut()276     public void animateOut() {
277         if (!mIsInflated) {
278             if (DEBUG) Log.d(TAG, "Layout not inflated");
279 
280             return;
281         }
282 
283         switch (mCurrentTransitionState) {
284             case ACTIVE_INIT:
285                 if (DEBUG) Log.d(TAG, "setTransition: inactiveFromActiveInit");
286 
287                 setTransition(R.id.inactiveFromActiveInit);
288                 break;
289             case ACTIVE:
290                 if (DEBUG) Log.d(TAG, "setTransition: inactiveFromActive");
291 
292                 setTransition(R.id.inactiveFromActive);
293                 break;
294             default:
295                 if (DEBUG) {
296                     Log.d(TAG, "Early exit, mCurrentTransitionState= "
297                             + mCurrentTransitionState);
298                 }
299 
300                 return;
301         }
302 
303         mExecutor.shutdownNow();
304         mExecutor = Executors.newSingleThreadScheduledExecutor();
305 
306         if (mCurrentTransitionState.equals(AnimationStates.MICROPHONE_OFF)) {
307             mCurrentTransitionState = AnimationStates.INACTIVE;
308             mExecutor.schedule(MicPrivacyChip.this::reset, mDelayToNoMicUsage,
309                     TimeUnit.MILLISECONDS);
310             return;
311         }
312 
313         // TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
314         mCurrentTransitionState = AnimationStates.INACTIVE;
315         transitionToEnd();
316         mExecutor.schedule(MicPrivacyChip.this::reset, mDelayToNoMicUsage,
317                 TimeUnit.MILLISECONDS);
318     }
319 
320     // TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
reset()321     private void reset() {
322         if (mIsMicrophoneEnabled) {
323             if (DEBUG) Log.d(TAG, "setTransition: invisibleFromInactive");
324 
325             setTransition(R.id.invisibleFromInactive);
326         } else {
327             if (DEBUG) Log.d(TAG, "setTransition: invisibleFromMicOff");
328 
329             setTransition(R.id.invisibleFromMicOff);
330         }
331 
332         // Since this is launched using a {@link ScheduledExecutorService}, its UI based elements
333         // need to execute on main executor.
334         getContext().getMainExecutor().execute(() -> {
335             mCurrentTransitionState = AnimationStates.INVISIBLE;
336             transitionToEnd();
337             setVisibility(View.GONE);
338         });
339     }
340 
341     private enum AnimationStates {
342         INVISIBLE,
343         ACTIVE_INIT,
344         ACTIVE,
345         INACTIVE,
346         MICROPHONE_OFF,
347     }
348 }
349