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..a29247b1796069a0925556f0cc31d4e16c0bedde |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/ManageSpaceActivity.java |
@@ -0,0 +1,274 @@ |
+// 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.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. |
+ * 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"; |
+ |
+ private TextView mUnimportantSiteDataSizeText; |
+ private TextView mSiteDataSizeText; |
+ private Button mClearUnimportantButton; |
+ private Button mManageSiteDataButton; |
+ private Button mClearAllDataButton; |
+ |
+ 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); |
+ } |
+ } |
+ |
+ public void finishNativeInitialization() { |
+ mIsNativeInitialized = true; |
+ mManageSiteDataButton.setEnabled(true); |
+ mClearUnimportantButton.setEnabled(true); |
+ refreshStorageNumbers(); |
+ } |
+ |
+ @Override |
+ public void onResume() { |
+ super.onResume(); |
+ if (mIsNativeInitialized) refreshStorageNumbers(); |
+ } |
+ |
+ /** 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); |
+ UnimportantSiteDataClearer clearer = new UnimportantSiteDataClearer(); |
+ clearer.clearData(); |
+ } |
+ |
+ @Override |
+ public void onClick(View view) { |
+ if (view == mClearUnimportantButton) { |
+ AlertDialog.Builder builder = new AlertDialog.Builder(this); |
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
+ @Override |
+ public void onClick(DialogInterface dialog, int id) { |
+ 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); |
+ builder.create().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()) { |
+ Log.i(TAG, site.getAddress().getOrigin() + " is important."); |
+ importantSiteStorageTotal += site.getTotalUsage(); |
+ } |
+ } |
+ onSiteStorageSizeCalculated( |
+ siteStorageSize, siteStorageSize - importantSiteStorageTotal); |
+ } |
+ } |
+ |
+ private class UnimportantSiteDataClearer |
+ implements WebsitePermissionsFetcher.WebsitePermissionsCallback { |
+ /** |
+ * 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 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()) { |
+ site.clearAllStoredData(new StoredDataClearedCallback() { |
+ @Override |
+ public void onStoredDataCleared() {} |
+ }); |
+ } else { |
+ siteStorageLeft += site.getTotalUsage(); |
+ } |
+ } |
+ 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); |
+ } |
+ } |
+} |