| Index: chrome/android/java/src/org/chromium/chrome/browser/preferences/website/ManageSpaceActivity.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/ManageSpaceActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/ManageSpaceActivity.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..45dfbdd36cad06b297fe57a3152004cd0cda6169
|
| --- /dev/null
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/ManageSpaceActivity.java
|
| @@ -0,0 +1,315 @@
|
| +// 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.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.support.v7.app.AlertDialog;
|
| +import android.support.v7.app.AppCompatActivity;
|
| +import android.text.format.Formatter;
|
| +import android.view.View;
|
| +import android.widget.Button;
|
| +import android.widget.TextView;
|
| +
|
| +import org.chromium.base.Log;
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.chrome.R;
|
| +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.Preferences;
|
| +import org.chromium.chrome.browser.preferences.PreferencesLauncher;
|
| +import org.chromium.chrome.browser.preferences.website.Website.StoredDataClearedCallback;
|
| +
|
| +import java.util.HashSet;
|
| +import java.util.Map;
|
| +import java.util.Set;
|
| +
|
| +/**
|
| + * 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.
|
| + * TODO(dmurph): Add UMA metrics.
|
| + */
|
| +@TargetApi(Build.VERSION_CODES.KITKAT)
|
| +public class ManageSpaceActivity extends AppCompatActivity implements View.OnClickListener {
|
| + private static final String TAG = "ManageSpaceActivity";
|
| +
|
| + 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;
|
| +
|
| + @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);
|
| + }
|
| + };
|
| +
|
| + 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);
|
| + refreshStorageNumbers();
|
| + }
|
| +
|
| + @Override
|
| + public void onResume() {
|
| + super.onResume();
|
| + if (mIsNativeInitialized) refreshStorageNumbers();
|
| + }
|
| +
|
| + @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;
|
| + 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);
|
| + 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) {
|
| + 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));
|
| + }
|
| +
|
| + /** This function takes sites by origin and host and adds them all to one set. */
|
| + private static Set<Website> collapseAllSites(
|
| + Map<String, Set<Website>> sitesByOrigin, Map<String, Set<Website>> sitesByHost) {
|
| + Set<Website> sites = new HashSet<>();
|
| + // Add sites by origins.
|
| + for (Map.Entry<String, Set<Website>> element : sitesByOrigin.entrySet()) {
|
| + for (Website site : element.getValue()) {
|
| + sites.add(site);
|
| + }
|
| + }
|
| + // Add sites accessible by host name.
|
| + for (Map.Entry<String, Set<Website>> element : sitesByHost.entrySet()) {
|
| + for (Website site : element.getValue()) {
|
| + sites.add(site);
|
| + }
|
| + }
|
| + return sites;
|
| + }
|
| +
|
| + private class SizeCalculator implements WebsitePermissionsFetcher.WebsitePermissionsCallback {
|
| + @Override
|
| + public void onWebsitePermissionsAvailable(
|
| + Map<String, Set<Website>> sitesByOrigin, Map<String, Set<Website>> sitesByHost) {
|
| + Set<Website> sites = collapseAllSites(sitesByOrigin, sitesByHost);
|
| +
|
| + 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(
|
| + Map<String, Set<Website>> sitesByOrigin, Map<String, Set<Website>> sitesByHost) {
|
| + Set<Website> sites = collapseAllSites(sitesByOrigin, sitesByHost);
|
| +
|
| + 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);
|
| + }
|
| + }
|
| +}
|
|
|