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