// Copyright 2014 The Chromium Authors. All rights reserved. // 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.preferences; import android.Manifest; import android.annotation.SuppressLint; import android.app.Fragment; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.BitmapFactory; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; import android.os.Process; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceFragment.OnPreferenceStartFragmentCallback; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.SuppressFBWarnings; import org.chromium.base.library_loader.ProcessInitException; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeApplication; import org.chromium.chrome.browser.help.HelpAndFeedback; import org.chromium.chrome.browser.init.ChromeBrowserInitializer; import org.chromium.chrome.browser.profiles.Profile; /** * The Chrome settings activity. * * This activity displays a single Fragment, typically a PreferenceFragment. As the user navigates * through settings, a separate Preferences activity is created for each screen. Thus each fragment * may freely modify its activity's action bar or title. This mimics the behavior of * android.preference.PreferenceActivity. */ public class Preferences extends AppCompatActivity implements OnPreferenceStartFragmentCallback { public static final String EXTRA_SHOW_FRAGMENT = "show_fragment"; public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = "show_fragment_args"; private static final String TAG = "Preferences"; /** The current instance of Preferences in the resumed state, if any. */ private static Preferences sResumedInstance; /** Whether this activity has been created for the first time but not yet resumed. */ private boolean mIsNewlyCreated; private static boolean sActivityNotExportedChecked; @SuppressFBWarnings("DM_EXIT") @SuppressLint("InlinedApi") @Override protected void onCreate(Bundle savedInstanceState) { ensureActivityNotExported(); // The browser process must be started here because this Activity may be started explicitly // from Android notifications, when Android is restoring Preferences 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. try { ChromeBrowserInitializer.getInstance(this).handleSynchronousStartup(); } catch (ProcessInitException e) { Log.e(TAG, "Failed to start browser process.", e); // This can only ever happen, if at all, when the activity is started from an Android // notification (or in tests). As such we don't want to show an error messsage to the // user. The application is completely broken at this point, so close it down // completely (not just the activity). System.exit(-1); return; } super.onCreate(savedInstanceState); mIsNewlyCreated = savedInstanceState == null; String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 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 = MainPreferences.class.getName(); Fragment fragment = Fragment.instantiate(this, initialFragment, initialArguments); getFragmentManager().beginTransaction() .replace(android.R.id.content, fragment) .commit(); } if (ApiCompatibilityUtils.checkPermission( this, Manifest.permission.NFC, Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED) { // Disable Android Beam on JB and later devices. // In ICS it does nothing - i.e. we will send a Play Store link if NFC is used. NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); if (nfcAdapter != null) nfcAdapter.setNdefPushMessage(null, this); } Resources res = getResources(); ApiCompatibilityUtils.setTaskDescription(this, res.getString(R.string.app_name), BitmapFactory.decodeResource(res, R.mipmap.app_icon), ApiCompatibilityUtils.getColor(res, R.color.default_primary_color)); } // OnPreferenceStartFragmentCallback: @Override public boolean onPreferenceStartFragment(PreferenceFragment preferenceFragment, Preference preference) { startFragment(preference.getFragment(), preference.getExtras()); return true; } /** * Starts a new Preferences 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(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Fragment fragment = getFragmentManager().findFragmentById(android.R.id.content); if (fragment instanceof PreferenceFragment && fragment.getView() != null) { // Set list view padding to 0 so dividers are the full width of the screen. fragment.getView().findViewById(android.R.id.list).setPadding(0, 0, 0, 0); } } } @Override protected void onResume() { super.onResume(); // Prevent the user from interacting with multiple instances of Preferences 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 Preferences 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; } } @Override protected void onPause() { super.onPause(); ChromeApplication.flushPersistentData(); } @Override protected void onStop() { super.onStop(); if (sResumedInstance == this) sResumedInstance = null; } /** * Returns the fragment showing as this activity's main content, typically a PreferenceFragment. * This does not include DialogFragments or other Fragments shown on top of the main content. */ @VisibleForTesting public Fragment getFragmentForTest() { return getFragmentManager().findFragmentById(android.R.id.content); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(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(R.drawable.ic_help_and_feedback); return true; } @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) { if (item.getItemId() == android.R.id.home) { finish(); return true; } else if (item.getItemId() == R.id.menu_id_general_help) { HelpAndFeedback.getInstance(this).show(this, getString(R.string.help_context_settings), Profile.getLastUsedProfile(), null); return true; } return super.onOptionsItemSelected(item); } private void ensureActivityNotExported() { if (sActivityNotExportedChecked) return; sActivityNotExportedChecked = true; try { ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), 0); // If Preferences 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("Preferences must not be exported."); } } catch (NameNotFoundException ex) { // Something terribly wrong has happened. throw new RuntimeException(ex); } } }