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

Commit 5c450ede authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Inline notif pipeline flag in NotifRemoteInputMgr" into tm-qpr-dev am: 7b82521b

parents 169ffabd 7b82521b
Loading
Loading
Loading
Loading
+6 −44
Original line number Original line Diff line number Diff line
@@ -286,10 +286,6 @@ public class NotificationRemoteInputManager implements Dumpable {
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        mRebuilder = rebuilder;
        mRebuilder = rebuilder;
        if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
            mRemoteInputListener = createLegacyRemoteInputLifetimeExtender(mainHandler,
                    notificationEntryManager, smartReplyController);
        }
        mKeyguardManager = context.getSystemService(KeyguardManager.class);
        mKeyguardManager = context.getSystemService(KeyguardManager.class);
        mStatusBarStateController = statusBarStateController;
        mStatusBarStateController = statusBarStateController;
        mRemoteInputUriController = remoteInputUriController;
        mRemoteInputUriController = remoteInputUriController;
@@ -313,17 +309,12 @@ public class NotificationRemoteInputManager implements Dumpable {
                    int reason) {
                    int reason) {
                // We're removing the notification, the smart controller can forget about it.
                // We're removing the notification, the smart controller can forget about it.
                mSmartReplyController.stopSending(entry);
                mSmartReplyController.stopSending(entry);

                if (removedByUser && entry != null) {
                    onPerformRemoveNotification(entry, entry.getKey());
                }
            }
            }
        });
        });
    }
    }


    /** Add a listener for various remote input events.  Works with NEW pipeline only. */
    /** Add a listener for various remote input events.  Works with NEW pipeline only. */
    public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) {
    public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) {
        if (mNotifPipelineFlags.isNewPipelineEnabled()) {
        if (mRemoteInputListener != null) {
        if (mRemoteInputListener != null) {
            throw new IllegalStateException("mRemoteInputListener is already set");
            throw new IllegalStateException("mRemoteInputListener is already set");
        }
        }
@@ -332,16 +323,6 @@ public class NotificationRemoteInputManager implements Dumpable {
            mRemoteInputListener.setRemoteInputController(mRemoteInputController);
            mRemoteInputListener.setRemoteInputController(mRemoteInputController);
        }
        }
    }
    }
    }

    @NonNull
    @VisibleForTesting
    protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
            Handler mainHandler,
            NotificationEntryManager notificationEntryManager,
            SmartReplyController smartReplyController) {
        return new LegacyRemoteInputLifetimeExtender();
    }


    /** Initializes this component with the provided dependencies. */
    /** Initializes this component with the provided dependencies. */
    public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
    public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
