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