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