chromium/chrome/android/junit/src/org/chromium/chrome/browser/media/ui/MediaNotificationTestBase.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.media.ui;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Icon;
import android.support.v4.media.session.MediaSessionCompat;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.base.SplitCompatService;
import org.chromium.chrome.browser.media.ui.ChromeMediaNotificationControllerDelegate.ListenerServiceImpl;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.components.browser_ui.media.MediaNotificationController;
import org.chromium.components.browser_ui.media.MediaNotificationInfo;
import org.chromium.components.browser_ui.media.MediaNotificationListener;
import org.chromium.components.browser_ui.media.MediaNotificationManager;
import org.chromium.components.browser_ui.notifications.ForegroundServiceUtils;
import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.media_session.mojom.MediaSessionAction;
import org.chromium.services.media_session.MediaMetadata;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/** Common test fixtures for MediaNotificationController JUnit tests. */
public class MediaNotificationTestBase {
    private static final int NOTIFICATION_ID = 0;
    private static final int TAB_ID = 0;
    static final String NOTIFICATION_GROUP_NAME = "group-name";
    static final Set<Integer> DEFAULT_ACTIONS =
            Stream.of(MediaSessionAction.PLAY).collect(Collectors.toSet());
    Context mMockContext;
    MockListenerService mService;
    MediaNotificationListener mListener;
    ForegroundServiceUtils mMockForegroundServiceUtils;
    NotificationUmaTracker mMockUmaTracker;

    MediaNotificationInfo.Builder mMediaNotificationInfoBuilder;

    @Rule public JniMocker mocker = new JniMocker();

    protected MediaNotificationTestTabHolder createMediaNotificationTestTabHolder(
            int tabId, String url, String title) {
        return new MediaNotificationTestTabHolder(tabId, url, title, mocker);
    }

    static class MockMediaNotificationController extends MediaNotificationController {
        public MockMediaNotificationController(MediaNotificationController.Delegate delegate) {
            super(delegate);
        }
    }

    class MockListenerServiceImpl extends ListenerServiceImpl {
        MockListenerServiceImpl() {
            super(MediaNotificationTestBase.this.getNotificationId());
        }

        public void setServiceForTesting(SplitCompatService service) {
            setService(service);
        }
    }

    class MockListenerService extends SplitCompatService {
        private MockListenerServiceImpl mImpl;

        MockListenerService() {
            super(null);
            mImpl = spy(new MockListenerServiceImpl());
            attachBaseContextForTesting(null, mImpl);
        }

        MockListenerServiceImpl getImpl() {
            return mImpl;
        }
    }

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;

        mMockContext = spy(RuntimeEnvironment.application);
        ContextUtils.initApplicationContextForTests(mMockContext);

        mListener = mock(MediaNotificationListener.class);

        ChromeMediaNotificationControllerDelegate.sMapNotificationIdToOptions.put(
                getNotificationId(),
                new ChromeMediaNotificationControllerDelegate.NotificationOptions(
                        MockListenerService.class, NOTIFICATION_GROUP_NAME));

        mMockUmaTracker = mock(NotificationUmaTracker.class);
        MediaNotificationManager.setControllerForTesting(
                getNotificationId(),
                spy(
                        new MockMediaNotificationController(
                                new ChromeMediaNotificationControllerDelegate(getNotificationId()) {
                                    @Override
                                    public void logNotificationShown(
                                            NotificationWrapper notification) {
                                        mMockUmaTracker.onNotificationShown(
                                                NotificationUmaTracker.SystemNotificationType.MEDIA,
                                                notification.getNotification());
                                    }
                                })));

        mMediaNotificationInfoBuilder =
                new MediaNotificationInfo.Builder()
                        .setMetadata(new MediaMetadata("title", "artist", "album"))
                        .setOrigin("https://example.com")
                        .setListener(mListener)
                        .setMediaSessionActions(DEFAULT_ACTIONS)
                        .setId(getNotificationId())
                        .setInstanceId(TAB_ID);

        doNothing().when(getController()).onServiceStarted(any(MockListenerService.class));
        // Robolectric does not have "ShadowMediaSession".
        doAnswer(
                        new Answer() {
                            @Override
                            public Object answer(InvocationOnMock invocation) {
                                MediaSessionCompat mockSession = mock(MediaSessionCompat.class);
                                getController().mMediaSession = mockSession;
                                doReturn(null).when(mockSession).getSessionToken();
                                return "Created mock media session";
                            }
                        })
                .when(getController())
                .updateMediaSession();

        doAnswer(
                        new Answer() {
                            @Override
                            public Object answer(InvocationOnMock invocation) {
                                Intent intent = (Intent) invocation.getArgument(0);
                                startService(intent);
                                return new ComponentName(mMockContext, MockListenerService.class);
                            }
                        })
                .when(mMockContext)
                .startService(any(Intent.class));

        mMockForegroundServiceUtils = mock(ForegroundServiceUtils.class);
        ForegroundServiceUtils.setInstanceForTesting(mMockForegroundServiceUtils);
    }

    @After
    public void tearDown() {
        MediaNotificationManager.clear(NOTIFICATION_ID);
    }

    MediaNotificationController getController() {
        return MediaNotificationManager.getController(getNotificationId());
    }

    void ensureMediaNotificationInfo() {
        getController().mMediaNotificationInfo = mMediaNotificationInfoBuilder.build();
    }

    void setUpServiceAndClearInvocations() {
        setUpService();
        clearInvocations(getController());
        clearInvocations(mService);
        clearInvocations(mMockContext);
        clearInvocations(mMockUmaTracker);
    }

    void setUpService() {
        ensureMediaNotificationInfo();

        Intent intent = getController().mDelegate.createServiceIntent();
        mMockContext.startService(intent);
    }

    private void startService(Intent intent) {
        ensureService();
        mService.onStartCommand(intent, 0, 0);
    }

    private void ensureService() {
        if (mService != null) return;
        mService = spy(new MockListenerService());
        MockListenerServiceImpl impl = mService.getImpl();
        impl.setServiceForTesting(mService);

        doAnswer(
                        new Answer() {
                            @Override
                            public Object answer(InvocationOnMock invocation) {
                                mService.onDestroy();
                                mService = null;
                                return "service stopped";
                            }
                        })
                .when(impl)
                .stopListenerService();
    }

    Bitmap iconToBitmap(Icon icon) {
        if (icon == null) return null;

        BitmapDrawable drawable = (BitmapDrawable) icon.loadDrawable(mMockContext);
        assert drawable != null;
        return drawable.getBitmap();
    }

    int getNotificationId() {
        return NOTIFICATION_ID;
    }

    void advanceTimeByMillis(int timeMillis) {
        ShadowLooper.idleMainLooper(timeMillis, TimeUnit.MILLISECONDS);
    }
}