@@ -381,12 +362,6 @@ public class NotificationRemoteInputManager implements Dumpable {
                }
                }
            }
            }
        });
        });
        if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
            mSmartReplyController.setCallback((entry, reply) -> {
                StatusBarNotification newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply);
                mEntryManager.updateNotification(newSbn, null /* ranking */);
            });
        }
    }
    }


    public void addControllerCallback(RemoteInputController.Callback callback) {
    public void addControllerCallback(RemoteInputController.Callback callback) {
@@ -588,19 +563,6 @@ public class NotificationRemoteInputManager implements Dumpable {
        return v.findViewWithTag(RemoteInputView.VIEW_TAG);
        return v.findViewWithTag(RemoteInputView.VIEW_TAG);
    }
    }


    public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
        // OLD pipeline code ONLY; can assume implementation
        return ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener).mLifetimeExtenders;
    }

    @VisibleForTesting
    void onPerformRemoveNotification(NotificationEntry entry, final String key) {
        // OLD pipeline code ONLY; can assume implementation
        ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener)
                .mKeysKeptForRemoteInputHistory.remove(key);
        cleanUpRemoteInputForUserRemoval(entry);
    }

    /**
    /**
     * Disable remote input on the entry and remove the remote input view.
     * Disable remote input on the entry and remove the remote input view.
     * This should be called when a user dismisses a notification that won't be lifetime extended.
     * This should be called when a user dismisses a notification that won't be lifetime extended.
+0 −2
Original line number Original line Diff line number Diff line
@@ -212,8 +212,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter,
                mEntryManager.setUpWithPresenter(this);
                mEntryManager.setUpWithPresenter(this);
                mEntryManager.addNotificationLifetimeExtender(mHeadsUpManager);
                mEntryManager.addNotificationLifetimeExtender(mHeadsUpManager);
                mEntryManager.addNotificationLifetimeExtender(mGutsManager);
                mEntryManager.addNotificationLifetimeExtender(mGutsManager);
                mEntryManager.addNotificationLifetimeExtenders(
                        remoteInputManager.getLifetimeExtenders());
            }
            }
            notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor);
            notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor);
            mLockscreenUserManager.setUpWithPresenter(this);
            mLockscreenUserManager.setUpWithPresenter(this);
+1 −73
Original line number Original line Diff line number Diff line
@@ -16,12 +16,9 @@


package com.android.systemui.statusbar;
package com.android.systemui.statusbar;


import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.assertTrue;


import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.when;


import android.app.Notification;
import android.app.Notification;
@@ -30,19 +27,14 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Looper;
import android.os.SystemClock;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.testing.AndroidTestingRunner;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper;


import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.filters.SmallTest;


import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputActiveExtender;
import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputHistoryExtender;
import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.SmartReplyHistoryExtender;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -52,8 +44,6 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.statusbar.policy.RemoteInputUriController;


import com.google.android.collect.Sets;

import org.junit.Before;
import org.junit.Before;
import org.junit.Test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.RunWith;
@@ -76,23 +66,15 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
    @Mock private NotificationRemoteInputManager.Callback mCallback;
    @Mock private NotificationRemoteInputManager.Callback mCallback;
    @Mock private RemoteInputController mController;
    @Mock private RemoteInputController mController;
    @Mock private SmartReplyController mSmartReplyController;
    @Mock private SmartReplyController mSmartReplyController;
    @Mock private NotificationListenerService.RankingMap mRanking;
    @Mock private ExpandableNotificationRow mRow;
    @Mock private ExpandableNotificationRow mRow;
    @Mock private StatusBarStateController mStateController;
    @Mock private StatusBarStateController mStateController;
    @Mock private RemoteInputUriController mRemoteInputUriController;
    @Mock private RemoteInputUriController mRemoteInputUriController;
    @Mock private NotificationClickNotifier mClickNotifier;
    @Mock private NotificationClickNotifier mClickNotifier;

    // Dependency mocks:
    @Mock private NotificationEntryManager mEntryManager;
    @Mock private NotificationEntryManager mEntryManager;
    @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
    @Mock private NotificationLockscreenUserManager mLockscreenUserManager;


    private TestableNotificationRemoteInputManager mRemoteInputManager;
    private TestableNotificationRemoteInputManager mRemoteInputManager;
    private NotificationEntry mEntry;
    private NotificationEntry mEntry;
    private RemoteInputHistoryExtender mRemoteInputHistoryExtender;
    private SmartReplyHistoryExtender mSmartReplyHistoryExtender;
    private RemoteInputActiveExtender mRemoteInputActiveExtender;
    private TestableNotificationRemoteInputManager.FakeLegacyRemoteInputLifetimeExtender
            mLegacyRemoteInputLifetimeExtender;


    @Before
    @Before
    public void setUp() {
    public void setUp() {
@@ -121,21 +103,7 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
                .build();
                .build();
        mEntry.setRow(mRow);
        mEntry.setRow(mRow);


        mRemoteInputManager.setUpWithPresenterForTest(mCallback,
        mRemoteInputManager.setUpWithPresenterForTest(mCallback, mDelegate, mController);
                mDelegate, mController);
        for (NotificationLifetimeExtender extender : mRemoteInputManager.getLifetimeExtenders()) {
            extender.setCallback(
                    mock(NotificationLifetimeExtender.NotificationSafeToRemoveCallback.class));
        }
    }

    @Test
    public void testPerformOnRemoveNotification() {
        when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
        mRemoteInputManager.onPerformRemoveNotification(mEntry, mEntry.getKey());

        assertFalse(mEntry.mRemoteEditImeVisible);
        verify(mController).removeRemoteInput(mEntry, null);
    }
    }


    @Test
    @Test
@@ -143,7 +111,6 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
        when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
        when(mController.isRemoteInputActive(mEntry)).thenReturn(true);


        assertTrue(mRemoteInputManager.isRemoteInputActive(mEntry));
        assertTrue(mRemoteInputManager.isRemoteInputActive(mEntry));
        assertTrue(mRemoteInputActiveExtender.shouldExtendLifetime(mEntry));
    }
    }


    @Test
    @Test
@@ -152,7 +119,6 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
        when(mController.isSpinning(mEntry.getKey())).thenReturn(true);
        when(mController.isSpinning(mEntry.getKey())).thenReturn(true);


        assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
        assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
        assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
    }
    }


    @Test
    @Test
@@ -161,7 +127,6 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
        mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();
        mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();


        assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
        assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
        assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
    }
    }


    @Test
    @Test
@@ -170,20 +135,6 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
        when(mSmartReplyController.isSendingSmartReply(mEntry.getKey())).thenReturn(true);
        when(mSmartReplyController.isSendingSmartReply(mEntry.getKey())).thenReturn(true);


        assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry));
        assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry));
        assertTrue(mSmartReplyHistoryExtender.shouldExtendLifetime(mEntry));
    }

    @Test
    public void testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
        mRemoteInputActiveExtender.setShouldManageLifetime(mEntry, true /* shouldManage */);

        assertEquals(mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive(),
                Sets.newArraySet(mEntry));

        mRemoteInputManager.onPanelCollapsed();

        assertTrue(
                mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive().isEmpty());
    }
    }


    private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
    private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
