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