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

Commit f744d23a authored by Jeff DeCew's avatar Jeff DeCew
Browse files

New Pipeline: Remote Input 3/4: Extract notification rebuilder methods to a utility

Fixes: 204127880
Bug: 203938360
Test: atest RemoteInputNotificationRebuilderTest
Merged-In: I80ace34e5e97e5ccc6135276b83102f5696dd23b
Change-Id: I80ace34e5e97e5ccc6135276b83102f5696dd23b
parent fac5da2f
Loading
Loading
Loading
Loading
+5 −82
Original line number Diff line number Diff line
@@ -21,13 +21,10 @@ import android.app.KeyguardManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -73,12 +70,10 @@ import com.android.systemui.statusbar.policy.RemoteInputView;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import dagger.Lazy;

@@ -111,6 +106,7 @@ public class NotificationRemoteInputManager implements Dumpable {
    protected final FeatureFlags mFeatureFlags;
    private final UserManager mUserManager;
    private final KeyguardManager mKeyguardManager;
    private final RemoteInputNotificationRebuilder mRebuilder;
    private final StatusBarStateController mStatusBarStateController;
    private final RemoteInputUriController mRemoteInputUriController;
    private final NotificationClickNotifier mClickNotifier;
@@ -288,6 +284,7 @@ public class NotificationRemoteInputManager implements Dumpable {
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        mRebuilder = new RemoteInputNotificationRebuilder(context);  // TODO: inject?
        if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
            mRemoteInputListener = createLegacyRemoteInputLifetimeExtender(mainHandler,
                    notificationEntryManager, smartReplyController);
@@ -371,7 +368,7 @@ public class NotificationRemoteInputManager implements Dumpable {
        if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
            // FIXME: Don't forget to implement this in the coordinator!
            mSmartReplyController.setCallback((entry, reply) -> {
                StatusBarNotification newSbn = rebuildNotificationForSendingSmartReply(entry, reply);
                StatusBarNotification newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply);
                mEntryManager.updateNotification(newSbn, null /* ranking */);
            });
        }
@@ -633,80 +630,6 @@ public class NotificationRemoteInputManager implements Dumpable {
        }
    }

    // FIXME: Move to a helper class and test separately
    public StatusBarNotification rebuildNotificationForSendingSmartReply(NotificationEntry entry,
            CharSequence reply) {
        return rebuildNotificationWithRemoteInputInserted(entry, reply,
                true /* showSpinner */,
                null /* mimeType */, null /* uri */);
    }

    // FIXME: Move to a helper class and test separately
    public StatusBarNotification rebuildNotificationForCanceledSmartReplies(
            NotificationEntry entry) {
        return rebuildNotificationWithRemoteInputInserted(entry, null /* remoteInputTest */,
                false /* showSpinner */, null /* mimeType */, null /* uri */);
    }

    // FIXME: Move to a helper class and test separately
    public StatusBarNotification rebuildNotificationForBasicExtension(NotificationEntry entry) {
        CharSequence remoteInputText = entry.remoteInputText;
        if (TextUtils.isEmpty(remoteInputText)) {
            remoteInputText = entry.remoteInputTextWhenReset;
        }
        String remoteInputMimeType = entry.remoteInputMimeType;
        Uri remoteInputUri = entry.remoteInputUri;
        StatusBarNotification newSbn = rebuildNotificationWithRemoteInputInserted(entry,
                remoteInputText, false /* showSpinner */, remoteInputMimeType,
                remoteInputUri);
        return newSbn;
    }

    // FIXME: Move to a helper class and test separately
    @VisibleForTesting
    StatusBarNotification rebuildNotificationWithRemoteInputInserted(NotificationEntry entry,
            CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
        StatusBarNotification sbn = entry.getSbn();

        Notification.Builder b = Notification.Builder
                .recoverBuilder(mContext, sbn.getNotification().clone());
        if (remoteInputText != null || uri != null) {
            RemoteInputHistoryItem newItem = uri != null
                    ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
                    : new RemoteInputHistoryItem(remoteInputText);
            Parcelable[] oldHistoryItems = sbn.getNotification().extras
                    .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
            RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
                    ? Stream.concat(
                                Stream.of(newItem),
                                Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
                            .toArray(RemoteInputHistoryItem[]::new)
                    : new RemoteInputHistoryItem[] { newItem };
            b.setRemoteInputHistory(newHistoryItems);
        }
        b.setShowRemoteInputSpinner(showSpinner);
        b.setHideSmartReplies(true);

        Notification newNotification = b.build();

        // Undo any compatibility view inflation
        newNotification.contentView = sbn.getNotification().contentView;
        newNotification.bigContentView = sbn.getNotification().bigContentView;
        newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;

        return new StatusBarNotification(
                sbn.getPackageName(),
                sbn.getOpPkg(),
                sbn.getId(),
                sbn.getTag(),
                sbn.getUid(),
                sbn.getInitialPid(),
                newNotification,
                sbn.getUser(),
                sbn.getOverrideGroupKey(),
                sbn.getPostTime());
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        if (mRemoteInputListener instanceof Dumpable) {
@@ -994,7 +917,7 @@ public class NotificationRemoteInputManager implements Dumpable {
            public void setShouldManageLifetime(NotificationEntry entry,
                    boolean shouldExtend) {
                if (shouldExtend) {
                    StatusBarNotification newSbn = rebuildNotificationForBasicExtension(entry);
                    StatusBarNotification newSbn = mRebuilder.rebuildForRemoteInputReply(entry);
                    entry.onRemoteInputInserted();

                    if (newSbn == null) {
@@ -1035,7 +958,7 @@ public class NotificationRemoteInputManager implements Dumpable {
            public void setShouldManageLifetime(NotificationEntry entry,
                    boolean shouldExtend) {
                if (shouldExtend) {
                    StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
                    StatusBarNotification newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry);

                    if (newSbn == null) {
                        return;
+141 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.statusbar;

import android.annotation.NonNull;
import android.app.Notification;
import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.net.Uri;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;

import java.util.Arrays;
import java.util.stream.Stream;

import javax.inject.Inject;

/**
 * A helper class which will augment the notifications using arguments and other information
 * accessible to the entry in order to provide intermediate remote input states.
 */
@SysUISingleton
public class RemoteInputNotificationRebuilder {

    private final Context mContext;

    @Inject
    RemoteInputNotificationRebuilder(Context context) {
        mContext = context;
    }

    /**
     * When a smart reply is sent off to the app, we insert the text into the remote input history,
     * and show a spinner to indicate that the app has yet to respond.
     */
    @NonNull
    public StatusBarNotification rebuildForSendingSmartReply(NotificationEntry entry,
            CharSequence reply) {
        return rebuildWithRemoteInputInserted(entry, reply,
                true /* showSpinner */,
                null /* mimeType */, null /* uri */);
    }

    /**
     * When the app cancels a notification in response to a smart reply, we remove the spinner
     * and leave the previously-added reply.  This is the lifetime-extended appearance of the
     * notification.
     */
    @NonNull
    public StatusBarNotification rebuildForCanceledSmartReplies(
            NotificationEntry entry) {
        return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
                false /* showSpinner */, null /* mimeType */, null /* uri */);
    }

    /**
     * When the app cancels a notification in response to a remote input reply, we update the
     * notification with the reply text and/or attachment. This is the lifetime-extended
     * appearance of the notification.
     */
    @NonNull
    public StatusBarNotification rebuildForRemoteInputReply(NotificationEntry entry) {
        CharSequence remoteInputText = entry.remoteInputText;
        if (TextUtils.isEmpty(remoteInputText)) {
            remoteInputText = entry.remoteInputTextWhenReset;
        }
        String remoteInputMimeType = entry.remoteInputMimeType;
        Uri remoteInputUri = entry.remoteInputUri;
        StatusBarNotification newSbn = rebuildWithRemoteInputInserted(entry,
                remoteInputText, false /* showSpinner */, remoteInputMimeType,
                remoteInputUri);
        return newSbn;
    }

    /** Inner method for generating the SBN */
    @VisibleForTesting
    @NonNull
    StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
            CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
        StatusBarNotification sbn = entry.getSbn();

        Notification.Builder b = Notification.Builder
                .recoverBuilder(mContext, sbn.getNotification().clone());
        if (remoteInputText != null || uri != null) {
            RemoteInputHistoryItem newItem = uri != null
                    ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
                    : new RemoteInputHistoryItem(remoteInputText);
            Parcelable[] oldHistoryItems = sbn.getNotification().extras
                    .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
            RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
                    ? Stream.concat(
                    Stream.of(newItem),
                    Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
                    .toArray(RemoteInputHistoryItem[]::new)
                    : new RemoteInputHistoryItem[] { newItem };
            b.setRemoteInputHistory(newHistoryItems);
        }
        b.setShowRemoteInputSpinner(showSpinner);
        b.setHideSmartReplies(true);

        Notification newNotification = b.build();

        // Undo any compatibility view inflation
        newNotification.contentView = sbn.getNotification().contentView;
        newNotification.bigContentView = sbn.getNotification().bigContentView;
        newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;

        return new StatusBarNotification(
                sbn.getPackageName(),
                sbn.getOpPkg(),
                sbn.getId(),
                sbn.getTag(),
                sbn.getUid(),
                sbn.getInitialPid(),
                newNotification,
                sbn.getUser(),
                sbn.getOverrideGroupKey(),
                sbn.getPostTime());
    }


}
+15 −106
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.statusbar;

@@ -10,15 +25,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Notification;
import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

@@ -165,109 +177,6 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
                mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive().isEmpty());
    }

    @Test
    public void testRebuildWithRemoteInput_noExistingInput_image() {
        Uri uri = mock(Uri.class);
        String mimeType = "image/jpeg";
        String text = "image inserted";
        StatusBarNotification newSbn =
                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                        mEntry, text, false, mimeType, uri);
        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
        assertEquals(1, messages.length);
        assertEquals(text, messages[0].getText());
        assertEquals(mimeType, messages[0].getMimeType());
        assertEquals(uri, messages[0].getUri());
    }

    @Test
    public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
        StatusBarNotification newSbn =
                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                        mEntry, "A Reply", false, null, null);
        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
        assertEquals(1, messages.length);
        assertEquals("A Reply", messages[0].getText());
        assertFalse(newSbn.getNotification().extras
                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
        assertTrue(newSbn.getNotification().extras
                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
    }

    @Test
    public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
        StatusBarNotification newSbn =
                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                        mEntry, "A Reply", true, null, null);
        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
        assertEquals(1, messages.length);
        assertEquals("A Reply", messages[0].getText());
        assertTrue(newSbn.getNotification().extras
                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
        assertTrue(newSbn.getNotification().extras
                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
    }

    @Test
    public void testRebuildWithRemoteInput_withExistingInput() {
        // Setup a notification entry with 1 remote input.
        StatusBarNotification newSbn =
                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                        mEntry, "A Reply", false, null, null);
        NotificationEntry entry = new NotificationEntryBuilder()
                .setSbn(newSbn)
                .build();

        // Try rebuilding to add another reply.
        newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                entry, "Reply 2", true, null, null);
        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
        assertEquals(2, messages.length);
        assertEquals("Reply 2", messages[0].getText());
        assertEquals("A Reply", messages[1].getText());
    }

    @Test
    public void testRebuildWithRemoteInput_withExistingInput_image() {
        // Setup a notification entry with 1 remote input.
        Uri uri = mock(Uri.class);
        String mimeType = "image/jpeg";
        String text = "image inserted";
        StatusBarNotification newSbn =
                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                        mEntry, text, false, mimeType, uri);
        NotificationEntry entry = new NotificationEntryBuilder()
                .setSbn(newSbn)
                .build();

        // Try rebuilding to add another reply.
        newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
                entry, "Reply 2", true, null, null);
        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
        assertEquals(2, messages.length);
        assertEquals("Reply 2", messages[0].getText());
        assertEquals(text, messages[1].getText());
        assertEquals(mimeType, messages[1].getMimeType());
        assertEquals(uri, messages[1].getUri());
    }

    @Test
    public void testRebuildNotificationForCanceledSmartReplies() {
        // Try rebuilding to remove spinner and hide buttons.
        StatusBarNotification newSbn =
                mRemoteInputManager.rebuildNotificationForCanceledSmartReplies(mEntry);
        assertFalse(newSbn.getNotification().extras
                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
        assertTrue(newSbn.getNotification().extras
                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
    }


    private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {

        TestableNotificationRemoteInputManager(
+174 −0

File added.

Preview size limit exceeded, changes collapsed.