chromium/chrome/android/javatests/src/org/chromium/chrome/browser/tracing/settings/TracingSettingsTest.java

// Copyright 2018 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.tracing.settings;

import static android.app.Notification.FLAG_ONGOING_EVENT;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Pair;

import androidx.core.app.NotificationCompat;
import androidx.preference.CheckBoxPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.settings.SettingsActivity;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.chrome.browser.tracing.TracingController;
import org.chromium.chrome.browser.tracing.TracingNotificationManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.browser_ui.notifications.MockNotificationManagerProxy;
import org.chromium.components.browser_ui.settings.ButtonPreference;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.components.browser_ui.settings.TextMessagePreference;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/** Tests for the Tracing settings menu. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class TracingSettingsTest {
    @Rule
    public final ChromeTabbedActivityTestRule mActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public final SettingsActivityTestRule<TracingSettings> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(TracingSettings.class);

    private MockNotificationManagerProxy mMockNotificationManager;

    @Before
    public void setUp() {
        mMockNotificationManager = new MockNotificationManagerProxy();
        TracingNotificationManager.overrideNotificationManagerForTesting(mMockNotificationManager);
    }

    @After
    public void tearDown() {
        TracingNotificationManager.overrideNotificationManagerForTesting(null);
    }

    /**
     * Waits until a notification has been displayed and then returns a NotificationEntry object to
     * the caller. Requires that only a single notification is displayed.
     *
     * @return The NotificationEntry object tracked by the MockNotificationManagerProxy.
     */
    private MockNotificationManagerProxy.NotificationEntry waitForNotification() {
        waitForNotificationManagerMutation();
        List<MockNotificationManagerProxy.NotificationEntry> notifications =
                mMockNotificationManager.getNotifications();
        Assert.assertEquals(1, notifications.size());
        return notifications.get(0);
    }

    /**
     * Waits for a mutation to occur in the mocked notification manager. This indicates that Chrome
     * called into Android to notify or cancel a notification.
     */
    private void waitForNotificationManagerMutation() {
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mMockNotificationManager.getMutationCountAndDecrement(),
                            Matchers.greaterThan(0));
                },
                /* maxTimeoutMs= */ 15000L,
                /* checkIntervalMs= */ 50);
    }

    private void waitForTracingControllerInitialization(PreferenceFragmentCompat fragment)
            throws Exception {
        final Preference defaultCategoriesPref =
                fragment.findPreference(TracingSettings.UI_PREF_DEFAULT_CATEGORIES);
        final Preference nonDefaultCategoriesPref =
                fragment.findPreference(TracingSettings.UI_PREF_NON_DEFAULT_CATEGORIES);
        final Preference modePref = fragment.findPreference(TracingSettings.UI_PREF_MODE);
        final Preference startTracingButton =
                fragment.findPreference(TracingSettings.UI_PREF_START_RECORDING);

        CallbackHelper callbackHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (TracingController.getInstance().getState()
                            == TracingController.State.INITIALIZING) {
                        // Controls should be disabled while initializing.
                        Assert.assertFalse(defaultCategoriesPref.isEnabled());
                        Assert.assertFalse(nonDefaultCategoriesPref.isEnabled());
                        Assert.assertFalse(modePref.isEnabled());
                        Assert.assertFalse(startTracingButton.isEnabled());

                        TracingController.getInstance()
                                .addObserver(
                                        new TracingController.Observer() {
                                            @Override
                                            public void onTracingStateChanged(
                                                    @TracingController.State int state) {
                                                callbackHelper.notifyCalled();
                                                TracingController.getInstance()
                                                        .removeObserver(this);
                                            }
                                        });
                        return;
                    }
                    // Already initialized.
                    callbackHelper.notifyCalled();
                });
        callbackHelper.waitForCallback(/* currentCallCount= */ 0);
    }

    @Test
    @MediumTest
    @Feature({"Preferences"})
    public void testRecordTrace() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        mSettingsActivityTestRule.startSettingsActivity();
        final PreferenceFragmentCompat fragment = mSettingsActivityTestRule.getFragment();
        final ButtonPreference startTracingButton =
                (ButtonPreference) fragment.findPreference(TracingSettings.UI_PREF_START_RECORDING);
        final ButtonPreference shareTraceButton =
                (ButtonPreference) fragment.findPreference(TracingSettings.UI_PREF_SHARE_TRACE);

        waitForTracingControllerInitialization(fragment);

        // Setting to IDLE state tries to dismiss the (non-existent) notification.
        waitForNotificationManagerMutation();
        Assert.assertEquals(0, mMockNotificationManager.getNotifications().size());

        CallbackHelper callbackHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            TracingController.State.IDLE,
                            TracingController.getInstance().getState());
                    Assert.assertTrue(startTracingButton.isEnabled());
                    Assert.assertEquals(TracingSettings.MSG_START, startTracingButton.getTitle());
                    Assert.assertFalse(shareTraceButton.isEnabled());

                    // Tap the button to start recording a trace.
                    startTracingButton
                            .getOnPreferenceClickListener()
                            .onPreferenceClick(startTracingButton);

                    Assert.assertEquals(
                            TracingController.State.STARTING,
                            TracingController.getInstance().getState());
                    Assert.assertFalse(startTracingButton.isEnabled());
                    Assert.assertEquals(TracingSettings.MSG_ACTIVE, startTracingButton.getTitle());

                    // Observe state changes to RECORDING, STOPPING, STOPPED, and IDLE.
                    TracingController.getInstance()
                            .addObserver(
                                    new TracingController.Observer() {
                                        @TracingController.State
                                        int mExpectedState = TracingController.State.RECORDING;

                                        @Override
                                        public void onTracingStateChanged(
                                                @TracingController.State int state) {
                                            // onTracingStateChanged() should be called four times
                                            // in total, with the right order of state changes:
                                            Assert.assertEquals(mExpectedState, state);
                                            if (state == TracingController.State.RECORDING) {
                                                mExpectedState = TracingController.State.STOPPING;
                                            } else if (state == TracingController.State.STOPPING) {
                                                mExpectedState = TracingController.State.STOPPED;
                                            } else if (state == TracingController.State.STOPPED) {
                                                mExpectedState = TracingController.State.IDLE;
                                            } else {
                                                TracingController.getInstance()
                                                        .removeObserver(this);
                                            }

                                            callbackHelper.notifyCalled();
                                        }
                                    });
                });

        // Wait for state change to RECORDING.
        callbackHelper.waitForCallback(/* currentCallCount= */ 0);

        // Recording started, a notification with a stop button should be displayed.
        Notification notification = waitForNotification().notification;
        Assert.assertEquals(FLAG_ONGOING_EVENT, notification.flags & FLAG_ONGOING_EVENT);
        Assert.assertEquals(null, notification.deleteIntent);
        Assert.assertEquals(1, NotificationCompat.getActionCount(notification));
        PendingIntent stopIntent = NotificationCompat.getAction(notification, 0).actionIntent;

        // Initiate stopping the recording and wait for state changes to STOPPING and STOPPED.
        stopIntent.send();
        callbackHelper.waitForCallback(
                /* currentCallCount= */ 1,
                /* numberOfCallsToWaitFor= */ 2,
                /* timeout= */ 15000L,
                TimeUnit.MILLISECONDS);

        // Notification should be replaced twice, once with an "is stopping" notification and then
        // with a notification to share the trace. Because the former is temporary, we can't
        // verify it reliably, so we skip over it and simply expect two notification mutations.
        waitForNotification();
        notification = waitForNotification().notification;
        Assert.assertEquals(0, notification.flags & FLAG_ONGOING_EVENT);
        Assert.assertNotEquals(null, notification.deleteIntent);
        PendingIntent deleteIntent = notification.deleteIntent;

        // The temporary tracing output file should now exist and the "share trace" button
        // should be active.
        File tempFile = TracingController.getInstance().getTracingTempFile();
        Assert.assertNotEquals(null, tempFile);
        Assert.assertTrue(tempFile.exists());
        Assert.assertTrue(shareTraceButton.isEnabled());

        // Discard the trace and wait for state change back to IDLE.
        deleteIntent.send();
        callbackHelper.waitForCallback(/* currentCallCount= */ 3);

        // The temporary file should be deleted asynchronously.
        CriteriaHelper.pollInstrumentationThread(() -> !tempFile.exists());

        // Notification should be deleted, too.
        waitForNotificationManagerMutation();
        Assert.assertEquals(0, mMockNotificationManager.getNotifications().size());

        // The share trace button should be disabled again.
        Assert.assertFalse(shareTraceButton.isEnabled());
    }

    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testNotificationsDisabledMessage() throws Exception {
        mMockNotificationManager.setNotificationsEnabled(false);

        mSettingsActivityTestRule.startSettingsActivity();
        final PreferenceFragmentCompat fragment = mSettingsActivityTestRule.getFragment();
        final ButtonPreference startTracingButton =
                (ButtonPreference) fragment.findPreference(TracingSettings.UI_PREF_START_RECORDING);
        final TextMessagePreference statusPreference =
                (TextMessagePreference)
                        fragment.findPreference(TracingSettings.UI_PREF_TRACING_STATUS);

        waitForTracingControllerInitialization(fragment);

        Assert.assertFalse(startTracingButton.isEnabled());
        Assert.assertEquals(
                TracingSettings.MSG_NOTIFICATIONS_DISABLED, statusPreference.getTitle());

        mMockNotificationManager.setNotificationsEnabled(true);
    }

    @Test
    @MediumTest
    @Feature({"Preferences"})
    public void testSelectCategories() throws Exception {
        // We need a renderer so that its tracing categories will be populated.
        mActivityTestRule.startMainActivityOnBlankPage();
        mSettingsActivityTestRule.startSettingsActivity();
        final PreferenceFragmentCompat fragment = mSettingsActivityTestRule.getFragment();
        final Preference defaultCategoriesPref =
                fragment.findPreference(TracingSettings.UI_PREF_DEFAULT_CATEGORIES);
        final Preference nonDefaultCategoriesPref =
                fragment.findPreference(TracingSettings.UI_PREF_NON_DEFAULT_CATEGORIES);

        waitForTracingControllerInitialization(fragment);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(defaultCategoriesPref.isEnabled());
                    Assert.assertTrue(nonDefaultCategoriesPref.isEnabled());
                });

        // Lists preferences for categories of a specific type and an example category name each.
        List<Pair<Preference, String>> categoriesPrefs =
                Arrays.asList(
                        new Pair<>(defaultCategoriesPref, "toplevel"),
                        new Pair<>(nonDefaultCategoriesPref, "disabled-by-default-cc.debug"));
        for (Pair<Preference, String> categoriesPrefAndSampleCategory : categoriesPrefs) {
            Preference categoriesPref = categoriesPrefAndSampleCategory.first;
            String sampleCategoryName = categoriesPrefAndSampleCategory.second;

            // Simulate clicking the preference, which should open a new preferences fragment in
            // a new activity.
            Context context = ApplicationProvider.getApplicationContext();
            Assert.assertNotNull(categoriesPref.getExtras());
            Assert.assertFalse(categoriesPref.getExtras().isEmpty());
            SettingsLauncher settingsLauncher = SettingsLauncherFactory.createSettingsLauncher();
            Intent intent =
                    settingsLauncher.createSettingsActivityIntent(
                            context, TracingCategoriesSettings.class, categoriesPref.getExtras());
            SettingsActivity categoriesActivity =
                    (SettingsActivity)
                            InstrumentationRegistry.getInstrumentation().startActivitySync(intent);

            PreferenceFragmentCompat categoriesFragment =
                    (PreferenceFragmentCompat) categoriesActivity.getMainFragment();
            Assert.assertEquals(TracingCategoriesSettings.class, categoriesFragment.getClass());

            CheckBoxPreference sampleCategoryPref =
                    (CheckBoxPreference) categoriesFragment.findPreference(sampleCategoryName);
            Assert.assertNotNull(sampleCategoryPref);

            boolean originallyEnabled =
                    TracingSettings.getEnabledCategories().contains(sampleCategoryName);
            Assert.assertEquals(originallyEnabled, sampleCategoryPref.isChecked());

            // Simulate selecting / deselecting the category.
            ThreadUtils.runOnUiThreadBlocking(sampleCategoryPref::performClick);
            Assert.assertNotEquals(originallyEnabled, sampleCategoryPref.isChecked());
            boolean finallyEnabled =
                    TracingSettings.getEnabledCategories().contains(sampleCategoryName);
            Assert.assertNotEquals(originallyEnabled, finallyEnabled);
        }
    }

    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSelectMode() throws Exception {
        mSettingsActivityTestRule.startSettingsActivity();
        final PreferenceFragmentCompat fragment = mSettingsActivityTestRule.getFragment();
        final ListPreference modePref =
                (ListPreference) fragment.findPreference(TracingSettings.UI_PREF_MODE);

        waitForTracingControllerInitialization(fragment);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(modePref.isEnabled());

                    // By default, the "record-until-full" mode is selected.
                    Assert.assertEquals(
                            "record-until-full", TracingSettings.getSelectedTracingMode());

                    // Dialog should contain 3 entries.
                    Assert.assertEquals(3, modePref.getEntries().length);

                    // Simulate changing the mode.
                    modePref.getOnPreferenceChangeListener()
                            .onPreferenceChange(modePref, "record-continuously");
                    Assert.assertEquals(
                            "record-continuously", TracingSettings.getSelectedTracingMode());
                });
    }
}