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