@@ -227,28 +178,5 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
            mRemoteInputController = controller;
            mRemoteInputController = controller;
        }
        }


        @NonNull
        @Override
        protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
                Handler mainHandler,
                NotificationEntryManager notificationEntryManager,
                SmartReplyController smartReplyController) {
            mLegacyRemoteInputLifetimeExtender = new FakeLegacyRemoteInputLifetimeExtender();
            return mLegacyRemoteInputLifetimeExtender;
        }

        class FakeLegacyRemoteInputLifetimeExtender extends LegacyRemoteInputLifetimeExtender {

            @Override
            protected void addLifetimeExtenders() {
                mRemoteInputActiveExtender = new RemoteInputActiveExtender();
                mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
                mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
                mLifetimeExtenders.add(mRemoteInputHistoryExtender);
                mLifetimeExtenders.add(mSmartReplyHistoryExtender);
                mLifetimeExtenders.add(mRemoteInputActiveExtender);
            }
        }

    }
    }
}
}
+30 −42
Original line number Original line Diff line number Diff line
@@ -17,15 +17,15 @@ package com.android.systemui.statusbar;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.assertTrue;


import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;


import android.app.ActivityManager;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.Notification;
import android.os.Handler;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.service.notification.StatusBarNotification;
@@ -39,14 +39,12 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.coordinator.RemoteInputCoordinator;
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.RemoteInputUriController;


import org.junit.Before;
import org.junit.Before;
import org.junit.Test;
import org.junit.Test;
@@ -54,8 +52,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoAnnotations;


import java.util.Optional;

