diff options
Diffstat (limited to 'core/java/android/widget/ToastPresenter.java')
| -rw-r--r-- | core/java/android/widget/ToastPresenter.java | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/core/java/android/widget/ToastPresenter.java b/core/java/android/widget/ToastPresenter.java new file mode 100644 index 000000000000..fb5d55dd2141 --- /dev/null +++ b/core/java/android/widget/ToastPresenter.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import static com.android.internal.util.Preconditions.checkState; + +import android.annotation.Nullable; +import android.app.INotificationManager; +import android.app.ITransientNotificationCallback; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.IAccessibilityManager; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; + +/** + * Class responsible for toast presentation inside app's process and in system UI. + * + * @hide + */ +public class ToastPresenter { + private static final String TAG = "ToastPresenter"; + private static final String WINDOW_TITLE = "Toast"; + private static final long SHORT_DURATION_TIMEOUT = 4000; + private static final long LONG_DURATION_TIMEOUT = 7000; + + @VisibleForTesting + public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification; + + /** + * Returns the default text toast view for message {@code text}. + */ + public static View getTextToastView(Context context, CharSequence text) { + View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null); + TextView textView = view.findViewById(com.android.internal.R.id.message); + textView.setText(text); + return view; + } + + private final Context mContext; + private final Resources mResources; + private final WindowManager mWindowManager; + private final AccessibilityManager mAccessibilityManager; + private final INotificationManager mNotificationManager; + private final String mPackageName; + private final WindowManager.LayoutParams mParams; + @Nullable private View mView; + @Nullable private IBinder mToken; + + public ToastPresenter(Context context, IAccessibilityManager accessibilityManager, + INotificationManager notificationManager, String packageName) { + mContext = context; + mResources = context.getResources(); + mWindowManager = context.getSystemService(WindowManager.class); + mNotificationManager = notificationManager; + mPackageName = packageName; + + // We obtain AccessibilityManager manually via its constructor instead of using method + // AccessibilityManager.getInstance() for 2 reasons: + // 1. We want to be able to inject IAccessibilityManager in tests to verify behavior. + // 2. getInstance() caches the instance for the process even if we pass a different + // context to it. This is problematic for multi-user because callers can pass a context + // created via Context.createContextAsUser(). + mAccessibilityManager = new AccessibilityManager(context, accessibilityManager, + context.getUserId()); + + mParams = createLayoutParams(); + } + + public String getPackageName() { + return mPackageName; + } + + public WindowManager.LayoutParams getLayoutParams() { + return mParams; + } + + /** + * Returns the {@link View} being shown at the moment or {@code null} if no toast is being + * displayed. + */ + @Nullable + public View getView() { + return mView; + } + + /** + * Returns the {@link IBinder} token used to display the toast or {@code null} if there is no + * toast being shown at the moment. + */ + @Nullable + public IBinder getToken() { + return mToken; + } + + /** + * Creates {@link WindowManager.LayoutParams} with default values for toasts. + */ + private WindowManager.LayoutParams createLayoutParams() { + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + params.width = WindowManager.LayoutParams.WRAP_CONTENT; + params.format = PixelFormat.TRANSLUCENT; + params.windowAnimations = R.style.Animation_Toast; + params.type = WindowManager.LayoutParams.TYPE_TOAST; + params.setFitInsetsIgnoringVisibility(true); + params.setTitle(WINDOW_TITLE); + params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + setShowForAllUsersIfApplicable(params, mPackageName); + return params; + } + + /** + * Customizes {@code params} according to other parameters, ready to be passed to {@link + * WindowManager#addView(View, ViewGroup.LayoutParams)}. + */ + private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken, + int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, + float verticalMargin) { + Configuration config = mResources.getConfiguration(); + int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection()); + params.gravity = absGravity; + if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { + params.horizontalWeight = 1.0f; + } + if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { + params.verticalWeight = 1.0f; + } + params.x = xOffset; + params.y = yOffset; + params.horizontalMargin = horizontalMargin; + params.verticalMargin = verticalMargin; + params.packageName = mContext.getPackageName(); + params.hideTimeoutMilliseconds = + (duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; + params.token = windowToken; + } + + /** + * Sets {@link WindowManager.LayoutParams#SYSTEM_FLAG_SHOW_FOR_ALL_USERS} flag if {@code + * packageName} is a cross-user package. + * + * <p>Implementation note: + * This code is safe to be executed in SystemUI and the app's process: + * <li>SystemUI: It's running on a trusted domain so apps can't tamper with it. SystemUI + * has the permission INTERNAL_SYSTEM_WINDOW needed by the flag, so SystemUI can add + * the flag on behalf of those packages, which all contain INTERNAL_SYSTEM_WINDOW + * permission. + * <li>App: The flag being added is protected behind INTERNAL_SYSTEM_WINDOW permission + * and any app can already add that flag via getWindowParams() if it has that + * permission, so we are just doing this automatically for cross-user packages. + */ + private void setShowForAllUsersIfApplicable(WindowManager.LayoutParams params, + String packageName) { + if (isCrossUserPackage(packageName)) { + params.privateFlags = WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + } + } + + private boolean isCrossUserPackage(String packageName) { + String[] packages = mResources.getStringArray(R.array.config_toastCrossUserPackages); + return ArrayUtils.contains(packages, packageName); + } + + /** + * Shows the toast in {@code view} with the parameters passed and callback {@code callback}. + */ + public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity, + int xOffset, int yOffset, float horizontalMargin, float verticalMargin, + @Nullable ITransientNotificationCallback callback) { + checkState(mView == null, "Only one toast at a time is allowed, call hide() first."); + mView = view; + mToken = token; + + adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset, + horizontalMargin, verticalMargin); + if (mView.getParent() != null) { + mWindowManager.removeView(mView); + } + try { + mWindowManager.addView(mView, mParams); + } catch (WindowManager.BadTokenException e) { + // Since the notification manager service cancels the token right after it notifies us + // to cancel the toast there is an inherent race and we may attempt to add a window + // after the token has been invalidated. Let us hedge against that. + Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e); + return; + } + trySendAccessibilityEvent(mView, mPackageName); + if (callback != null) { + try { + callback.onToastShown(); + } catch (RemoteException e) { + Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e); + } + } + } + + /** + * Hides toast that was shown using {@link #show(View, IBinder, IBinder, int, + * int, int, int, float, float, ITransientNotificationCallback)}. + * + * <p>This method has to be called on the same thread on which {@link #show(View, IBinder, + * IBinder, int, int, int, int, float, float, ITransientNotificationCallback)} was called. + */ + public void hide(@Nullable ITransientNotificationCallback callback) { + checkState(mView != null, "No toast to hide."); + + if (mView.getParent() != null) { + mWindowManager.removeViewImmediate(mView); + } + try { + mNotificationManager.finishToken(mPackageName, mToken); + } catch (RemoteException e) { + Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e); + } + if (callback != null) { + try { + callback.onToastHidden(); + } catch (RemoteException e) { + Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()", e); + } + } + mView = null; + mToken = null; + } + + /** + * Sends {@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED} event if accessibility is + * enabled. + */ + public void trySendAccessibilityEvent(View view, String packageName) { + if (!mAccessibilityManager.isEnabled()) { + return; + } + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); + event.setClassName(Toast.class.getName()); + event.setPackageName(packageName); + view.dispatchPopulateAccessibilityEvent(event); + mAccessibilityManager.sendAccessibilityEvent(event); + } +} |
