blob: 42f66875e7a193b97af8a854f2894270fc65480f [file] [log] [blame]
/*
* 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 com.android.systemui.toast;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.INotificationManager;
import android.app.ITransientNotificationCallback;
import android.content.Context;
import android.content.res.Configuration;
import android.os.IBinder;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManager;
import android.widget.ToastPresenter;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.SystemUI;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.CommandQueue;
import java.util.Objects;
import javax.inject.Inject;
/**
* Controls display of text toasts.
*/
@SysUISingleton
public class ToastUI extends SystemUI implements CommandQueue.Callbacks {
// values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY
private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds
private static final int TOAST_SHORT_TIME = 2000; // 2 seconds
private static final String TAG = "ToastUI";
private final CommandQueue mCommandQueue;
private final INotificationManager mNotificationManager;
private final IAccessibilityManager mIAccessibilityManager;
private final AccessibilityManager mAccessibilityManager;
private final ToastFactory mToastFactory;
private final ToastLogger mToastLogger;
@Nullable private ToastPresenter mPresenter;
@Nullable private ITransientNotificationCallback mCallback;
private ToastOutAnimatorListener mToastOutAnimatorListener;
@VisibleForTesting SystemUIToast mToast;
private int mOrientation = ORIENTATION_PORTRAIT;
@Inject
public ToastUI(
Context context,
CommandQueue commandQueue,
ToastFactory toastFactory,
ToastLogger toastLogger) {
this(context, commandQueue,
INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE)),
IAccessibilityManager.Stub.asInterface(
ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)),
toastFactory,
toastLogger);
}
@VisibleForTesting
ToastUI(Context context, CommandQueue commandQueue, INotificationManager notificationManager,
@Nullable IAccessibilityManager accessibilityManager,
ToastFactory toastFactory, ToastLogger toastLogger
) {
super(context);
mCommandQueue = commandQueue;
mNotificationManager = notificationManager;
mIAccessibilityManager = accessibilityManager;
mToastFactory = toastFactory;
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mToastLogger = toastLogger;
}
@Override
public void start() {
mCommandQueue.addCallback(this);
}
@Override
@MainThread
public void showToast(int uid, String packageName, IBinder token, CharSequence text,
IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
Runnable showToastRunnable = () -> {
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
Context context = mContext.createContextAsUser(userHandle, 0);
mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,
userHandle.getIdentifier(), mOrientation);
if (mToast.getInAnimation() != null) {
mToast.getInAnimation().start();
}
mCallback = callback;
mPresenter = new ToastPresenter(context, mIAccessibilityManager,
mNotificationManager, packageName);
// Set as trusted overlay so touches can pass through toasts
mPresenter.getLayoutParams().setTrustedOverlay();
mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());
mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),
mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),
mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());
};
if (mToastOutAnimatorListener != null) {
// if we're currently animating out a toast, show new toast after prev toast is hidden
mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable);
} else if (mPresenter != null) {
// if there's a toast already showing that we haven't tried hiding yet, hide it and
// then show the next toast after its hidden animation is done
hideCurrentToast(showToastRunnable);
} else {
// else, show this next toast immediately
showToastRunnable.run();
}
}
@Override
@MainThread
public void hideToast(String packageName, IBinder token) {
if (mPresenter == null || !Objects.equals(mPresenter.getPackageName(), packageName)
|| !Objects.equals(mPresenter.getToken(), token)) {
Log.w(TAG, "Attempt to hide non-current toast from package " + packageName);
return;
}
mToastLogger.logOnHideToast(packageName, token.toString());
hideCurrentToast(null);
}
@MainThread
private void hideCurrentToast(Runnable runnable) {
if (mToast.getOutAnimation() != null) {
Animator animator = mToast.getOutAnimation();
mToastOutAnimatorListener = new ToastOutAnimatorListener(mPresenter, mCallback,
runnable);
animator.addListener(mToastOutAnimatorListener);
animator.start();
} else {
mPresenter.hide(mCallback);
if (runnable != null) {
runnable.run();
}
}
mToast = null;
mPresenter = null;
mCallback = null;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
if (newConfig.orientation != mOrientation) {
mOrientation = newConfig.orientation;
if (mToast != null) {
mToastLogger.logOrientationChange(mToast.mText.toString(),
mOrientation == ORIENTATION_PORTRAIT);
mToast.onOrientationChange(mOrientation);
mPresenter.updateLayoutParams(
mToast.getXOffset(),
mToast.getYOffset(),
mToast.getHorizontalMargin(),
mToast.getVerticalMargin(),
mToast.getGravity());
}
}
}
/**
* Once the out animation for a toast is finished, start showing the next toast.
*/
class ToastOutAnimatorListener extends AnimatorListenerAdapter {
final ToastPresenter mPrevPresenter;
final ITransientNotificationCallback mPrevCallback;
@Nullable Runnable mShowNextToastRunnable;
ToastOutAnimatorListener(
@NonNull ToastPresenter presenter,
@NonNull ITransientNotificationCallback callback,
@Nullable Runnable runnable) {
mPrevPresenter = presenter;
mPrevCallback = callback;
mShowNextToastRunnable = runnable;
}
void setShowNextToastRunnable(Runnable runnable) {
mShowNextToastRunnable = runnable;
}
@Override
public void onAnimationEnd(Animator animation) {
mPrevPresenter.hide(mPrevCallback);
if (mShowNextToastRunnable != null) {
mShowNextToastRunnable.run();
}
mToastOutAnimatorListener = null;
}
}
}