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