Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 56e77d8f authored by Jan Tomljanovic's avatar Jan Tomljanovic Committed by Android (Google) Code Review
Browse files

Merge "Block custom toasts when app is in the background."

parents f981579c 549d48e7
Loading
Loading
Loading
Loading
+70 −64
Original line number Diff line number Diff line
@@ -242,7 +242,6 @@ import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.compat.IPlatformCompat;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.InstanceIdSequence;
@@ -468,7 +467,6 @@ public class NotificationManagerService extends SystemService {
    private UriGrantsManagerInternal mUgmInternal;
    private RoleObserver mRoleObserver;
    private UserManager mUm;
    private IPlatformCompat mPlatformCompat;
    private ShortcutHelper mShortcutHelper;

    final IBinder mForegroundToken = new Binder();
@@ -1987,8 +1985,6 @@ public class NotificationManagerService extends SystemService {
        mDeviceIdleManager = getContext().getSystemService(DeviceIdleManager.class);
        mDpm = dpm;
        mUm = userManager;
        mPlatformCompat = IPlatformCompat.Stub.asInterface(
                ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));

        mUiHandler = new Handler(UiThread.get().getLooper());
        String[] extractorNames;
@@ -2886,16 +2882,16 @@ public class NotificationManagerService extends SystemService {
        return userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId;
    }

    private ToastRecord getToastRecord(int uid, int pid, String packageName, IBinder token,
            @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration,
            Binder windowToken, int displayId,
    private ToastRecord getToastRecord(int uid, int pid, String packageName, boolean isSystemToast,
            IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback,
            int duration, Binder windowToken, int displayId,
            @Nullable ITransientNotificationCallback textCallback) {
        if (callback == null) {
            return new TextToastRecord(this, mStatusBar, uid, pid, packageName, token, text,
                    duration, windowToken, displayId, textCallback);
            return new TextToastRecord(this, mStatusBar, uid, pid, packageName,
                    isSystemToast, token, text, duration, windowToken, displayId, textCallback);
        } else {
            return new CustomToastRecord(this, uid, pid, packageName, token, callback, duration,
                    windowToken, displayId);
            return new CustomToastRecord(this, uid, pid, packageName,
                    isSystemToast, token, callback, duration, windowToken, displayId);
        }
    }

@@ -2966,32 +2962,11 @@ public class NotificationManagerService extends SystemService {
            }

            boolean isAppRenderedToast = (callback != null);
            if (isAppRenderedToast && !isSystemToast && !isPackageInForegroundForToast(pkg,
                    callingUid)) {
                boolean block;
                final long id = Binder.clearCallingIdentity();
                try {
                    // CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK is gated on targetSdk, so block will be
                    // false for apps with targetSdk < R. For apps with targetSdk R+, text toasts
                    // are not app-rendered, so isAppRenderedToast == true means it's a custom
                    // toast.
                    block = mPlatformCompat.isChangeEnabledByPackageName(
                            CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK, pkg,
                            callingUser.getIdentifier());
                } catch (RemoteException e) {
                    // Shouldn't happen have since it's a local local
                    Slog.e(TAG, "Unexpected exception while checking block background custom toasts"
                            + " change", e);
                    block = false;
                } finally {
                    Binder.restoreCallingIdentity(id);
                }
                if (block) {
            if (blockToast(callingUid, isSystemToast, isAppRenderedToast)) {
                Slog.w(TAG, "Blocking custom toast from package " + pkg
                            + " due to package not in the foreground");
                        + " due to package not in the foreground at time the toast was posted");
                return;
            }
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
@@ -3023,8 +2998,8 @@ public class NotificationManagerService extends SystemService {

                        Binder windowToken = new Binder();
                        mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId);
                        record = getToastRecord(callingUid, callingPid, pkg, token, text, callback,
                                duration, windowToken, displayId, textCallback);
                        record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,
                                text, callback, duration, windowToken, displayId, textCallback);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveForToastIfNeededLocked(callingPid);
@@ -3042,28 +3017,6 @@ public class NotificationManagerService extends SystemService {
            }
        }

        /**
         * Implementation note: Our definition of foreground for toasts is an implementation matter
         * and should strike a balance between functionality and anti-abuse effectiveness. We
         * currently worry about the following cases:
         * <ol>
         *     <li>App with fullscreen activity: Allow toasts
         *     <li>App behind translucent activity from other app: Block toasts
         *     <li>App in multi-window: Allow toasts
         *     <li>App with expanded bubble: Allow toasts
         *     <li>App posting toasts on onCreate(), onStart(), onResume(): Allow toasts
         *     <li>App posting toasts on onPause(), onStop(), onDestroy(): Block toasts
         * </ol>
         * Checking if the UID has any resumed activities satisfy use-cases above.
         *
         * <p>Checking if {@code mActivityManager.getUidImportance(callingUid) ==
         * IMPORTANCE_FOREGROUND} does not work because it considers the app in foreground if it has
         * any visible activities, failing case 2 in list above.
         */
        private boolean isPackageInForegroundForToast(String pkg, int callingUid) {
            return mAtm.hasResumedActivity(callingUid);
        }

        @Override
        public void cancelToast(String pkg, IBinder token) {
            Slog.i(TAG, "cancelToast pkg=" + pkg + " token=" + token);
@@ -7388,17 +7341,16 @@ public class NotificationManagerService extends SystemService {
                    CompatChanges.isChangeEnabled(RATE_LIMIT_TOASTS, record.uid);
            boolean isWithinQuota =
                    mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG);
            if ((!rateLimitingEnabled || isWithinQuota) && record.show()) {

            if (tryShowToast(record, rateLimitingEnabled, isWithinQuota)) {
                scheduleDurationReachedLocked(record);
                mIsCurrentToastShown = true;
                if (rateLimitingEnabled) {
                    mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
                }
                return;
            } else if (rateLimitingEnabled && !isWithinQuota) {
                Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
                        + "following toast was blocked and discarded: " + record);
            }

            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
@@ -7407,6 +7359,22 @@ public class NotificationManagerService extends SystemService {
        }
    }

    /** Returns true if it successfully showed the toast. */
    private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
            boolean isWithinQuota) {
        if (rateLimitingEnabled && !isWithinQuota) {
            Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
                    + "following toast was blocked and discarded: " + record);
            return false;
        }
        if (blockToast(record.uid, record.isSystemToast, record.isAppRendered())) {
            Slog.w(TAG, "Blocking custom toast from package " + record.pkg
                    + " due to package not in the foreground at the time of showing the toast");
            return false;
        }
        return record.show();
    }

    @GuardedBy("mToastQueue")
    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
