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