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