@RunWith(AndroidTestingRunner.class)
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@TestableLooper.RunWithLooper
@SmallTest
@SmallTest
@@ -67,57 +63,50 @@ public class SmartReplyControllerTest extends SysuiTestCase {
    private static final int TEST_CHOICE_COUNT = 4;
    private static final int TEST_CHOICE_COUNT = 4;
    private static final int TEST_ACTION_COUNT = 3;
    private static final int TEST_ACTION_COUNT = 3;


    private Notification mNotification;
    private NotificationEntry mEntry;
    private NotificationEntry mEntry;
    private SmartReplyController mSmartReplyController;
    private SmartReplyController mSmartReplyController;
    private NotificationRemoteInputManager mRemoteInputManager;


    @Mock private NotificationVisibilityProvider mVisibilityProvider;
    @Mock private NotificationVisibilityProvider mVisibilityProvider;
    @Mock private RemoteInputController.Delegate mDelegate;
    @Mock private NotificationRemoteInputManager.Callback mCallback;
    @Mock private StatusBarNotification mSbn;
    @Mock private StatusBarNotification mSbn;
    @Mock private NotificationEntryManager mNotificationEntryManager;
    @Mock private IStatusBarService mIStatusBarService;
    @Mock private IStatusBarService mIStatusBarService;
    @Mock private StatusBarStateController mStatusBarStateController;
    @Mock private RemoteInputUriController mRemoteInputUriController;
    @Mock private NotificationClickNotifier mClickNotifier;
    @Mock private NotificationClickNotifier mClickNotifier;
    @Mock private InternalNotifUpdater mInternalNotifUpdater;


    @Before
    @Before
    public void setUp() {
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        MockitoAnnotations.initMocks(this);
        mDependency.injectTestDependency(NotificationEntryManager.class,
                mNotificationEntryManager);


        mSmartReplyController = new SmartReplyController(
        mSmartReplyController = new SmartReplyController(
                mock(DumpManager.class),
                mock(DumpManager.class),
                mVisibilityProvider,
                mVisibilityProvider,
                mIStatusBarService,
                mIStatusBarService,
                mClickNotifier);
                mClickNotifier);
        mDependency.injectTestDependency(SmartReplyController.class,
        RemoteInputCoordinator remoteInputCoordinator = new RemoteInputCoordinator(
                mock(DumpManager.class),
                new RemoteInputNotificationRebuilder(mContext),
                mock(NotificationRemoteInputManager.class),
                mock(Handler.class),
                mSmartReplyController);
                mSmartReplyController);
        remoteInputCoordinator.setRemoteInputController(mock(RemoteInputController.class));
        NotifPipeline notifPipeline = mock(NotifPipeline.class);
        when(notifPipeline.getInternalNotifUpdater(anyString())).thenReturn(mInternalNotifUpdater);
        remoteInputCoordinator.attach(notifPipeline);


        mRemoteInputManager = new NotificationRemoteInputManager(mContext,
        Notification notification = new Notification.Builder(mContext, "")
                mock(NotifPipelineFlags.class),
                mock(NotificationLockscreenUserManager.class),
                mSmartReplyController,
                mVisibilityProvider,
                mNotificationEntryManager,
                new RemoteInputNotificationRebuilder(mContext),
                () -> Optional.of(mock(CentralSurfaces.class)),
                mStatusBarStateController,
                Handler.createAsync(Looper.myLooper()),
                mRemoteInputUriController,
                mClickNotifier,
                mock(ActionClickLogger.class),
                mock(DumpManager.class));
        mRemoteInputManager.setUpWithCallback(mCallback, mDelegate);
        mNotification = new Notification.Builder(mContext, "")
                .setSmallIcon(R.drawable.ic_person)
                .setSmallIcon(R.drawable.ic_person)
                .setContentTitle("Title")
                .setContentTitle("Title")
                .setContentText("Text").build();
                .setContentText("Text").build();

        mSbn = new StatusBarNotification(
        mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
                TEST_PACKAGE_NAME,
                0, mNotification, new UserHandle(ActivityManager.getCurrentUser()), null, 0);
                TEST_PACKAGE_NAME,
                0,
                null,
                TEST_UID,
                0,
                notification,
                new UserHandle(ActivityManager.getCurrentUser()),
                null,
                0);
        mEntry = new NotificationEntryBuilder()
        mEntry = new NotificationEntryBuilder()
                .setSbn(mSbn)
                .setSbn(mSbn)
                .build();
                .build();
@@ -128,10 +117,9 @@ public class SmartReplyControllerTest extends SysuiTestCase {
        mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT,
        mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT,
                MetricsEvent.LOCATION_UNKNOWN, false /* modifiedBeforeSending */);
                MetricsEvent.LOCATION_UNKNOWN, false /* modifiedBeforeSending */);


        // Sending smart reply should make calls to NotificationEntryManager
        // Sending smart reply should update the notification with reply and spinner.
        // to update the notification with reply and spinner.
        verify(mInternalNotifUpdater).onInternalNotificationUpdate(
        verify(mNotificationEntryManager).updateNotification(
                argThat(sbn -> sbn.getKey().equals(mSbn.getKey())), anyString());
                argThat(sbn -> sbn.getKey().equals(mSbn.getKey())), isNull());
    }
    }


    @Test
    @Test