Chromium Code Reviews| 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..626cac505cb63d2e62a9242e774eb7e0d6199804 |
| --- /dev/null |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/ManageSpaceActivity.java |
| @@ -0,0 +1,304 @@ |
| +// 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.base.library_loader.ProcessInitException; |
| +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. |
|
Theresa
2016/05/13 06:02:30
Is there any way to finch this or are we rolling i
dmurph
2016/05/13 23:46:24
We're rolling this out 100% immediately. I confirm
Theresa
2016/05/16 22:37:08
It'd be nice to have a kill switch in case things
|
| + * The browser process must be started here because this Activity may be started explicitly from |
|
Theresa
2016/05/13 06:02:30
nit: this line and the two below should be indente
dmurph
2016/05/13 23:46:24
Done.
|
| + * 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"; |
| + |
| + 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 (ProcessInitException 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); |
|
Theresa
2016/05/13 06:02:30
Should we set the data size text fields to "unknow
dmurph
2016/05/13 23:46:23
Done.
|
| + } |
| + } |
| + |
| + 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); |
| + UnimportantSiteDataClearer clearer = new UnimportantSiteDataClearer(); |
| + clearer.clearData(); |
| + } |
| + |
| + @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().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 = 0; |
| + |
| + /** |
| + * 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) mClearUnimportantButton.setEnabled(true); |
| + } |
| + |
| + @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().isDomainImportant()) { |
| + mNumSitesClearing++; |
| + site.clearAllStoredData(this); |
| + } else { |
| + siteStorageLeft += site.getTotalUsage(); |
| + } |
| + } |
| + if (sites.size() == 0) { |
| + Log.i(TAG, "No websites!" + sitesByOrigin + sitesByHost); |
| + 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); |
| + } |
| + } |
| +} |