• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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