@@ -7526,6 +7494,44 @@ public class NotificationManagerService extends SystemService {
        }
    }

    /**
     * Implementation note: Our definition of foreground for toasts is an implementation matter
     * and should strike a balance between functionality and anti-abuse effectiveness. We
     * currently worry about the following cases:
     * <ol>
     *     <li>App with fullscreen activity: Allow toasts
     *     <li>App behind translucent activity from other app: Block toasts
     *     <li>App in multi-window: Allow toasts
     *     <li>App with expanded bubble: Allow toasts
     *     <li>App posting toasts on onCreate(), onStart(), onResume(): Allow toasts
     *     <li>App posting toasts on onPause(), onStop(), onDestroy(): Block toasts
     * </ol>
     * Checking if the UID has any resumed activities satisfy use-cases above.
     *
     * <p>Checking if {@code mActivityManager.getUidImportance(callingUid) ==
     * IMPORTANCE_FOREGROUND} does not work because it considers the app in foreground if it has
     * any visible activities, failing case 2 in list above.
     */
    private boolean isPackageInForegroundForToast(int callingUid) {
        return mAtm.hasResumedActivity(callingUid);
    }

    /**
     * True if the toast should be blocked. It will return true if all of the following conditions
     * apply: it's a custom toast, it's not a system toast, the package that sent the toast is in
     * the background and CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK is enabled.
     *
     * CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK is gated on targetSdk, so it will return false for apps
     * with targetSdk < R. For apps with targetSdk R+, text toasts are not app-rendered, so
     * isAppRenderedToast == true means it's a custom toast.
     */
    private boolean blockToast(int uid, boolean isSystemToast, boolean isAppRenderedToast) {
        return isAppRenderedToast
                && !isSystemToast
                && !isPackageInForegroundForToast(uid)
                && CompatChanges.isChangeEnabled(CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK, uid);
    }

    private void handleRankingReconsideration(Message message) {
        if (!(message.obj instanceof RankingReconsideration)) return;
        RankingReconsideration recon = (RankingReconsideration) message.obj;
+10 −3
Original line number Diff line number Diff line
@@ -37,9 +37,10 @@ public class CustomToastRecord extends ToastRecord {
    public final ITransientNotification callback;

    public CustomToastRecord(NotificationManagerService notificationManager, int uid, int pid,
            String packageName, IBinder token, ITransientNotification callback, int duration,
            Binder windowToken, int displayId) {
        super(notificationManager, uid, pid, packageName, token, duration, windowToken, displayId);
            String packageName, boolean isSystemToast, IBinder token,
            ITransientNotification callback, int duration, Binder windowToken, int displayId) {
        super(notificationManager, uid, pid, packageName, isSystemToast, token, duration,
                windowToken, displayId);
        this.callback = checkNotNull(callback);
    }

@@ -77,11 +78,17 @@ public class CustomToastRecord extends ToastRecord {
        return true;
    }

    @Override
    public boolean isAppRendered() {
        return true;
    }

    @Override
    public String toString() {
        return "CustomToastRecord{"
                + Integer.toHexString(System.identityHashCode(this))
                + " " + pid + ":" +  pkg + "/" + UserHandle.formatUid(uid)
                + " isSystemToast=" + isSystemToast
                + " token=" + token
                + " callback=" + callback
                + " duration=" + getDuration()
+11 −3
Original line number Diff line number Diff line
@@ -43,9 +43,11 @@ public class TextToastRecord extends ToastRecord {

    public TextToastRecord(NotificationManagerService notificationManager,
            @Nullable StatusBarManagerInternal statusBarManager, int uid, int pid,
            String packageName, IBinder token, CharSequence text, int duration, Binder windowToken,
            int displayId, @Nullable ITransientNotificationCallback callback) {
        super(notificationManager, uid, pid, packageName, token, duration, windowToken, displayId);
            String packageName, boolean isSystemToast, IBinder token, CharSequence text,
            int duration, Binder windowToken, int displayId,
            @Nullable ITransientNotificationCallback callback) {
        super(notificationManager, uid, pid, packageName, isSystemToast, token, duration,
                windowToken, displayId);
        mStatusBar = statusBarManager;
        mCallback = callback;
        this.text = checkNotNull(text);
@@ -72,11 +74,17 @@ public class TextToastRecord extends ToastRecord {
        mStatusBar.hideToast(pkg, token);
    }

    @Override
    public boolean isAppRendered() {
        return false;
    }

    @Override
    public String toString() {
        return "TextToastRecord{"
                + Integer.toHexString(System.identityHashCode(this))
                + " " + pid + ":" +  pkg + "/" + UserHandle.formatUid(uid)
                + " isSystemToast=" + isSystemToast
                + " token=" + token
                + " text=" + text
                + " duration=" + getDuration()
+10 −1
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ public abstract class ToastRecord {
    public final int uid;
    public final int pid;
    public final String pkg;
    public final boolean isSystemToast;
    public final IBinder token;
    public final int displayId;
    public final Binder windowToken;
@@ -38,11 +39,13 @@ public abstract class ToastRecord {
    private int mDuration;

    protected ToastRecord(NotificationManagerService notificationManager, int uid, int pid,
            String pkg, IBinder token, int duration, Binder windowToken, int displayId) {
            String pkg, boolean isSystemToast, IBinder token, int duration, Binder windowToken,
            int displayId) {
        this.mNotificationManager = notificationManager;
        this.uid = uid;
        this.pid = pid;
        this.pkg = pkg;
        this.isSystemToast = isSystemToast;
        this.token = token;
        this.windowToken = windowToken;
        this.displayId = displayId;
@@ -95,4 +98,10 @@ public abstract class ToastRecord {
        // should override this method.
        return false;
    }

    /**
     * Returns true if the app is responsible for rendering the toast, false otherwise (for example,
     * if it's rendered by the system).
     */
    public abstract boolean isAppRendered();
}
+34 −0
Original line number Diff line number Diff line
@@ -4979,6 +4979,40 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        verify(callback, times(0)).show(any());
    }

    @Test
    public void testCustomToastPostedWhileInForeground_blockedIfAppGoesToBackground()
            throws Exception {
        final String testPackage = "testPackageName";
        assertEquals(0, mService.mToastQueue.size());
        mService.isSystemUid = false;
        setToastRateIsWithinQuota(true);

        // package is not suspended
        when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
                .thenReturn(false);

        setAppInForegroundForToasts(mUid, true);

        Binder token1 = new Binder();
        Binder token2 = new Binder();
        ITransientNotification callback1 = mock(ITransientNotification.class);
        ITransientNotification callback2 = mock(ITransientNotification.class);
        INotificationManager nmService = (INotificationManager) mService.mService;

        nmService.enqueueToast(testPackage, token1, callback1, 2000, 0);
        nmService.enqueueToast(testPackage, token2, callback2, 2000, 0);

        assertEquals(2, mService.mToastQueue.size()); // Both toasts enqueued.
        verify(callback1, times(1)).show(any()); // First toast shown.

        setAppInForegroundForToasts(mUid, false);

        mService.cancelToastLocked(0); // Remove the first toast, and show next.

        assertEquals(0, mService.mToastQueue.size()); // Both toasts processed.
        verify(callback2, never()).show(any()); // Second toast was never shown.
    }

    @Test
    public void testAllowForegroundTextToasts() throws Exception {
        final String testPackage = "testPackageName";