// 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 android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tracing.TracingController;
import org.chromium.chrome.browser.tracing.TracingNotificationManager;
import org.chromium.components.browser_ui.settings.SettingsPage;
import org.chromium.components.browser_ui.settings.SettingsUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/** Settings fragment that shows options for recording a performance trace. */
public class TracingSettings extends PreferenceFragmentCompat
implements SettingsPage, TracingController.Observer {
static final String NON_DEFAULT_CATEGORY_PREFIX = "disabled-by-default-";
@VisibleForTesting static final String UI_PREF_DEFAULT_CATEGORIES = "default_categories";
@VisibleForTesting
static final String UI_PREF_NON_DEFAULT_CATEGORIES = "non_default_categories";
@VisibleForTesting static final String UI_PREF_MODE = "mode";
@VisibleForTesting static final String UI_PREF_START_RECORDING = "start_recording";
@VisibleForTesting static final String UI_PREF_SHARE_TRACE = "share_trace";
@VisibleForTesting static final String UI_PREF_TRACING_STATUS = "tracing_status";
// Non-translated strings:
private static final String MSG_TRACING_TITLE = "Tracing";
private static final String MSG_PRIVACY_NOTICE =
"Traces may contain user or site data related to the active browsing session, "
+ "including incognito tabs.";
private static final String MSG_ACTIVE_SUMMARY =
"A trace is being recorded. Use the notification to stop and share the result.";
@VisibleForTesting static final String MSG_START = "Record trace";
@VisibleForTesting static final String MSG_ACTIVE = "Recording…";
private static final String MSG_CATEGORIES_SUMMARY = "%s out of %s enabled";
private static final String MSG_MODE_RECORD_UNTIL_FULL = "Record until full";
private static final String MSG_MODE_RECORD_AS_MUCH_AS_POSSIBLE =
"Record until full (large buffer)";
private static final String MSG_MODE_RECORD_CONTINUOUSLY = "Record continuously";
private static final String MSG_SHARE_TRACE = "Share trace";
private static final ObservableSupplier<String> sPageTitle =
new ObservableSupplierImpl<>(MSG_TRACING_TITLE);
@VisibleForTesting
static final String MSG_NOTIFICATIONS_DISABLED =
"Please enable Chrome browser notifications to record a trace.";
// Ordered map that maps tracing mode string to resource id for its description.
private static final Map<String, String> TRACING_MODES = createTracingModesMap();
private Preference mPrefDefaultCategories;
private Preference mPrefNondefaultCategories;
private ListPreference mPrefMode;
private Preference mPrefStartRecording;
private Preference mPrefShareTrace;
private Preference mPrefTracingStatus;
/** Type of a tracing category indicating whether it is enabled by default or not. */
@IntDef({CategoryType.DEFAULT, CategoryType.NON_DEFAULT})
@Retention(RetentionPolicy.SOURCE)
public @interface CategoryType {
int DEFAULT = 0;
int NON_DEFAULT = 1;
}
private static Map<String, String> createTracingModesMap() {
Map<String, String> map = new LinkedHashMap<>();
map.put("record-until-full", MSG_MODE_RECORD_UNTIL_FULL);
map.put("record-as-much-as-possible", MSG_MODE_RECORD_AS_MUCH_AS_POSSIBLE);
map.put("record-continuously", MSG_MODE_RECORD_CONTINUOUSLY);
return map;
}
/**
* @return the current set of all enabled categories, irrespective of their type.
*/
public static Set<String> getEnabledCategories() {
Set<String> enabled =
ChromeSharedPreferences.getInstance()
.readStringSet(
ChromePreferenceKeys.SETTINGS_DEVELOPER_TRACING_CATEGORIES, null);
if (enabled == null) {
enabled = new HashSet<>();
// By default, enable all default categories.
for (String category : TracingController.getInstance().getKnownCategories()) {
if (getCategoryType(category) == CategoryType.DEFAULT) enabled.add(category);
}
}
return enabled;
}
/**
* Get the set of enabled categories of a given category type.
*
* @param type the category type.
* @return the current set of enabled categories with |type|.
*/
public static Set<String> getEnabledCategories(@CategoryType int type) {
Set<String> enabled = new HashSet<>();
for (String category : getEnabledCategories()) {
if (type == getCategoryType(category)) {
enabled.add(category);
}
}
return enabled;
}
/**
* Set the enabled categories of a given category type. The set of enabled categories with
* different types will not be changed.
*
* @param type the category type.
* @param enabledOfType the set of enabled categories with the given |type|.
*/
public static void setEnabledCategories(@CategoryType int type, Set<String> enabledOfType) {
Set<String> enabled = new HashSet<>(enabledOfType);
for (String category : getEnabledCategories()) {
if (type != getCategoryType(category)) {
enabled.add(category);
}
}
ChromeSharedPreferences.getInstance()
.writeStringSet(
ChromePreferenceKeys.SETTINGS_DEVELOPER_TRACING_CATEGORIES, enabled);
}
/**
* Get the type of a category derived from its name.
* @param category the name of the category.
* @return the type of the category.
*/
public static @CategoryType int getCategoryType(String category) {
return category.startsWith(NON_DEFAULT_CATEGORY_PREFIX)
? CategoryType.NON_DEFAULT
: CategoryType.DEFAULT;
}
/**
* @return the current tracing mode stored in the preferences. Either "record-until-full",
* "record-as-much-as-possible", or "record-continuously".
*/
public static String getSelectedTracingMode() {
return ChromeSharedPreferences.getInstance()
.readString(
ChromePreferenceKeys.SETTINGS_DEVELOPER_TRACING_MODE,
TRACING_MODES.keySet().iterator().next());
}
/**
* Select and store a new tracing mode in the preferences.
*
* @param tracingMode the new tracing mode, should be either "record-until-full",
* "record-as-much-as-possible", or "record-continuously".
*/
public static void setSelectedTracingMode(String tracingMode) {
assert TRACING_MODES.containsKey(tracingMode);
ChromeSharedPreferences.getInstance()
.writeString(ChromePreferenceKeys.SETTINGS_DEVELOPER_TRACING_MODE, tracingMode);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
SettingsUtils.addPreferencesFromResource(this, R.xml.tracing_preferences);
mPrefDefaultCategories = findPreference(UI_PREF_DEFAULT_CATEGORIES);
mPrefNondefaultCategories = findPreference(UI_PREF_NON_DEFAULT_CATEGORIES);
mPrefMode = (ListPreference) findPreference(UI_PREF_MODE);
mPrefStartRecording = findPreference(UI_PREF_START_RECORDING);
mPrefShareTrace = findPreference(UI_PREF_SHARE_TRACE);
mPrefTracingStatus = findPreference(UI_PREF_TRACING_STATUS);
mPrefDefaultCategories
.getExtras()
.putInt(TracingCategoriesSettings.EXTRA_CATEGORY_TYPE, CategoryType.DEFAULT);
mPrefNondefaultCategories
.getExtras()
.putInt(TracingCategoriesSettings.EXTRA_CATEGORY_TYPE, CategoryType.NON_DEFAULT);
mPrefMode.setEntryValues(TRACING_MODES.keySet().toArray(new String[TRACING_MODES.size()]));
String[] descriptions =
TRACING_MODES.values().toArray(new String[TRACING_MODES.values().size()]);
mPrefMode.setEntries(descriptions);
mPrefMode.setOnPreferenceChangeListener(
(preference, newValue) -> {
setSelectedTracingMode((String) newValue);
updatePreferences();
return true;
});
mPrefStartRecording.setOnPreferenceClickListener(
preference -> {
TracingController.getInstance().startRecording();
updatePreferences();
return true;
});
mPrefShareTrace.setTitle(MSG_SHARE_TRACE);
mPrefShareTrace.setOnPreferenceClickListener(
preference -> {
TracingController.getInstance().shareTrace();
updatePreferences();
return true;
});
}
@Override
public ObservableSupplier<String> getPageTitle() {
return sPageTitle;
}
@Override
public void onResume() {
super.onResume();
updatePreferences();
TracingController.getInstance().addObserver(this);
}
@Override
public void onPause() {
super.onPause();
TracingController.getInstance().removeObserver(this);
}
@Override
public void onTracingStateChanged(@TracingController.State int state) {
updatePreferences();
}
private void updatePreferences() {
@TracingController.State int state = TracingController.getInstance().getState();
boolean initialized = state != TracingController.State.INITIALIZING;
boolean idle = state == TracingController.State.IDLE || !initialized;
boolean hasTrace = state == TracingController.State.STOPPED;
boolean notificationsEnabled = TracingNotificationManager.browserNotificationsEnabled();
mPrefDefaultCategories.setEnabled(initialized);
mPrefNondefaultCategories.setEnabled(initialized);
mPrefMode.setEnabled(initialized);
mPrefStartRecording.setEnabled(idle && initialized && notificationsEnabled);
mPrefShareTrace.setEnabled(hasTrace && notificationsEnabled);
if (initialized) {
int defaultTotal = 0;
int nondefaultTotal = 0;
for (String category : TracingController.getInstance().getKnownCategories()) {
if (getCategoryType(category) == CategoryType.DEFAULT) {
defaultTotal++;
} else {
nondefaultTotal++;
}
}
int defaultEnabled = getEnabledCategories(CategoryType.DEFAULT).size();
int nondefaultEnabled = getEnabledCategories(CategoryType.NON_DEFAULT).size();
mPrefDefaultCategories.setSummary(
String.format(MSG_CATEGORIES_SUMMARY, defaultEnabled, defaultTotal));
mPrefNondefaultCategories.setSummary(
String.format(MSG_CATEGORIES_SUMMARY, nondefaultEnabled, nondefaultTotal));
mPrefMode.setValue(getSelectedTracingMode());
mPrefMode.setSummary(TRACING_MODES.get(getSelectedTracingMode()));
}
if (!notificationsEnabled) {
mPrefStartRecording.setTitle(MSG_START);
mPrefTracingStatus.setTitle(MSG_NOTIFICATIONS_DISABLED);
} else if (idle) {
mPrefStartRecording.setTitle(MSG_START);
mPrefTracingStatus.setTitle(MSG_PRIVACY_NOTICE);
} else {
mPrefStartRecording.setTitle(MSG_ACTIVE);
mPrefTracingStatus.setTitle(MSG_ACTIVE_SUMMARY);
}
}
}