Bug 706876 - Create "Clear Site Settings" menuitem. r=mfinkle
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Sat, 17 Dec 2011 13:50:09 -0800
changeset 84535 f4c27f53131d0bd9a7d817bc28fc4d6f0762d6f1
parent 84534 e72b863261249ff89ccd8de9f3cd8cde8779d8d7
child 84536 cd1252510d17b0f4e678722829e77652ccf17877
push id519
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 00:38:35 +0000
treeherdermozilla-beta@788ea1ef610b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs706876
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 706876 - Create "Clear Site Settings" menuitem. r=mfinkle
mobile/android/base/GeckoApp.java
mobile/android/base/Makefile.in
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/layout/gecko_menu.xml
mobile/android/base/resources/layout/site_setting_title.xml
mobile/android/base/strings.xml.in
mobile/android/chrome/content/browser.js
mobile/android/locales/en-US/chrome/browser.properties
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -539,16 +539,19 @@ abstract public class GeckoApp
                 return true;
             case R.id.save_as_pdf:
                 GeckoAppShell.sendEventToGecko(new GeckoEvent("SaveAs:PDF", null));
                 return true;
             case R.id.preferences:
                 intent = new Intent(this, GeckoPreferences.class);
                 startActivity(intent);
                 return true;
+            case R.id.site_settings:
+                GeckoAppShell.sendEventToGecko(new GeckoEvent("Permissions:Get", null));
+                return true;
             case R.id.addons:
                 GeckoAppShell.sendEventToGecko(new GeckoEvent("about:addons"));
                 return true;
             case R.id.agent_mode:
                 Tab selectedTab = Tabs.getInstance().getSelectedTab();
                 if (selectedTab == null)
                     return true;
                 JSONObject args = new JSONObject();
@@ -971,16 +974,20 @@ abstract public class GeckoApp
                 int tabId = message.getInt("tabId");
                 Tab tab = Tabs.getInstance().getTab(tabId);
                 if (tab == null)
                     return;
 
                 tab.setAgentMode(agentMode);
                 if (tab == Tabs.getInstance().getSelectedTab())
                     updateAgentModeMenuItem(tab, agentMode);
+            } else if (event.equals("Permissions:Data")) {
+                String host = message.getString("host");
+                JSONArray permissions = message.getJSONArray("permissions");
+                showSiteSettingsDialog(host, permissions);
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     public void showAboutHome() {
         Runnable r = new AboutHomeRunnable(true);
@@ -1025,16 +1032,83 @@ abstract public class GeckoApp
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     int strId = agentMode == Tab.AgentMode.MOBILE ? R.string.agent_request_desktop : R.string.agent_request_mobile;
                     sMenu.findItem(R.id.agent_mode).setTitle(getString(strId));
                 }
             }
         });
     }
 
