• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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.server.am;
18 
19 import static com.android.server.am.UserController.USER_SWITCHING_DIALOG_ANIMATION_TIMEOUT_MSG;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.UserIdInt;
24 import android.app.Dialog;
25 import android.content.Context;
26 import android.content.pm.UserInfo;
27 import android.content.res.Resources;
28 import android.graphics.Bitmap;
29 import android.graphics.BitmapFactory;
30 import android.graphics.BitmapShader;
31 import android.graphics.Canvas;
32 import android.graphics.Paint;
33 import android.graphics.RectF;
34 import android.graphics.Shader;
35 import android.graphics.drawable.Animatable2;
36 import android.graphics.drawable.AnimatedVectorDrawable;
37 import android.graphics.drawable.Drawable;
38 import android.os.Handler;
39 import android.os.SystemProperties;
40 import android.os.Trace;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.util.Slog;
44 import android.util.TypedValue;
45 import android.view.View;
46 import android.view.Window;
47 import android.view.WindowManager;
48 import android.view.animation.AlphaAnimation;
49 import android.view.animation.Animation;
50 import android.widget.ImageView;
51 import android.widget.TextView;
52 
53 import com.android.internal.R;
54 import com.android.internal.util.ObjectUtils;
55 import com.android.internal.util.UserIcons;
56 
57 import java.util.Objects;
58 import java.util.concurrent.atomic.AtomicBoolean;
59 
60 /**
61  * Dialog to show during the user switch. This dialog shows target user's name and their profile
62  * picture with a circular spinner animation around it if the animations for this dialog are not
63  * disabled. And covers the whole screen so that all the UI jank caused by the switch are hidden.
64  */
65 class UserSwitchingDialog extends Dialog {
66     private static final String TAG = "UserSwitchingDialog";
67     private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
68 
69     // User switching doesn't happen that frequently, so it doesn't hurt to have it always on
70     protected static final boolean DEBUG = true;
71 
72     private static final long DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS = 300;
73     private final boolean mDisableAnimations;
74 
75     // Time to wait for the onAnimationEnd() callbacks before moving on
76     private static final int ANIMATION_TIMEOUT_MS = 1000;
77     private final Handler mHandler;
78 
79     protected final UserInfo mOldUser;
80     protected final UserInfo mNewUser;
81     @Nullable
82     private final String mSwitchingFromUserMessage;
83     @Nullable
84     private final String mSwitchingToUserMessage;
85     protected final Context mContext;
86     private final int mTraceCookie;
87 
UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler, @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage)88     UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler,
89             @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage) {
90         super(context, R.style.Theme_Material_NoActionBar_Fullscreen);
91 
92         mContext = context;
93         mOldUser = oldUser;
94         mNewUser = newUser;
95         mHandler = handler;
96         mSwitchingFromUserMessage = switchingFromUserMessage;
97         mSwitchingToUserMessage = switchingToUserMessage;
98         mDisableAnimations = SystemProperties.getBoolean(
99                 "debug.usercontroller.disable_user_switching_dialog_animations", false);
100         mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id;
101 
102         inflateContent();
103         configureWindow();
104     }
105 
configureWindow()106     private void configureWindow() {
107         final Window window = getWindow();
108         final WindowManager.LayoutParams attrs = window.getAttributes();
109         attrs.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR |
110                 WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
111         attrs.layoutInDisplayCutoutMode =
112                 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
113         window.setAttributes(attrs);
114         window.setBackgroundDrawableResource(android.R.color.transparent);
115         window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
116         window.setDecorFitsSystemWindows(false);
117     }
118 
inflateContent()119     void inflateContent() {
120         setCancelable(false);
121         setContentView(R.layout.user_switching_dialog);
122 
123         final TextView textView = findViewById(R.id.message);
124         if (textView != null) {
125             final String message = getTextMessage();
126             textView.setAccessibilityPaneTitle(message);
127             textView.setText(message);
128         }
129 
130         final ImageView imageView = findViewById(R.id.icon);
131         if (imageView != null) {
132             imageView.setImageBitmap(getUserIconRounded());
133         }
134 
135         final ImageView progressCircular = findViewById(R.id.progress_circular);
136         if (progressCircular != null) {
137             if (mDisableAnimations) {
138                 progressCircular.setVisibility(View.GONE);
139             } else {
140                 final TypedValue value = new TypedValue();
141                 getContext().getTheme().resolveAttribute(R.attr.colorAccentPrimary, value, true);
142                 progressCircular.setColorFilter(value.data);
143             }
144         }
145     }
146 
getUserIconRounded()147     private Bitmap getUserIconRounded() {
148         final Bitmap bmp = ObjectUtils.getOrElse(BitmapFactory.decodeFile(mNewUser.iconPath),
149                 defaultUserIcon(mNewUser.id));
150         final int w = bmp.getWidth();
151         final int h = bmp.getHeight();
152         final Bitmap bmpRounded = Bitmap.createBitmap(w, h, bmp.getConfig());
153         final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
154         paint.setShader(new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
155         new Canvas(bmpRounded).drawRoundRect((new RectF(0, 0, w, h)), w / 2f, h / 2f, paint);
156         return bmpRounded;
157     }
158 
defaultUserIcon(@serIdInt int userId)159     private Bitmap defaultUserIcon(@UserIdInt int userId) {
160         final Resources res = getContext().getResources();
161         final Drawable icon = UserIcons.getDefaultUserIcon(res, userId, /* light= */ false);
162         return UserIcons.convertToBitmapAtUserIconSize(res, icon);
163     }
164 
getTextMessage()165     private String getTextMessage() {
166         final Resources res = getContext().getResources();
167 
168         if (UserManager.isDeviceInDemoMode(mContext)) {
169             return res.getString(mOldUser.isDemo()
170                     ? R.string.demo_restarting_message
171                     : R.string.demo_starting_message);
172         }
173 
174         if (mSwitchingFromUserMessage != null || mSwitchingToUserMessage != null) {
175             if (mSwitchingFromUserMessage != null && mSwitchingToUserMessage != null) {
176                 return mSwitchingFromUserMessage + " " + mSwitchingToUserMessage;
177             }
178             return Objects.requireNonNullElse(mSwitchingFromUserMessage, mSwitchingToUserMessage);
179         }
180 
181         return res.getString(R.string.user_switching_message, mNewUser.name);
182     }
183 
184     @Override
show()185     public void show() {
186         asyncTraceBegin("dialog", 0);
187         super.show();
188     }
189 
190     @Override
dismiss()191     public void dismiss() {
192         super.dismiss();
193         asyncTraceEnd("dialog", 0);
194     }
195 
show(@onNull Runnable onShown)196     public void show(@NonNull Runnable onShown) {
197         if (DEBUG) Slog.d(TAG, "show called");
198         show();
199         startShowAnimation(() -> {
200             onShown.run();
201         });
202     }
203 
dismiss(@ullable Runnable onDismissed)204     public void dismiss(@Nullable Runnable onDismissed) {
205         if (DEBUG) Slog.d(TAG, "dismiss called");
206         if (onDismissed == null) {
207             // no animation needed
208             dismiss();
209         } else {
210             startDismissAnimation(() -> {
211                 dismiss();
212                 onDismissed.run();
213             });
214         }
215     }
216 
startShowAnimation(Runnable onAnimationEnd)217     private void startShowAnimation(Runnable onAnimationEnd) {
218         if (mDisableAnimations) {
219             onAnimationEnd.run();
220             return;
221         }
222         asyncTraceBegin("showAnimation", 1);
223         startDialogAnimation("show", new AlphaAnimation(0, 1), () -> {
224             asyncTraceEnd("showAnimation", 1);
225 
226             asyncTraceBegin("spinnerAnimation", 2);
227             startProgressAnimation(() -> {
228                 asyncTraceEnd("spinnerAnimation", 2);
229 
230                 onAnimationEnd.run();
231             });
232         });
233     }
234 
startDismissAnimation(Runnable onAnimationEnd)235     private void startDismissAnimation(Runnable onAnimationEnd) {
236         if (mDisableAnimations) {
237             // animations are disabled or screen is frozen, no need to play an animation
238             onAnimationEnd.run();
239             return;
240         }
241         asyncTraceBegin("dismissAnimation", 3);
242         startDialogAnimation("dismiss", new AlphaAnimation(1, 0), () -> {
243             asyncTraceEnd("dismissAnimation", 3);
244 
245             onAnimationEnd.run();
246         });
247     }
248 
startProgressAnimation(Runnable onAnimationEnd)249     private void startProgressAnimation(Runnable onAnimationEnd) {
250         final AnimatedVectorDrawable avd = getSpinnerAVD();
251         if (mDisableAnimations || avd == null) {
252             onAnimationEnd.run();
253             return;
254         }
255         final Runnable onAnimationEndWithTimeout = animationWithTimeout("spinner", onAnimationEnd);
256         avd.registerAnimationCallback(new Animatable2.AnimationCallback() {
257             @Override
258             public void onAnimationEnd(Drawable drawable) {
259                 onAnimationEndWithTimeout.run();
260             }
261         });
262         avd.start();
263     }
264 
getSpinnerAVD()265     private AnimatedVectorDrawable getSpinnerAVD() {
266         final ImageView view = findViewById(R.id.progress_circular);
267         if (view != null) {
268             final Drawable drawable = view.getDrawable();
269             if (drawable instanceof AnimatedVectorDrawable) {
270                 return (AnimatedVectorDrawable) drawable;
271             }
272         }
273         return null;
274     }
275 
startDialogAnimation(String name, Animation animation, Runnable onAnimationEnd)276     private void startDialogAnimation(String name, Animation animation, Runnable onAnimationEnd) {
277         final View view = findViewById(R.id.content);
278         if (mDisableAnimations || view == null) {
279             onAnimationEnd.run();
280             return;
281         }
282         final Runnable onAnimationEndWithTimeout = animationWithTimeout(name, onAnimationEnd);
283         animation.setDuration(DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS);
284         animation.setAnimationListener(new Animation.AnimationListener() {
285             @Override
286             public void onAnimationStart(Animation animation) {
287 
288             }
289 
290             @Override
291             public void onAnimationEnd(Animation animation) {
292                 onAnimationEndWithTimeout.run();
293             }
294 
295             @Override
296             public void onAnimationRepeat(Animation animation) {
297 
298             }
299         });
300         view.startAnimation(animation);
301     }
302 
animationWithTimeout(String name, Runnable onAnimationEnd)303     private Runnable animationWithTimeout(String name, Runnable onAnimationEnd) {
304         final AtomicBoolean isFirst = new AtomicBoolean(true);
305         final Runnable onAnimationEndOrTimeout = () -> {
306             if (isFirst.getAndSet(false)) {
307                 mHandler.removeMessages(USER_SWITCHING_DIALOG_ANIMATION_TIMEOUT_MSG);
308                 onAnimationEnd.run();
309             }
310         };
311         mHandler.postDelayed(() -> {
312             Slog.w(TAG, name + " animation not completed in " + ANIMATION_TIMEOUT_MS + " ms");
313             onAnimationEndOrTimeout.run();
314         }, USER_SWITCHING_DIALOG_ANIMATION_TIMEOUT_MSG, ANIMATION_TIMEOUT_MS);
315 
316         return onAnimationEndOrTimeout;
317     }
318 
asyncTraceBegin(String subTag, int subCookie)319     private void asyncTraceBegin(String subTag, int subCookie) {
320         if (DEBUG) Slog.d(TAG, "asyncTraceBegin-" + subTag);
321         Trace.asyncTraceBegin(TRACE_TAG, TAG + subTag, mTraceCookie + subCookie);
322     }
323 
asyncTraceEnd(String subTag, int subCookie)324     private void asyncTraceEnd(String subTag, int subCookie) {
325         Trace.asyncTraceEnd(TRACE_TAG, TAG + subTag, mTraceCookie + subCookie);
326         if (DEBUG) Slog.d(TAG, "asyncTraceEnd-" + subTag);
327     }
328 }
329