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