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.systemui.toast; 18 19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.annotation.MainThread; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.INotificationManager; 27 import android.app.ITransientNotificationCallback; 28 import android.content.Context; 29 import android.content.res.Configuration; 30 import android.hardware.display.DisplayManager; 31 import android.os.IBinder; 32 import android.os.ServiceManager; 33 import android.os.UserHandle; 34 import android.util.Log; 35 import android.view.Display; 36 import android.view.accessibility.AccessibilityManager; 37 import android.view.accessibility.IAccessibilityManager; 38 import android.widget.ToastPresenter; 39 40 import androidx.annotation.VisibleForTesting; 41 42 import com.android.systemui.CoreStartable; 43 import com.android.systemui.dagger.SysUISingleton; 44 import com.android.systemui.statusbar.CommandQueue; 45 import com.android.systemui.statusbar.policy.ConfigurationController; 46 47 import java.util.Objects; 48 49 import javax.inject.Inject; 50 51 /** 52 * Controls display of text toasts. 53 */ 54 @SysUISingleton 55 public class ToastUI implements 56 CoreStartable, 57 ConfigurationController.ConfigurationListener, 58 CommandQueue.Callbacks { 59 // values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY 60 private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds 61 private static final int TOAST_SHORT_TIME = 2000; // 2 seconds 62 63 private static final String TAG = "ToastUI"; 64 65 private final Context mContext; 66 private final CommandQueue mCommandQueue; 67 private final INotificationManager mNotificationManager; 68 private final IAccessibilityManager mIAccessibilityManager; 69 private final AccessibilityManager mAccessibilityManager; 70 private final ToastFactory mToastFactory; 71 private final ToastLogger mToastLogger; 72 @Nullable private ToastPresenter mPresenter; 73 @Nullable private ITransientNotificationCallback mCallback; 74 @VisibleForTesting ToastOutAnimatorListener mToastOutAnimatorListener; 75 76 @VisibleForTesting SystemUIToast mToast; 77 private int mOrientation = ORIENTATION_PORTRAIT; 78 79 @Inject ToastUI( Context context, CommandQueue commandQueue, ToastFactory toastFactory, ToastLogger toastLogger)80 public ToastUI( 81 Context context, 82 CommandQueue commandQueue, 83 ToastFactory toastFactory, 84 ToastLogger toastLogger) { 85 this(context, commandQueue, 86 INotificationManager.Stub.asInterface( 87 ServiceManager.getService(Context.NOTIFICATION_SERVICE)), 88 IAccessibilityManager.Stub.asInterface( 89 ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)), 90 toastFactory, 91 toastLogger); 92 } 93 94 @VisibleForTesting ToastUI(Context context, CommandQueue commandQueue, INotificationManager notificationManager, @Nullable IAccessibilityManager accessibilityManager, ToastFactory toastFactory, ToastLogger toastLogger )95 ToastUI(Context context, CommandQueue commandQueue, INotificationManager notificationManager, 96 @Nullable IAccessibilityManager accessibilityManager, 97 ToastFactory toastFactory, ToastLogger toastLogger 98 ) { 99 mContext = context; 100 mCommandQueue = commandQueue; 101 mNotificationManager = notificationManager; 102 mIAccessibilityManager = accessibilityManager; 103 mToastFactory = toastFactory; 104 mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); 105 mToastLogger = toastLogger; 106 } 107 108 @Override start()109 public void start() { 110 mCommandQueue.addCallback(this); 111 } 112 113 @Override 114 @MainThread showToast(int uid, String packageName, IBinder token, CharSequence text, IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback, int displayId)115 public void showToast(int uid, String packageName, IBinder token, CharSequence text, 116 IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback, 117 int displayId) { 118 Runnable showToastRunnable = () -> { 119 UserHandle userHandle = UserHandle.getUserHandleForUid(uid); 120 Context context = mContext.createContextAsUser(userHandle, 0); 121 122 DisplayManager mDisplayManager = mContext.getSystemService(DisplayManager.class); 123 Display display = mDisplayManager.getDisplay(displayId); 124 if (display == null) { 125 // Display for which this toast was scheduled for is no longer available. 126 mToastLogger.logOnSkipToastForInvalidDisplay(packageName, token.toString(), 127 displayId); 128 return; 129 } 130 Context displayContext = context.createDisplayContext(display); 131 mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName, 132 userHandle.getIdentifier(), mOrientation); 133 134 if (mToast.getInAnimation() != null) { 135 mToast.getInAnimation().start(); 136 } 137 138 mCallback = callback; 139 mPresenter = new ToastPresenter(displayContext, mIAccessibilityManager, 140 mNotificationManager, packageName); 141 // Set as trusted overlay so touches can pass through toasts 142 mPresenter.getLayoutParams().setTrustedOverlay(); 143 mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString()); 144 mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(), 145 mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(), 146 mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation()); 147 }; 148 149 if (mToastOutAnimatorListener != null) { 150 // if we're currently animating out a toast, show new toast after prev toast is hidden 151 mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable); 152 } else if (mPresenter != null) { 153 // if there's a toast already showing that we haven't tried hiding yet, hide it and 154 // then show the next toast after its hidden animation is done 155 hideCurrentToast(showToastRunnable); 156 } else { 157 // else, show this next toast immediately 158 showToastRunnable.run(); 159 } 160 } 161 162 @Override 163 @MainThread hideToast(String packageName, IBinder token)164 public void hideToast(String packageName, IBinder token) { 165 if (mPresenter == null || !Objects.equals(mPresenter.getPackageName(), packageName) 166 || !Objects.equals(mPresenter.getToken(), token)) { 167 Log.w(TAG, "Attempt to hide non-current toast from package " + packageName); 168 return; 169 } 170 mToastLogger.logOnHideToast(packageName, token.toString()); 171 hideCurrentToast(null); 172 } 173 174 @MainThread hideCurrentToast(Runnable runnable)175 private void hideCurrentToast(Runnable runnable) { 176 if (mToast.getOutAnimation() != null) { 177 Animator animator = mToast.getOutAnimation(); 178 mToastOutAnimatorListener = new ToastOutAnimatorListener(mPresenter, mCallback, 179 runnable, animator); 180 animator.addListener(mToastOutAnimatorListener); 181 animator.start(); 182 } else { 183 mPresenter.hide(mCallback); 184 if (runnable != null) { 185 runnable.run(); 186 } 187 } 188 mToast = null; 189 mPresenter = null; 190 mCallback = null; 191 } 192 193 @Override onConfigChanged(Configuration newConfig)194 public void onConfigChanged(Configuration newConfig) { 195 if (newConfig.orientation != mOrientation) { 196 mOrientation = newConfig.orientation; 197 if (mToast != null) { 198 mToastLogger.logOrientationChange(mToast.mText.toString(), 199 mOrientation == ORIENTATION_PORTRAIT); 200 mToast.onOrientationChange(mOrientation); 201 mPresenter.updateLayoutParams( 202 mToast.getXOffset(), 203 mToast.getYOffset(), 204 mToast.getHorizontalMargin(), 205 mToast.getVerticalMargin(), 206 mToast.getGravity()); 207 } 208 } 209 } 210 211 /** 212 * Once the out animation for a toast is finished, start showing the next toast. 213 */ 214 class ToastOutAnimatorListener extends AnimatorListenerAdapter { 215 final ToastPresenter mPrevPresenter; 216 final ITransientNotificationCallback mPrevCallback; 217 @Nullable Runnable mShowNextToastRunnable; 218 @NonNull private final Animator mAnimator; 219 ToastOutAnimatorListener( @onNull ToastPresenter presenter, @NonNull ITransientNotificationCallback callback, @Nullable Runnable runnable, @NonNull Animator animator)220 ToastOutAnimatorListener( 221 @NonNull ToastPresenter presenter, 222 @NonNull ITransientNotificationCallback callback, 223 @Nullable Runnable runnable, 224 @NonNull Animator animator) { 225 mPrevPresenter = presenter; 226 mPrevCallback = callback; 227 mShowNextToastRunnable = runnable; 228 mAnimator = animator; 229 } 230 setShowNextToastRunnable(Runnable runnable)231 void setShowNextToastRunnable(Runnable runnable) { 232 mShowNextToastRunnable = runnable; 233 } 234 235 @Override onAnimationEnd(Animator animation)236 public void onAnimationEnd(Animator animation) { 237 mPrevPresenter.hide(mPrevCallback); 238 if (mShowNextToastRunnable != null) { 239 mShowNextToastRunnable.run(); 240 } 241 mAnimator.removeListener(this); 242 mShowNextToastRunnable = null; 243 mToastOutAnimatorListener = null; 244 } 245 } 246 } 247