// Copyright 2014 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.settings;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Color;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import org.chromium.base.BuildInfo;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeBaseAppCompatActivity;
import org.chromium.chrome.browser.back_press.BackPressHelper;
import org.chromium.chrome.browser.back_press.SecondaryActivityBackPressUma.SecondaryActivity;
import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherImpl;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.profiles.ProfileManagerUtils;
import org.chromium.chrome.browser.ui.device_lock.MissingDeviceLockLauncher;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarManageable;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerFactory;
import org.chromium.components.browser_ui.bottomsheet.ManagedBottomSheetController;
import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
import org.chromium.components.browser_ui.settings.SettingsPage;
import org.chromium.components.browser_ui.util.TraceEventVectorDrawableCompat;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
import java.util.Locale;
/**
* The Chrome settings activity.
*
* <p>This activity displays a single {@link Fragment}, typically a {@link
* PreferenceFragmentCompat}. As the user navigates through settings, a separate Settings activity
* is created for each screen. Thus each fragment may freely modify its activity's action bar or
* title. This mimics the behavior of {@link android.preference.PreferenceActivity}.
*/
public class SettingsActivity extends ChromeBaseAppCompatActivity
implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, SnackbarManageable {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String EXTRA_SHOW_FRAGMENT = "show_fragment";
static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = "show_fragment_args";
/** The current instance of SettingsActivity in the resumed state, if any. */
private static SettingsActivity sResumedInstance;
/** Whether this activity has been created for the first time but not yet resumed. */
private boolean mIsNewlyCreated;
private static boolean sActivityNotExportedChecked;
private Profile mProfile;
private ScrimCoordinator mScrim;
private ManagedBottomSheetController mManagedBottomSheetController;
private final OneshotSupplierImpl<BottomSheetController> mBottomSheetControllerSupplier =
new OneshotSupplierImpl<>();
private final OneshotSupplierImpl<SnackbarManager> mSnackbarManagerSupplier =
new OneshotSupplierImpl<>();
private FragmentDependencyProvider mFragmentDependencyProvider;
// This is only used on automotive.
private @Nullable MissingDeviceLockLauncher mMissingDeviceLockLauncher;
@SuppressLint("InlinedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
setTitle(R.string.settings);
ensureActivityNotExported();
// The browser process must be started here because this Activity may be started explicitly
// from Android notifications, when Android is restoring Settings after Chrome was
// killed, or for tests. This should happen before super.onCreate() because it might
// recreate a fragment, and a fragment might depend on the native library.
ChromeBrowserInitializer.getInstance().handleSynchronousStartup();
mProfile = ProfileManager.getLastUsedRegularProfile();
// Initialize FragmentDependencyProvider before calling super.onCreate() because it may
// create fragments if there is a saved instance state.
mFragmentDependencyProvider =
new FragmentDependencyProvider(
this,
mProfile,
mSnackbarManagerSupplier,
mBottomSheetControllerSupplier,
getModalDialogManagerSupplier());
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
Toolbar actionBar = findViewById(R.id.action_bar);
setSupportActionBar(actionBar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mIsNewlyCreated = savedInstanceState == null;
String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
// If savedInstanceState is non-null, then the activity is being
// recreated and super.onCreate() has already recreated the fragment.
if (savedInstanceState == null) {
if (initialFragment == null) initialFragment = MainSettings.class.getName();
Fragment fragment = Fragment.instantiate(this, initialFragment, initialArguments);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.content, fragment)
.runOnCommit(this::onMainFragmentCommitted)
.commit();
} else {
getSupportFragmentManager()
.beginTransaction()
.runOnCommit(this::onMainFragmentCommitted)
.commit();
}
setStatusBarColor();
initBottomSheet();
mSnackbarManagerSupplier.set(
new SnackbarManager(this, findViewById(android.R.id.content), null));
}
/**
* Called when the main fragment is committed with a fragment transaction. We can assume here
* that the main fragment and its views exist.
*/
private void onMainFragmentCommitted() {
Fragment mainFragment = getMainFragment();
// TODO(b/356743945): Enforce that all main fragments implement SettingsPage.
// For now, PrivacyGuideFragment is shown with SettingsActivity but it does not implement
// SettingsPage.
if (mainFragment instanceof SettingsPage settingFragment) {
settingFragment
.getPageTitle()
.addObserver(
(title) -> {
if (title == null) {
title = "";
}
setTitle(title);
});
}
// Apply the wide display style after the main fragment is committed since its views
// (particularly a recycler view) are not accessible before the transaction completes.
WideDisplayPadding.apply(mainFragment, this);
}
/** Set up the bottom sheet for this activity. */
private void initBottomSheet() {
ViewGroup sheetContainer = findViewById(R.id.sheet_container);
mScrim =
new ScrimCoordinator(
this,
new ScrimCoordinator.SystemUiScrimDelegate() {
@Override
public void setStatusBarScrimFraction(float scrimFraction) {
// TODO: Implement if status bar needs to change color with the
// scrim.
}
@Override
public void setNavigationBarScrimFraction(float scrimFraction) {
// TODO: Implement if navigation bar needs to change color with the
// scrim.
}
},
(ViewGroup) sheetContainer.getParent(),
getColor(R.color.default_scrim_color));
mManagedBottomSheetController =
BottomSheetControllerFactory.createBottomSheetController(
() -> mScrim,
(sheet) -> {},
getWindow(),
KeyboardVisibilityDelegate.getInstance(),
() -> sheetContainer,
() -> 0);
mBottomSheetControllerSupplier.set(mManagedBottomSheetController);
}
// OnPreferenceStartFragmentCallback:
@Override
public boolean onPreferenceStartFragment(
PreferenceFragmentCompat caller, Preference preference) {
startFragment(preference.getFragment(), preference.getExtras());
return true;
}
/**
* Starts a new Settings activity showing the desired fragment.
*
* @param fragmentClass The Class of the fragment to show.
* @param args Arguments to pass to Fragment.instantiate(), or null.
*/
public void startFragment(String fragmentClass, Bundle args) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClass(this, getClass());
intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentClass);
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
startActivity(intent);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
initBackPressHandler();
}
@Override
protected void onResume() {
super.onResume();
// Prevent the user from interacting with multiple instances of SettingsActivity at the same
// time (e.g. in multi-instance mode on a Samsung device), which would cause many fun bugs.
if (sResumedInstance != null
&& sResumedInstance.getTaskId() != getTaskId()
&& !mIsNewlyCreated) {
// This activity was unpaused or recreated while another instance of SettingsActivity
// was already showing. The existing instance takes precedence.
finish();
} else {
// This activity was newly created and takes precedence over sResumedInstance.
if (sResumedInstance != null && sResumedInstance.getTaskId() != getTaskId()) {
sResumedInstance.finish();
}
sResumedInstance = this;
mIsNewlyCreated = false;
}
checkForMissingDeviceLockOnAutomotive();
}
private void checkForMissingDeviceLockOnAutomotive() {
if (BuildInfo.getInstance().isAutomotive) {
if (mMissingDeviceLockLauncher == null) {
mMissingDeviceLockLauncher =
new MissingDeviceLockLauncher(
this, mProfile, getModalDialogManagerSupplier().get());
}
mMissingDeviceLockLauncher.checkPrivateDataIsProtectedByDeviceLock();
}
}
@Override
protected void onPause() {
super.onPause();
ProfileManagerUtils.flushPersistentDataForAllProfiles();
}
@Override
protected void onStop() {
super.onStop();
if (sResumedInstance == this) sResumedInstance = null;
}
@Override
protected void onDestroy() {
mScrim.destroy();
super.onDestroy();
}
/**
* Returns the fragment showing as this activity's main content, typically a {@link
* PreferenceFragmentCompat}. This does not include dialogs or other {@link Fragment}s shown on
* top of the main content.
*/
@VisibleForTesting
public Fragment getMainFragment() {
return getSupportFragmentManager().findFragmentById(R.id.content);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// By default, every screen in Settings shows a "Help & feedback" menu item.
MenuItem help =
menu.add(
Menu.NONE,
R.id.menu_id_general_help,
Menu.CATEGORY_SECONDARY,
R.string.menu_help);
help.setIcon(
TraceEventVectorDrawableCompat.create(
getResources(), R.drawable.ic_help_and_feedback, getTheme()));
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (menu.size() == 1) {
MenuItem item = menu.getItem(0);
if (item.getIcon() != null) item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Fragment mainFragment = getMainFragment();
if (mainFragment != null && mainFragment.onOptionsItemSelected(item)) {
return true;
}
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.menu_id_general_help) {
HelpAndFeedbackLauncherImpl.getForProfile(mProfile)
.show(this, getString(R.string.help_context_settings), null);
return true;
}
return super.onOptionsItemSelected(item);
}
private void initBackPressHandler() {
// Handlers registered last will be called first.
registerMainFragmentBackPressHandler();
registerBottomSheetBackPressHandler();
}
private void registerMainFragmentBackPressHandler() {
Fragment activeFragment = getMainFragment();
if (activeFragment instanceof BackPressHandler) {
BackPressHelper.create(
activeFragment.getViewLifecycleOwner(),
getOnBackPressedDispatcher(),
(BackPressHandler) activeFragment,
SecondaryActivity.SETTINGS);
}
}
private void registerBottomSheetBackPressHandler() {
if (!mBottomSheetControllerSupplier.hasValue()) return;
BackPressHandler bottomSheetBackPressHandler =
mBottomSheetControllerSupplier.get().getBottomSheetBackPressHandler();
if (bottomSheetBackPressHandler != null) {
BackPressHelper.create(
this,
getOnBackPressedDispatcher(),
bottomSheetBackPressHandler,
SecondaryActivity.SETTINGS);
}
}
@Override
public void onAttachFragment(Fragment fragment) {
String className = fragment.getClass().getSimpleName();
RecordHistogram.recordSparseHistogram("Settings.FragmentAttached", className.hashCode());
// Log hashCode to easily add new class names to enums.xml.
Log.d(
"SettingsActivity",
String.format(
Locale.ENGLISH,
"Settings.FragmentAttached: <int value=\"%d\" label=\"%s\"/>",
className.hashCode(),
className));
mFragmentDependencyProvider.provide(fragment);
}
@Override
public SnackbarManager getSnackbarManager() {
return mSnackbarManagerSupplier.get();
}
private void ensureActivityNotExported() {
if (sActivityNotExportedChecked) return;
sActivityNotExportedChecked = true;
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), 0);
// If SettingsActivity is exported, then it's vulnerable to a fragment injection
// exploit:
// http://securityintelligence.com/new-vulnerability-android-framework-fragment-injection
if (activityInfo.exported) {
throw new IllegalStateException("SettingsActivity must not be exported.");
}
} catch (NameNotFoundException ex) {
// Something terribly wrong has happened.
throw new RuntimeException(ex);
}
}
/** Set device status bar to match the activity background color, if supported. */
private void setStatusBarColor() {
// On P+, the status bar color is set via the XML theme.
if (VERSION.SDK_INT >= Build.VERSION_CODES.P
&& !BuildInfo.getInstance().isAutomotive
&& !DeviceFormFactor.isNonMultiDisplayContextOnTablet(this)) {
return;
}
if (UiUtils.isSystemUiThemingDisabled()) return;
// Use transparent color, so the AppBarLayout can color the status bar on scroll.
UiUtils.setStatusBarColor(getWindow(), Color.TRANSPARENT);
// Set status bar icon color according to background color.
UiUtils.setStatusBarIconColor(
getWindow().getDecorView().getRootView(),
getResources().getBoolean(R.bool.window_light_status_bar));
}
@Override
protected ModalDialogManager createModalDialogManager() {
return new ModalDialogManager(new AppModalPresenter(this), ModalDialogType.APP);
}
}