// Copyright 2016 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.website; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.os.StrictMode; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.text.format.Formatter; import android.view.View; import android.widget.Button; import android.widget.TextView; import org.chromium.base.ContextUtils; import org.chromium.base.Log; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeVersionInfo; import org.chromium.chrome.browser.init.BrowserParts; import org.chromium.chrome.browser.init.ChromeBrowserInitializer; import org.chromium.chrome.browser.init.EmptyBrowserParts; import org.chromium.chrome.browser.preferences.AboutChromePreferences; import org.chromium.chrome.browser.preferences.Preferences; import org.chromium.chrome.browser.preferences.PreferencesLauncher; import org.chromium.chrome.browser.preferences.website.Website.StoredDataClearedCallback; import java.util.Collection; /** * This is the target activity for the "Manage Storage" button in the Android Settings UI. This is * configured in AndroidManifest.xml by setting android:manageSpaceActivity for the application. * The browser process must be started here because this Activity may be started explicitly from * Android settings, when Android is restoring ManageSpaceActivity after Chrome was killed, or for * tests. */ @TargetApi(Build.VERSION_CODES.KITKAT) public class ManageSpaceActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = "ManageSpaceActivity"; // Do not change these constants except for the MAX entry, they are used with UMA histograms. private static final int OPTION_CLEAR_UNIMPORTANT = 0; private static final int OPTION_MANAGE_STORAGE = 1; private static final int OPTION_CLEAR_APP_DATA = 2; private static final int OPTION_MAX = 3; private static final String PREF_FAILED_BUILD_VERSION = "ManagedSpace.FailedBuildVersion"; private TextView mUnimportantSiteDataSizeText; private TextView mSiteDataSizeText; private Button mClearUnimportantButton; private Button mManageSiteDataButton; private Button mClearAllDataButton; // Stored for testing. private AlertDialog mUnimportantDialog; private static boolean sActivityNotExportedChecked; private boolean mIsNativeInitialized; @SuppressLint("CommitPrefEdits") @Override protected void onCreate(Bundle savedInstanceState) { ensureActivityNotExported(); setContentView(R.layout.manage_space_activity); Resources r = getResources(); setTitle(String.format(r.getString(R.string.storage_management_activity_label), r.getString(R.string.app_name))); mSiteDataSizeText = (TextView) findViewById(R.id.site_data_storage_size_text); mSiteDataSizeText.setText(R.string.storage_management_computing_size); mUnimportantSiteDataSizeText = (TextView) findViewById(R.id.unimportant_site_data_storage_size_text); mUnimportantSiteDataSizeText.setText(R.string.storage_management_computing_size); mManageSiteDataButton = (Button) findViewById(R.id.manage_site_data_storage); mClearUnimportantButton = (Button) findViewById(R.id.clear_unimportant_site_data_storage); // We initially disable all of our buttons except for the 'Clear All Data' button, and wait // until the browser is finished initializing to enable them. We want to make sure the // 'Clear All Data' button is enabled so users can do this even if it's taking forever for // the Chromium process to boot up. mManageSiteDataButton.setEnabled(false); mClearUnimportantButton.setEnabled(false); mManageSiteDataButton.setOnClickListener(this); mClearUnimportantButton.setOnClickListener(this); // We should only be using this activity if we're >= KitKat. assert android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT; mClearAllDataButton = (Button) findViewById(R.id.clear_all_data); mClearAllDataButton.setOnClickListener(this); super.onCreate(savedInstanceState); BrowserParts parts = new EmptyBrowserParts() { @Override public void finishNativeInitialization() { ManageSpaceActivity.this.finishNativeInitialization(); } @Override public void onStartupFailure() { mSiteDataSizeText.setText(R.string.storage_management_startup_failure); mUnimportantSiteDataSizeText.setText(R.string.storage_management_startup_failure); } }; // Allow reading/writing to disk to check whether the last attempt was successful before // kicking off the browser process initialization. StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); StrictMode.allowThreadDiskWrites(); try { String productVersion = AboutChromePreferences.getApplicationVersion( this, ChromeVersionInfo.getProductVersion()); String failedVersion = ContextUtils.getAppSharedPreferences().getString( PREF_FAILED_BUILD_VERSION, null); if (TextUtils.equals(failedVersion, productVersion)) { parts.onStartupFailure(); return; } // If the native library crashes and kills the browser process, there is no guarantee // java-side the pref will be written before the process dies. We want to make sure we // don't attempt to start the browser process and have it kill chrome. This activity is // used to clear data for the chrome app, so it must be particularly error resistant. ContextUtils.getAppSharedPreferences().edit() .putString(PREF_FAILED_BUILD_VERSION, productVersion) .commit(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { ChromeBrowserInitializer.getInstance(getApplicationContext()) .handlePreNativeStartup(parts); ChromeBrowserInitializer.getInstance(getApplicationContext()) .handlePostNativeStartup(true, parts); } catch (Exception e) { // We don't want to exit, as the user should still be able to clear all browsing data. Log.e(TAG, "Unable to load native library.", e); mSiteDataSizeText.setText(R.string.storage_management_startup_failure); mUnimportantSiteDataSizeText.setText(R.string.storage_management_startup_failure); } } /** @see BrowserParts#finishNativeInitialization */ public void finishNativeInitialization() { mIsNativeInitialized = true; mManageSiteDataButton.setEnabled(true); mClearUnimportantButton.setEnabled(true); RecordUserAction.record("Android.ManageSpace"); refreshStorageNumbers(); } @Override public void onResume() { super.onResume(); if (mIsNativeInitialized) refreshStorageNumbers(); } @Override protected void onStop() { super.onStop(); ContextUtils.getAppSharedPreferences().edit() .putString(PREF_FAILED_BUILD_VERSION, null) .apply(); } @VisibleForTesting public Button getClearUnimportantButton() { return mClearUnimportantButton; } @VisibleForTesting public AlertDialog getUnimportantConfirmDialog() { return mUnimportantDialog; } /** This refreshes the storage numbers by fetching all site permissions. */ private void refreshStorageNumbers() { WebsitePermissionsFetcher fetcher = new WebsitePermissionsFetcher(new SizeCalculator()); fetcher.fetchPreferencesForCategory( SiteSettingsCategory.fromString(SiteSettingsCategory.CATEGORY_USE_STORAGE)); } /** Data will be cleared once we fetch all site size and important status info. */ private void clearUnimportantData() { mSiteDataSizeText.setText(R.string.storage_management_computing_size); mUnimportantSiteDataSizeText.setText(R.string.storage_management_computing_size); mClearUnimportantButton.setEnabled(false); mManageSiteDataButton.setEnabled(false); UnimportantSiteDataClearer clearer = new UnimportantSiteDataClearer(); clearer.clearData(); } /** Called after we finish clearing unimportant data. Re-enables our buttons. */ private void clearUnimportantDataDone() { mClearUnimportantButton.setEnabled(true); mManageSiteDataButton.setEnabled(true); } @Override public void onClick(View view) { if (view == mClearUnimportantButton) { if (mUnimportantDialog == null) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { mUnimportantDialog = null; RecordHistogram.recordEnumeratedHistogram("Android.ManageSpace.ActionTaken", OPTION_CLEAR_UNIMPORTANT, OPTION_MAX); clearUnimportantData(); } }); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.storage_clear_site_storage_title); builder.setMessage(R.string.storage_management_clear_unimportant_dialog_text); mUnimportantDialog = builder.create(); } mUnimportantDialog.show(); } else if (view == mManageSiteDataButton) { Intent intent = PreferencesLauncher.createIntentForSettingsPage( this, SingleCategoryPreferences.class.getName()); Bundle initialArguments = new Bundle(); initialArguments.putString(SingleCategoryPreferences.EXTRA_CATEGORY, SiteSettingsCategory.CATEGORY_USE_STORAGE); initialArguments.putString(SingleCategoryPreferences.EXTRA_TITLE, getString(R.string.website_settings_storage)); intent.putExtra(Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS, initialArguments); RecordHistogram.recordEnumeratedHistogram( "Android.ManageSpace.ActionTaken", OPTION_MANAGE_STORAGE, OPTION_MAX); startActivity(intent); } else if (view == mClearAllDataButton) { final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { if (mIsNativeInitialized) { // This probably won't actually be uploaded, as android will probably kill // all processes & data before it gets sent to the network. RecordHistogram.recordEnumeratedHistogram("Android.ManageSpace.ActionTaken", OPTION_CLEAR_APP_DATA, OPTION_MAX); } activityManager.clearApplicationUserData(); } }); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.storage_management_reset_app_dialog_title); builder.setMessage(R.string.storage_management_reset_app_dialog_text); builder.create().show(); } } private void onSiteStorageSizeCalculated(long totalSize, long unimportantSize) { mSiteDataSizeText.setText(Formatter.formatFileSize(this, totalSize)); mUnimportantSiteDataSizeText.setText(Formatter.formatFileSize(this, unimportantSize)); } private class SizeCalculator implements WebsitePermissionsFetcher.WebsitePermissionsCallback { @Override public void onWebsitePermissionsAvailable(Collection<Website> sites) { long siteStorageSize = 0; long importantSiteStorageTotal = 0; for (Website site : sites) { siteStorageSize += site.getTotalUsage(); if (site.getLocalStorageInfo() != null && site.getLocalStorageInfo().isDomainImportant()) { importantSiteStorageTotal += site.getTotalUsage(); } } onSiteStorageSizeCalculated( siteStorageSize, siteStorageSize - importantSiteStorageTotal); } } private class UnimportantSiteDataClearer implements WebsitePermissionsFetcher.WebsitePermissionsCallback, StoredDataClearedCallback { // We keep track of the number of sites waiting to be cleared, and when it reaches 0 we can // set our testing variable. private int mNumSitesClearing; /** * We fetch all the websites and clear all the non-important data. This happens * asynchronously, and at the end we update the UI with the new storage numbers. */ public void clearData() { WebsitePermissionsFetcher fetcher = new WebsitePermissionsFetcher(this); fetcher.fetchPreferencesForCategory( SiteSettingsCategory.fromString(SiteSettingsCategory.CATEGORY_USE_STORAGE)); } @Override public void onStoredDataCleared() { mNumSitesClearing--; if (mNumSitesClearing <= 0) clearUnimportantDataDone(); } @Override public void onWebsitePermissionsAvailable(Collection<Website> sites) { long siteStorageLeft = 0; for (Website site : sites) { if (site.getLocalStorageInfo() == null || !site.getLocalStorageInfo().isDomainImportant()) { mNumSitesClearing++; site.clearAllStoredData(this); } else { siteStorageLeft += site.getTotalUsage(); } } if (sites.size() == 0) { onStoredDataCleared(); } onSiteStorageSizeCalculated(siteStorageLeft, 0); } } // If ManageSpaceActivity is exported, then it's vulnerable to a fragment injection exploit: // http://securityintelligence.com/new-vulnerability-android-framework-fragment-injection private void ensureActivityNotExported() { if (sActivityNotExportedChecked) return; sActivityNotExportedChecked = true; try { ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), 0); if (activityInfo.exported) { throw new IllegalStateException("ManageSpaceActivity must not be exported."); } } catch (NameNotFoundException ex) { // Something terribly wrong has happened. throw new RuntimeException(ex); } } }