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 android.widget;
18 
19 import static com.android.internal.util.Preconditions.checkState;
20 
21 import android.annotation.Nullable;
22 import android.app.INotificationManager;
23 import android.app.ITransientNotificationCallback;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.PixelFormat;
28 import android.graphics.drawable.Drawable;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.util.Log;
32 import android.view.Gravity;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.WindowManager;
36 import android.view.accessibility.AccessibilityEvent;
37 import android.view.accessibility.AccessibilityManager;
38 import android.view.accessibility.IAccessibilityManager;
39 
40 import com.android.internal.R;
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.util.ArrayUtils;
43 
44 import java.lang.ref.WeakReference;
45 
46 /**
47  * Class responsible for toast presentation inside app's process and in system UI.
48  *
49  * @hide
50  */
51 public class ToastPresenter {
52     private static final String TAG = "ToastPresenter";
53     private static final String WINDOW_TITLE = "Toast";
54 
55     // exclusively used to guarantee window timeouts
56     private static final long SHORT_DURATION_TIMEOUT = 4000;
57     private static final long LONG_DURATION_TIMEOUT = 7000;
58 
59     @VisibleForTesting
60     public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification;
61     @VisibleForTesting
62     public static final int TEXT_TOAST_LAYOUT_WITH_ICON = R.layout.transient_notification_with_icon;
63 
64     /**
65      * Returns the default text toast view for message {@code text}.
66      */
getTextToastView(Context context, CharSequence text)67     public static View getTextToastView(Context context, CharSequence text) {
68         View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
69         TextView textView = view.findViewById(com.android.internal.R.id.message);
70         textView.setText(text);
71         return view;
72     }
73 
74     /**
75      * Returns the default icon text toast view for message {@code text} and the icon {@code icon}.
76      */
getTextToastViewWithIcon(Context context, CharSequence text, Drawable icon)77     public static View getTextToastViewWithIcon(Context context, CharSequence text, Drawable icon) {
78         if (icon == null) {
79             return getTextToastView(context, text);
80         }
81 
82         View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT_WITH_ICON, null);
83         TextView textView = view.findViewById(com.android.internal.R.id.message);
84         textView.setText(text);
85         ImageView imageView = view.findViewById(com.android.internal.R.id.icon);
86         if (imageView != null) {
87             imageView.setImageDrawable(icon);
88         }
89         return view;
90     }
91 
92     private final WeakReference<Context> mContext;
93     private final Resources mResources;
94     private final WeakReference<WindowManager> mWindowManager;
95     private final IAccessibilityManager mAccessibilityManagerService;
96     private final INotificationManager mNotificationManager;
97     private final String mPackageName;
98     private final String mContextPackageName;
99     private final WindowManager.LayoutParams mParams;
100     @Nullable private View mView;
101     @Nullable private IBinder mToken;
102 
ToastPresenter(Context context, IAccessibilityManager accessibilityManager, INotificationManager notificationManager, String packageName)103     public ToastPresenter(Context context, IAccessibilityManager accessibilityManager,
104             INotificationManager notificationManager, String packageName) {
105         mContext = new WeakReference<>(context);
106         mResources = context.getResources();
107         mWindowManager = new WeakReference<>(context.getSystemService(WindowManager.class));
108         mNotificationManager = notificationManager;
109         mPackageName = packageName;
110         mContextPackageName = context.getPackageName();
111         mParams = createLayoutParams();
112         mAccessibilityManagerService = accessibilityManager;
113     }
114 
getPackageName()115     public String getPackageName() {
116         return mPackageName;
117     }
118 
getLayoutParams()119     public WindowManager.LayoutParams getLayoutParams() {
120         return mParams;
121     }
122 
123     /**
124      * Returns the {@link View} being shown at the moment or {@code null} if no toast is being
125      * displayed.
126      */
127     @Nullable
getView()128     public View getView() {
129         return mView;
130     }
131 
132     /**
133      * Returns the {@link IBinder} token used to display the toast or {@code null} if there is no
134      * toast being shown at the moment.
135      */
136     @Nullable
getToken()137     public IBinder getToken() {
138         return mToken;
139     }
140 
141     /**
142      * Creates {@link WindowManager.LayoutParams} with default values for toasts.
143      */
createLayoutParams()144     private WindowManager.LayoutParams createLayoutParams() {
145         WindowManager.LayoutParams params = new WindowManager.LayoutParams();
146         params.height = WindowManager.LayoutParams.WRAP_CONTENT;
147         params.width = WindowManager.LayoutParams.WRAP_CONTENT;
148         params.format = PixelFormat.TRANSLUCENT;
149         params.windowAnimations = R.style.Animation_Toast;
150         params.type = WindowManager.LayoutParams.TYPE_TOAST;
151         params.setFitInsetsIgnoringVisibility(true);
152         params.setTitle(WINDOW_TITLE);
153         params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
154                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
155                 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
156         setShowForAllUsersIfApplicable(params, mPackageName);
157         return params;
158     }
159 
160     /**
161      * Customizes {@code params} according to other parameters, ready to be passed to {@link
162      * WindowManager#addView(View, ViewGroup.LayoutParams)}.
163      */
adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, boolean removeWindowAnimations)164     private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
165             int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
166             float verticalMargin, boolean removeWindowAnimations) {
167         Configuration config = mResources.getConfiguration();
168         int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
169         params.gravity = absGravity;
170         if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
171             params.horizontalWeight = 1.0f;
172         }
173         if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
174             params.verticalWeight = 1.0f;
175         }
176         params.x = xOffset;
177         params.y = yOffset;
178         params.horizontalMargin = horizontalMargin;
179         params.verticalMargin = verticalMargin;
180         params.packageName = mContextPackageName;
181         params.hideTimeoutMilliseconds =
182                 (duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
183         params.token = windowToken;
184 
185         if (removeWindowAnimations && params.windowAnimations == R.style.Animation_Toast) {
186             params.windowAnimations = 0;
187         }
188     }
189 
190     /**
191      * Update the LayoutParameters of the currently showing toast view. This is used for layout
192      * updates based on orientation changes.
193      */
updateLayoutParams(int xOffset, int yOffset, float horizontalMargin, float verticalMargin, int gravity)194     public void updateLayoutParams(int xOffset, int yOffset, float horizontalMargin,
195             float verticalMargin, int gravity) {
196         checkState(mView != null, "Toast must be showing to update its layout parameters.");
197         Configuration config = mResources.getConfiguration();
198         mParams.gravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
199         mParams.x = xOffset;
200         mParams.y = yOffset;
201         mParams.horizontalMargin = horizontalMargin;
202         mParams.verticalMargin = verticalMargin;
203         mView.setLayoutParams(mParams);
204     }
205 
206     /**
207      * Sets {@link WindowManager.LayoutParams#SYSTEM_FLAG_SHOW_FOR_ALL_USERS} flag if {@code
208      * packageName} is a cross-user package.
209      *
210      * <p>Implementation note:
211      *     This code is safe to be executed in SystemUI and the app's process:
212      *         <li>SystemUI: It's running on a trusted domain so apps can't tamper with it. SystemUI
213      *             has the permission INTERNAL_SYSTEM_WINDOW needed by the flag, so SystemUI can add
214      *             the flag on behalf of those packages, which all contain INTERNAL_SYSTEM_WINDOW
215      *             permission.
216      *         <li>App: The flag being added is protected behind INTERNAL_SYSTEM_WINDOW permission
217      *             and any app can already add that flag via getWindowParams() if it has that
218      *             permission, so we are just doing this automatically for cross-user packages.
219      */
setShowForAllUsersIfApplicable(WindowManager.LayoutParams params, String packageName)220     private void setShowForAllUsersIfApplicable(WindowManager.LayoutParams params,
221             String packageName) {
222         if (isCrossUserPackage(packageName)) {
223             params.privateFlags = WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
224         }
225     }
226 
isCrossUserPackage(String packageName)227     private boolean isCrossUserPackage(String packageName) {
228         String[] packages = mResources.getStringArray(R.array.config_toastCrossUserPackages);
229         return ArrayUtils.contains(packages, packageName);
230     }
231 
232     /**
233      * Shows the toast in {@code view} with the parameters passed and callback {@code callback}.
234      * Uses window animations to animate the toast.
235      */
show(View view, IBinder token, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, @Nullable ITransientNotificationCallback callback)236     public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
237             int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
238             @Nullable ITransientNotificationCallback callback) {
239         show(view, token, windowToken, duration, gravity, xOffset, yOffset, horizontalMargin,
240                 verticalMargin, callback, false /* removeWindowAnimations */);
241     }
242 
243     /**
244      * Shows the toast in {@code view} with the parameters passed and callback {@code callback}.
245      * Can optionally remove window animations from the toast window.
246      */
show(View view, IBinder token, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, @Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations)247     public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
248             int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
249             @Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
250         checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
251         mView = view;
252         mToken = token;
253 
254         adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
255                 horizontalMargin, verticalMargin, removeWindowAnimations);
256         addToastView();
257         trySendAccessibilityEvent(mView, mPackageName);
258         if (callback != null) {
259             try {
260                 callback.onToastShown();
261             } catch (RemoteException e) {
262                 Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
263             }
264         }
265     }
266 
267     /**
268      * Hides toast that was shown using {@link #show(View, IBinder, IBinder, int,
269      * int, int, int, float, float, ITransientNotificationCallback)}.
270      *
271      * <p>This method has to be called on the same thread on which {@link #show(View, IBinder,
272      * IBinder, int, int, int, int, float, float, ITransientNotificationCallback)} was called.
273      */
hide(@ullable ITransientNotificationCallback callback)274     public void hide(@Nullable ITransientNotificationCallback callback) {
275         checkState(mView != null, "No toast to hide.");
276 
277         final WindowManager windowManager = mWindowManager.get();
278         if (mView.getParent() != null && windowManager != null) {
279             windowManager.removeViewImmediate(mView);
280         }
281         try {
282             mNotificationManager.finishToken(mPackageName, mToken);
283         } catch (RemoteException e) {
284             Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e);
285         }
286         if (callback != null) {
287             try {
288                 callback.onToastHidden();
289             } catch (RemoteException e) {
290                 Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()",
291                         e);
292             }
293         }
294         mView = null;
295         mToken = null;
296     }
297 
298     /**
299      * Sends {@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED} event if accessibility is
300      * enabled.
301      */
trySendAccessibilityEvent(View view, String packageName)302     public void trySendAccessibilityEvent(View view, String packageName) {
303         final Context context = mContext.get();
304         if (context == null) {
305             return;
306         }
307 
308         // We obtain AccessibilityManager manually via its constructor instead of using method
309         // AccessibilityManager.getInstance() for 2 reasons:
310         //   1. We want to be able to inject IAccessibilityManager in tests to verify behavior.
311         //   2. getInstance() caches the instance for the process even if we pass a different
312         //      context to it. This is problematic for multi-user because callers can pass a context
313         //      created via Context.createContextAsUser().
314         final AccessibilityManager accessibilityManager = new AccessibilityManager(context,
315                     mAccessibilityManagerService, context.getUserId());
316 
317         if (!accessibilityManager.isEnabled()) {
318             accessibilityManager.removeClient();
319             return;
320         }
321         AccessibilityEvent event = AccessibilityEvent.obtain(
322                 AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
323         event.setClassName(Toast.class.getName());
324         event.setPackageName(packageName);
325         view.dispatchPopulateAccessibilityEvent(event);
326         accessibilityManager.sendAccessibilityEvent(event);
327         // Every new instance of A11yManager registers an IA11yManagerClient object with the
328         // backing service. This client isn't removed until the calling process is destroyed,
329         // causing a leak here. We explicitly remove the client.
330         accessibilityManager.removeClient();
331     }
332 
addToastView()333     private void addToastView() {
334         final WindowManager windowManager = mWindowManager.get();
335         if (windowManager == null) {
336             return;
337         }
338         if (mView.getParent() != null) {
339             windowManager.removeView(mView);
340         }
341         try {
342             windowManager.addView(mView, mParams);
343         } catch (WindowManager.BadTokenException e) {
344             // Since the notification manager service cancels the token right after it notifies us
345             // to cancel the toast there is an inherent race and we may attempt to add a window
346             // after the token has been invalidated. Let us hedge against that.
347             Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
348             return;
349         }
350     }
351 }
352