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