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