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