+    /**
+     * @param aPermissions
+     *        Array of JSON objects to represent site permissions.
+     *        Example: { type: "offline-app", setting: "Store Offline Data: Allow" }
+     */
+    private void showSiteSettingsDialog(String aHost, JSONArray aPermissions) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+
+        View customTitleView = getLayoutInflater().inflate(R.layout.site_setting_title, null);
+        ((TextView) customTitleView.findViewById(R.id.title)).setText(R.string.site_settings_title);
+        ((TextView) customTitleView.findViewById(R.id.host)).setText(aHost);        
+        builder.setCustomTitle(customTitleView);
+
+        // If there are no permissions to clear, show the user a message about that.
+        // In the future, we want to disable the menu item if there are no permissions to clear.
+        if (aPermissions.length() == 0) {
+            builder.setMessage(R.string.site_settings_no_settings);
+        } else {
+            // Eventually we should use a list adapter and custom checkable list items
+            // to make a two-line UI to match the mock-ups
+            CharSequence[] items = new CharSequence[aPermissions.length()];
+            boolean[] states = new boolean[aPermissions.length()];
+            for (int i = 0; i < aPermissions.length(); i++) {
+                try {
+                    items[i] = aPermissions.getJSONObject(i).
+                               getString("setting");
+                    // Make all the items checked by default
+                    states[i] = true;
+                } catch (JSONException e) {
+                    Log.i(LOGTAG, "JSONException: " + e);
+                }
+            }
+            builder.setMultiChoiceItems(items, states, new DialogInterface.OnMultiChoiceClickListener(){
+                public void onClick(DialogInterface dialog, int item, boolean state) {
+                    // Do nothing
+                }
+            });
+            builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int id) {
+                    ListView listView = ((AlertDialog) dialog).getListView();
+                    SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
+
+                    // An array of the indices of the permissions we want to clear
+                    JSONArray permissionsToClear = new JSONArray();
+                    for (int i = 0; i < checkedItemPositions.size(); i++) {
+                        boolean checked = checkedItemPositions.get(i);
+                        if (checked)
+                            permissionsToClear.put(i);
+                    }
+                    GeckoAppShell.sendEventToGecko(new GeckoEvent("Permissions:Clear", permissionsToClear.toString()));
+                }
+            });
+        }
+
+        builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener(){
+            public void onClick(DialogInterface dialog, int id) {
+                dialog.cancel();
+            }            
+        });
+
+        mMainHandler.post(new Runnable() {
+            public void run() {
+                builder.create().show();
+            }
+        });
+    }
+
     void handleDoorHanger(JSONObject geckoObject) throws JSONException {
         final String message = geckoObject.getString("message");
         final String value = geckoObject.getString("value");
         final JSONArray buttons = geckoObject.getJSONArray("buttons");
         final int tabId = geckoObject.getInt("tabID");
         final JSONObject options = geckoObject.getJSONObject("options");
 
         Log.i(LOGTAG, "DoorHanger received for tab " + tabId + ", msg:" + message);
@@ -1458,16 +1532,17 @@ abstract public class GeckoApp
         GeckoAppShell.registerGeckoEventListener("Menu:Add", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Menu:Remove", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Gecko:Ready", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Toast:Show", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("ToggleChrome:Hide", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("ToggleChrome:Show", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("AgentMode:Changed", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("FormAssist:AutoComplete", GeckoApp.mAppContext);
+        GeckoAppShell.registerGeckoEventListener("Permissions:Data", GeckoApp.mAppContext);
 
         mConnectivityFilter = new IntentFilter();
         mConnectivityFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
         mConnectivityReceiver = new GeckoConnectivityReceiver();
 
         IntentFilter batteryFilter = new IntentFilter();
         batteryFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
         mBatteryReceiver = new GeckoBatteryManager();
@@ -1688,16 +1763,17 @@ abstract public class GeckoApp
         GeckoAppShell.unregisterGeckoEventListener("Menu:Add", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Menu:Remove", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Gecko:Ready", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Toast:Show", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("ToggleChrome:Hide", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("ToggleChrome:Show", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("AgentMode:Changed", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("FormAssist:AutoComplete", GeckoApp.mAppContext);
+        GeckoAppShell.unregisterGeckoEventListener("Permissions:Data", GeckoApp.mAppContext);
 
         mFavicons.close();
 
         super.onDestroy();
 
         unregisterReceiver(mSmsReceiver);
         unregisterReceiver(mBatteryReceiver);
     }
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -187,16 +187,17 @@ RES_LAYOUT = \
   res/layout/doorhanger.xml \
   res/layout/gecko_app.xml \
   res/layout/gecko_menu.xml \
   res/layout/launch_app_list.xml \
   res/layout/launch_app_listitem.xml \
   res/layout/notification_icon_text.xml \
   res/layout/notification_progress.xml \
   res/layout/notification_progress_text.xml \
+  res/layout/site_setting_title.xml \
   res/layout/tabs_row.xml \
   res/layout/tabs_tray.xml \
   res/layout/list_item_header.xml \
   res/layout/select_dialog_list.xml \
   res/layout/abouthome_content.xml \
   res/layout/abouthome_topsite_item.xml \
   res/layout/abouthome_addon_row.xml \
   $(NULL)
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -75,8 +75,13 @@
 <!ENTITY quit "Quit">
 
 <!ENTITY addons "Add-ons">
 
 <!ENTITY share "Share">
 <!ENTITY save_as_pdf "Save as PDF">
 <!ENTITY agent_request_desktop "Request Desktop Site">
 <!ENTITY agent_request_mobile "Request Mobile Site">
+
+<!ENTITY site_settings_title        "Clear Site Settings">
+<!ENTITY site_settings_cancel       "Cancel">
+<!ENTITY site_settings_clear        "Clear">
+<!ENTITY site_settings_no_settings  "There are no settings to clear.">
--- a/mobile/android/base/resources/layout/gecko_menu.xml
+++ b/mobile/android/base/resources/layout/gecko_menu.xml
@@ -22,14 +22,17 @@
           android:title="@string/save_as_pdf" />
 
     <item android:id="@+id/agent_mode"
           android:title="@string/agent_request_desktop" />
 
     <item android:id="@+id/preferences"
           android:title="@string/preferences" />
 
+    <item android:id="@+id/site_settings"
+          android:title="@string/site_settings_title" />
+
     <item android:id="@+id/addons"
           android:title="@string/addons"/>
 
     <item android:id="@+id/quit"
           android:title="@string/quit" />
 </menu>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/site_setting_title.xml
@@ -0,0 +1,28 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:layout_weight="1"
+              android:orientation="vertical"
+              android:gravity="center_vertical">
+
+    <TextView android:id="@+id/title"
+              style="?android:attr/windowTitleStyle"
+              android:textAppearance="?android:attr/textAppearanceLarge"
+              android:layout_width="match_parent" 
+              android:layout_height="wrap_content"
+              android:paddingTop="6dip"
+              android:paddingBottom="0dip"
+              android:paddingLeft="10dip"
+              android:paddingRight="10dip"/>
+
+    <TextView android:id="@+id/host"
+              style="?android:attr/windowTitleStyle"
+              android:textAppearance="?android:attr/textAppearanceMedium"
+              android:layout_width="match_parent" 
+              android:layout_height="wrap_content"
+              android:paddingTop="2dip"
+              android:paddingBottom="6dip"
+              android:paddingLeft="10dip"
+              android:paddingRight="10dip"/>
+
+</LinearLayout>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -77,9 +77,14 @@
   <string name="pref_font_size_medium">&pref_font_size_medium;</string>
   <string name="pref_font_size_large">&pref_font_size_large;</string>
   <string name="pref_font_size_xlarge">&pref_font_size_xlarge;</string>
 
   <string name="reload">&reload;</string>
   <string name="forward">&forward;</string>
   <string name="new_tab">&new_tab;</string>
   <string name="addons">&addons;</string>
+
+  <string name="site_settings_title">&site_settings_title;</string>
+  <string name="site_settings_cancel">&site_settings_cancel;</string>
+  <string name="site_settings_clear">&site_settings_clear;</string>
+  <string name="site_settings_no_settings">&site_settings_no_settings;</string>
 </resources>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -202,16 +202,17 @@ var BrowserApp = {
     NativeWindow.init();
     Downloads.init();
     FormAssistant.init();
     OfflineApps.init();
     IndexedDB.init();
     XPInstallObserver.init();
     ConsoleAPI.init();
     ClipboardHelper.init();
+    PermissionsHelper.init();
 
     // Init LoginManager
     Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
     // Init FormHistory
     Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2);
 
     let uri = "about:home";
     if ("arguments" in window && window.arguments[0])
@@ -3254,8 +3255,164 @@ var PluginHelper = {
     // XXX bug 446693. The text-shadow on the submitted-report text at
     //     the bottom causes scrollHeight to be larger than it should be.
     let overflows = (overlay.scrollWidth > pluginRect.width) ||
                     (overlay.scrollHeight - 5 > pluginRect.height);
 
     return overflows;
   }
 };
+
+var PermissionsHelper = {
+
+  _permissonTypes: ["password", "geo", "popup", "indexedDB",
+                    "offline-app", "desktop-notification"],
+  _permissionStrings: {
+    "password": {
+      label: "password.rememberPassword",
+      allowed: "password.remember",
+      denied: "password.never"
+    },
+    "geo": {
+      label: "geolocation.shareLocation",
+      allowed: "geolocation.alwaysShare",
+      denied: "geolocation.neverShare"
+    },
+    "popup": {
+      label: "blockPopups.label",
+      allowed: "popupButtonAlwaysAllow2",
+      denied: "popupButtonNeverWarn2"
+    },
+    "indexedDB": {
+      label: "offlineApps.storeOfflineData",
+      allowed: "offlineApps.allow",
+      denied: "offlineApps.never"
+    },
+    "offline-app": {
+      label: "offlineApps.storeOfflineData",
+      allowed: "offlineApps.allow",
+      denied: "offlineApps.never"
+    },
+    "desktop-notification": {
+      label: "desktopNotification.useNotifications",
+      allowed: "desktopNotification.allow",
+      denied: "desktopNotification.dontAllow"
+    }
+  },
+
+  init: function init() {
+    Services.obs.addObserver(this, "Permissions:Get", false);
+    Services.obs.addObserver(this, "Permissions:Clear", false);
+  },
+
+  observe: function observe(aSubject, aTopic, aData) {
+    let uri = BrowserApp.selectedBrowser.currentURI;
+
+    switch (aTopic) {
+      case "Permissions:Get":
+        let permissions = [];
+        for (let i = 0; i < this._permissonTypes.length; i++) {
+          let type = this._permissonTypes[i];
+          let value = this.getPermission(uri, type);
+
+          // Only add the permission if it was set by the user
+          if (value == Services.perms.UNKNOWN_ACTION)
+            continue;
+
+          // Get the strings that correspond to the permission type
+          let typeStrings = this._permissionStrings[type];
+          let label = Strings.browser.GetStringFromName(typeStrings["label"]);
+
+          // Get the key to look up the appropriate string entity
+          let valueKey = value == Services.perms.ALLOW_ACTION ?
+                         "allowed" : "denied";
+          let valueString = Strings.browser.GetStringFromName(typeStrings[valueKey]);
+
+          // If we implement a two-line UI, we will need to pass the label and
+          // value individually and let java handle the formatting
+          let setting = Strings.browser.formatStringFromName("siteSettings.labelToValue",
+                                                             [ label, valueString ], 2)
+          permissions.push({
+            type: type,
+            setting: setting
+          });
+        }
+
+        // Keep track of permissions, so we know which ones to clear
+        this._currentPermissions = permissions; 
+
+        sendMessageToJava({
+          gecko: {
+            type: "Permissions:Data",
+            host: uri.host,
+            permissions: permissions
+          }
+        });
+        break;
+ 
+      case "Permissions:Clear":
+        // An array of the indices of the permissions we want to clear
+        let permissionsToClear = JSON.parse(aData);
+
+        for (let i = 0; i < permissionsToClear.length; i++) {
+          let indexToClear = permissionsToClear[i];
+          let permissionType = this._currentPermissions[indexToClear]["type"];
+          this.clearPermission(uri, permissionType);
+        }
+        break;
+    }
+  },
+
+  /**
+   * Gets the permission value stored for a specified permission type.
+   *
+   * @param aType
+   *        The permission type string stored in permission manager.
+   *        e.g. "cookie", "geo", "indexedDB", "popup", "image"
+   *
+   * @return A permission value defined in nsIPermissionManager.
+   */
+  getPermission: function getPermission(aURI, aType) {
+    // Password saving isn't a nsIPermissionManager permission type, so handle
+    // it seperately.
+    if (aType == "password") {
+      // By default, login saving is enabled, so if it is disabled, the
+      // user selected the never remember option
+      if (!Services.logins.getLoginSavingEnabled(aURI.prePath))
+        return Services.perms.DENY_ACTION;
+
+      // Check to see if the user ever actually saved a login
+      if (Services.logins.countLogins(aURI.prePath, "", ""))
+        return Services.perms.ALLOW_ACTION;
+
+      return Services.perms.UNKNOWN_ACTION;
+    }
+
+    // Geolocation consumers use testExactPermission
+    if (aType == "geo")
+      return Services.perms.testExactPermission(aURI, aType);
+
+    return Services.perms.testPermission(aURI, aType);
+  },
+
+  /**
+   * Clears a user-set permission value for the site given a permission type.
+   *
+   * @param aType
+   *        The permission type string stored in permission manager.
+   *        e.g. "cookie", "geo", "indexedDB", "popup", "image"
+   */
+  clearPermission: function clearPermission(aURI, aType) {
+    // Password saving isn't a nsIPermissionManager permission type, so handle
+    // it seperately.
+    if (aType == "password") {
+      // Get rid of exisiting stored logings
+      let logins = Services.logins.findLogins({}, aURI.prePath, "", "");
+      for (let i = 0; i < logins.length; i++) {
+        Services.logins.removeLogin(logins[i]);
+      }
+      // Re-set login saving to enabled
+      Services.logins.setLoginSavingEnabled(aURI.prePath, true);
+    } else {
+      Services.perms.remove(aURI.host, aType);
+    }
+  }
+}
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -117,16 +117,20 @@ notificationRestart.button=Restart
 
 # Popup Blocker
 popupWarning=%S prevented this site from opening a pop-up window.
 popupWarningMultiple=%S prevented this site from opening %S pop-up windows.
 popupButtonAllowOnce=Show
 popupButtonAlwaysAllow2=Always Show
 popupButtonNeverWarn2=Never Show
 
+# LOCALIZATION NOTE (blockPopups.label): Label that will be used in
+# site settings dialog.
+blockPopups.label=Block Popups
+
 # Telemetry
 telemetry.optin.message=Help improve %S by sending anonymous usage information to Mozilla?
 telemetry.optin.yes=Yes
 telemetry.optin.no=No
 
 # XPInstall
 xpinstallPromptWarning2=%S prevented this site (%S) from asking you to install software on your device.
 xpinstallPromptAllowButton=Allow
@@ -142,53 +146,74 @@ identity.identified.title_with_country=%
 identity.encrypted2=Encrypted
 identity.unencrypted2=Not encrypted
 identity.unknown.tooltip=This website does not supply identity information.
 identity.ownerUnknown2=(unknown)
 
 # Geolocation UI
 geolocation.allow=Share
 geolocation.dontAllow=Don't share
+geolocation.alwaysAllow=Always Share
+geolocation.neverAllow=Never Share
 geolocation.wantsTo=%S wants your location.
+# LOCALIZATION NOTE (geolocation.shareLocation): Label that will be used in
+# site settings dialog.
+geolocation.shareLocation=Share Location
 
 # Desktop notification UI
 desktopNotification.allow=Allow
 desktopNotification.dontAllow=Don't allow
 desktopNotification.wantsTo=%S wants to use notifications.
+# LOCALIZATION NOTE (desktopNotification.useNotifications): Label that will be
+# used in site settings dialog.
+desktopNotification.useNotifications=Use Notifications
 
 # New Tab Popup
 # LOCALIZATION NOTE (newtabpopup): Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 number of tabs
 newtabpopup.opened=New tab opened;#1 new tabs opened
 
 # Error Console
 typeError=Error:
 typeWarning=Warning:
 
 # Offline web applications
 offlineApps.available2=%S wants to store data on your device for offline use.
 offlineApps.allow=Allow
 offlineApps.never=Don't Allow
 offlineApps.notNow=Not Now
+# LOCALIZATION NOTE (offlineApps.storeOfflineData): Label that will be used in
+# site settings dialog.
+offlineApps.storeOfflineData=Store Offline Data
 
 # New-style ContentPermissionPrompt values
 offlineApps.dontAllow=Don't Allow
 offlineApps.wantsTo=%S wants to store data on your device for offline use.
 
 # IndexedDB Quota increases
 indexedDBQuota.allow=Allow
 indexedDBQuota.dontAllow=Don't Allow
 indexedDBQuota.wantsTo=%S wants to store a lot of data on your device for offline use.
 
 # Open Web Apps management API
 openWebappsManage.allow=Allow
 openWebappsManage.dontAllow=Don't Allow
 openWebappsManage.wantsTo=%S wants to manage applications on your device.
 
+# LOCALIZATION NOTE (password.rememberPassword): Label that will be used in
+# site settings dialog.
+password.rememberPassword=Remember Password
+# LOCALIZATION NOTE (password.remember): This should match
+# promptRememberButtonText in passwordmgr.properties
+password.remember=Remember
+# LOCALIZATION NOTE (password.never): This should match
+# promptNeverForSiteButtonText in passwordmgr.properties
+password.never=Never
+
 # Bookmark List
 bookmarkList.desktop=Desktop Bookmarks
 
 # Closing Tabs
 tabs.closeWarningTitle=Confirm close
 
 # LOCALIZATION NOTE (tabs.closeWarning): Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
@@ -273,8 +298,14 @@ timer.start=%S: timer started
 # This string is used to display the result of the console.timeEnd() call.
 # %1$S=name of timer, %2$S=number of milliseconds
 timer.end=%1$S: %2$Sms
 
 # Click to play plugins
 clickToPlayFlash.message=This page contains flash content. Would you like to play it?
 clickToPlayFlash.yes=Yes
 clickToPlayFlash.no=No
+
+# Site settings dialog
+# LOCALIZATION NOTE (siteSettings.labelToValue): This string will be used to
+# dislay a list of current permissions settings for a site.
+# Example: "Store Offline Data: Allow"
+siteSettings.labelToValue=%S: %S