Bug 699052 - Android back button should close the selected tab and return to the parent tab when possible [r=mfinkle]
authorMatt Brubeck <mbrubeck@mozilla.com>
Mon, 19 Dec 2011 10:44:52 -0800
changeset 84694 e588135f62ab2e9b1e688982ea284d1489a7b7f7
parent 84693 c62b8bcb94d41415a23bfb775abc623d00436256
child 84695 53d6808502faeab4c0ad65d2850a73913522fdbc
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
bugs699052
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 699052 - Android back button should close the selected tab and return to the parent tab when possible [r=mfinkle]
mobile/android/base/GeckoApp.java
mobile/android/base/LinkPreference.java
mobile/android/base/Tab.java
mobile/android/base/Tabs.java
mobile/android/base/TabsTray.java
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -543,17 +543,17 @@ abstract public class GeckoApp
             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"));
+                loadUrlInNewTab("about:addons");
                 return true;
             case R.id.agent_mode:
                 Tab selectedTab = Tabs.getInstance().getSelectedTab();
                 if (selectedTab == null)
                     return true;
                 JSONObject args = new JSONObject();
                 try {
                     args.put("agent", selectedTab.getAgentMode() == Tab.AgentMode.MOBILE ? "desktop" : "mobile");
@@ -906,22 +906,20 @@ abstract public class GeckoApp
                 final String uri = message.getString("uri");
                 final String title = message.getString("title");
                 handleLoadError(tabId, uri, title);
             } else if (event.equals("onCameraCapture")) {
                 //GeckoApp.mAppContext.doCameraCapture(message.getString("path"));
                 doCameraCapture();
             } else if (event.equals("Tab:Added")) {
                 Log.i(LOGTAG, "Created a new tab");
-                int tabId = message.getInt("tabID");
-                String uri = message.getString("uri");
+                Tab tab = handleAddTab(message);
                 Boolean selected = message.getBoolean("selected");
-                handleAddTab(tabId, uri);
                 if (selected)
-                    handleSelectTab(tabId);
+                    handleSelectTab(tab.getId());
             } else if (event.equals("Tab:Closed")) {
                 Log.i(LOGTAG, "Destroyed a tab");
                 int tabId = message.getInt("tabID");
                 handleCloseTab(tabId);
             } else if (event.equals("Tab:Selected")) {
                 int tabId = message.getInt("tabID");
                 Log.i(LOGTAG, "Switched to tab: " + tabId);
                 handleSelectTab(tabId);
@@ -1132,24 +1130,27 @@ abstract public class GeckoApp
                 if (tab == null)
                     return;
                 tab.removeDoorHanger(value);
                 mDoorHangerPopup.updatePopup();
             }
         });
     }
 
-    void handleAddTab(final int tabId, final String uri) {
-        final Tab tab = Tabs.getInstance().addTab(tabId, uri);
+    Tab handleAddTab(JSONObject params) throws JSONException {
+        Log.i(LOGTAG, params.toString());
+        final Tab tab = Tabs.getInstance().addTab(params);
 
-        mMainHandler.post(new Runnable() { 
+        mMainHandler.post(new Runnable() {
             public void run() {
                 mBrowserToolbar.updateTabs(Tabs.getInstance().getCount());
             }
         });
+
+        return tab;
     }
 
     void handleCloseTab(final int tabId) {
         final Tab tab = Tabs.getInstance().getTab(tabId);
         Tabs.getInstance().removeTab(tabId);
         tab.removeAllDoorHangers();
 
         mMainHandler.post(new Runnable() { 
@@ -2003,20 +2004,41 @@ abstract public class GeckoApp
             return;
         }
 
         if (mFullScreen) {
             GeckoAppShell.sendEventToGecko(new GeckoEvent("FullScreen:Exit", null));
             return;
         }
 
-        Tab tab = Tabs.getInstance().getSelectedTab();
-        if (tab == null || !tab.doBack()) {
+        Tabs tabs = Tabs.getInstance();
+        Tab tab = tabs.getSelectedTab();
+        if (tab == null) {
+            moveTaskToBack(true);
+            return;
+        }
+
+        if (tab.doBack())
+            return;
+
+        if (tab.isExternal()) {
             moveTaskToBack(true);
+            tabs.closeTab(tab);
+            return;
         }
+
+        int parentId = tab.getParentId();
+        Tab parent = tabs.getTab(parentId);
+        if (parent != null) {
+            // The back button should always return to the parent (not a sibling).
+            tabs.closeTab(tab, parent);
+            return;
+        }
+
+        moveTaskToBack(true);
     }
 
     static int kCaptureIndex = 0;
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode,
                                     Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
@@ -2123,16 +2145,32 @@ abstract public class GeckoApp
             GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Load", args.toString()));
         }
     }
 
     public void loadUrl(String url, AwesomeBar.Type type) {
         loadRequest(url, type, null);
     }
 
+    /**
+     * Open the link as a new tab, and mark the selected tab as its "parent".
+     * Use this for tabs opened by the browser chrome, so users can press the
+     * "Back" button to return to the previous tab.
+     */
+    public void loadUrlInNewTab(String url) {
+        JSONObject args = new JSONObject();
+        try {
+            args.put("url", url);
+            args.put("parentId", Tabs.getInstance().getSelectedTabId());
+        } catch (Exception e) {
+            Log.e(LOGTAG, "error building JSON arguments");
+        }
+        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Add", args.toString()));
+    }
+
     public GeckoSoftwareLayerClient getSoftwareLayerClient() { return mSoftwareLayerClient; }
     public LayerController getLayerController() { return mLayerController; }
 
     // accelerometer
     public void onAccuracyChanged(Sensor sensor, int accuracy)
     {
     }
 
--- a/mobile/android/base/LinkPreference.java
+++ b/mobile/android/base/LinkPreference.java
@@ -50,12 +50,12 @@ class LinkPreference extends Preference 
     }
     public LinkPreference(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mUrl = attrs.getAttributeValue(null, "url");
     }
 
     @Override
     protected void onClick() {
-        GeckoApp.mAppContext.loadUrl(mUrl, AwesomeBar.Type.ADD);
+        GeckoApp.mAppContext.loadUrlInNewTab(mUrl);
         callChangeListener(mUrl);
     }
 }
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -65,16 +65,18 @@ public class Tab {
     private String mUrl;
     private String mTitle;
     private Drawable mFavicon;
     private String mFaviconUrl;
     private String mSecurityMode;
     private Drawable mThumbnail;
     private List<HistoryEntry> mHistory;
     private int mHistoryIndex;
+    private int mParentId;
+    private boolean mExternal;
     private boolean mLoading;
     private boolean mBookmark;
     private HashMap<String, DoorHanger> mDoorHangers;
     private long mFaviconLoadId;
     private AgentMode mAgentMode = AgentMode.MOBILE;
     private String mDocumentURI;
     private String mContentType;
 
@@ -84,22 +86,24 @@ public class Tab {
 
         public HistoryEntry(String uri, String title) {
             mUri = uri;
             mTitle = title;
         }
     }
 
     public Tab() {
-        this(-1, "");
+        this(-1, "", false, -1);
     }
 
-    public Tab(int id, String url) {
+    public Tab(int id, String url, boolean external, int parentId) {
         mId = id;
         mUrl = url;
+        mExternal = external;
+        mParentId = parentId;
         mTitle = "";
         mFavicon = null;
         mFaviconUrl = null;
         mSecurityMode = "unknown";
         mThumbnail = null;
         mHistory = new ArrayList<HistoryEntry>();
         mHistoryIndex = -1;
         mBookmark = false;
@@ -108,16 +112,20 @@ public class Tab {
         mDocumentURI = "";
         mContentType = "";
     }
 
     public int getId() {
         return mId;
     }
 
+    public int getParentId() {
+        return mParentId;
+    }
+
     public String getURL() {
         return mUrl;
     }
 
     public String getTitle() {
         return mTitle;
     }
 
@@ -173,16 +181,20 @@ public class Tab {
     public boolean isLoading() {
         return mLoading;
     }
 
     public boolean isBookmark() {
         return mBookmark;
     }
 
+    public boolean isExternal() {
+        return mExternal;
+    }
+
     public void updateURL(String url) {
         if (url != null && url.length() > 0) {
             mUrl = url;
             Log.i(LOGTAG, "Updated url: " + url + " for tab with id: " + mId);
             updateBookmark();
         }
     }
 
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -39,16 +39,17 @@ package org.mozilla.gecko;
 
 import java.util.*;
 
 import android.content.ContentResolver;
 import android.graphics.drawable.*;
 import android.util.Log;
 
 import org.json.JSONObject;
+import org.json.JSONException;
 
 public class Tabs implements GeckoEventListener {
     private static final String LOGTAG = "GeckoTabs";
 
     private static int selectedTab = -1;
     private HashMap<Integer, Tab> tabs;
     private ArrayList<Tab> order;
     private ContentResolver resolver;
@@ -62,21 +63,26 @@ public class Tabs implements GeckoEventL
         GeckoAppShell.registerGeckoEventListener("SessionHistory:Goto", this);
         GeckoAppShell.registerGeckoEventListener("SessionHistory:Purge", this);
     }
 
     public int getCount() {
         return tabs.size();
     }
 
-    public Tab addTab(int id, String url) {
+    public Tab addTab(JSONObject params) throws JSONException {
+        int id = params.getInt("tabID");
         if (tabs.containsKey(id))
            return tabs.get(id);
 
-        Tab tab = new Tab(id, url);
+        String url = params.getString("uri");
+        Boolean external = params.getBoolean("external");
+        int parentId = params.getInt("parentId");
+
+        Tab tab = new Tab(id, url, external, parentId);
         tabs.put(id, tab);
         order.add(tab);
         Log.i(LOGTAG, "Added a tab with id: " + id + ", url: " + url);
         return tab;
     }
 
     public void removeTab(int id) {
         if (tabs.containsKey(id)) {
@@ -122,16 +128,52 @@ public class Tabs implements GeckoEventL
             return null;
 
         if (!tabs.containsKey(id))
            return null;
 
         return tabs.get(id);
     }
 
+    /** Close tab and then select the default next tab */
+    public void closeTab(Tab tab) {
+        closeTab(tab, getNextTab(tab));
+    }
+
+    /** Close tab and then select nextTab */
+    public void closeTab(Tab tab, Tab nextTab) {
+        if (tab == null)
+            return;
+
+        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Select", String.valueOf(nextTab.getId())));
+        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Close", String.valueOf(tab.getId())));
+    }
+
+    /** Return the tab that will be selected by default after this one is closed */
+    public Tab getNextTab(Tab tab) {
+        Tab selectedTab = getSelectedTab();
+        if (selectedTab != tab)
+            return selectedTab;
+
+        int index = getIndexOf(tab);
+        Tab nextTab = getTabAt(index + 1);
+        if (nextTab == null)
+            nextTab = getTabAt(index - 1);
+
+        Tab parent = getTab(tab.getParentId());
+        if (parent != null) {
+            // If the next tab is a sibling, switch to it. Otherwise go back to the parent.
+            if (nextTab != null && nextTab.getParentId() == tab.getParentId())
+                return nextTab;
+            else
+                return parent;
+        }
+        return nextTab;
+    }
+
     public HashMap<Integer, Tab> getTabs() {
         if (getCount() == 0)
             return null;
 
         return tabs;
     }
     
     public ArrayList<Tab> getTabsInOrder() {
--- a/mobile/android/base/TabsTray.java
+++ b/mobile/android/base/TabsTray.java
@@ -169,39 +169,23 @@ public class TabsTray extends Activity i
                     finishActivity();
                 }
             };
 
             mOnCloseClickListener = new Button.OnClickListener() {
                 public void onClick(View v) {
                     if (mWaitingForClose)
                         return;
-                
+
                     mWaitingForClose = true;
-               
+
                     String tabId = v.getTag().toString();
                     Tabs tabs = Tabs.getInstance();
                     Tab tab = tabs.getTab(Integer.parseInt(tabId));
-
-                    if (tab == null)
-                        return;
-                
-                    if (tabs.isSelectedTab(tab)) {
-                        int index = tabs.getIndexOf(tab);
-                        if (index >= 1)
-                            index--;
-                        else
-                            index = 1;
-                        int id = tabs.getTabAt(index).getId();
-                        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Select", String.valueOf(id)));
-                        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Close", tabId));
-                    } else {
-                        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Close", tabId));
-                        GeckoAppShell.sendEventToGecko(new GeckoEvent("Tab:Select", String.valueOf(tabs.getSelectedTabId())));
-                    }
+                    tabs.closeTab(tab);
                 }
             };
         }
 
         @Override    
         public int getCount() {
             return mTabs.size();
         }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -577,32 +577,31 @@ var BrowserApp = {
     sendMessageToJava({
       gecko: {
         type: "SearchEngines:Data",
         searchEngines: searchEngines
       }
     });
   },
 
-  getSearchOrFixupURI: function(aData) {
-    let args = JSON.parse(aData);
+  getSearchOrFixupURI: function(aParams) {
     let uri;
-    if (args.engine) {
+    if (aParams.engine) {
       let engine;
-      if (args.engine == "__default__")
+      if (aParams.engine == "__default__")
         engine = Services.search.currentEngine || Services.search.defaultEngine;
       else
-        engine = Services.search.getEngineByName(args.engine);
+        engine = Services.search.getEngineByName(aParams.engine);
 
       if (engine)
-        uri = engine.getSubmission(args.url).uri;
+        uri = engine.getSubmission(aParams.url).uri;
     } else {
-      uri = URIFixup.createFixupURI(args.url, Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
+      uri = URIFixup.createFixupURI(aParams.url, Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
     }
-    return uri ? uri.spec : args.url;
+    return uri ? uri.spec : aParams.url;
   },
 
   scrollToFocusedInput: function(aBrowser) {
     let doc = aBrowser.contentDocument;
     if (!doc)
       return;
     let focused = doc.activeElement;
     if ((focused instanceof HTMLInputElement && focused.mozIsTextField(false)) || (focused instanceof HTMLTextAreaElement)) {
@@ -624,24 +623,27 @@ var BrowserApp = {
       browser.goBack();
     } else if (aTopic == "Session:Forward") {
       browser.goForward();
     } else if (aTopic == "Session:Reload") {
       browser.reload();
     } else if (aTopic == "Session:Stop") {
       browser.stop();
     } else if (aTopic == "Tab:Add" || aTopic == "Tab:Load") {
+      let data = JSON.parse(aData);
+
       // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
       // inheriting the currently loaded document's principal.
       let params = {
         selected: true,
+        parentId: ("parentId" in data) ? data.parentId : -1,
         flags: Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER
       };
 
-      let url = this.getSearchOrFixupURI(aData);
+      let url = this.getSearchOrFixupURI(data);
       if (aTopic == "Tab:Add")
         this.addTab(url, params);
       else
         this.loadURI(url, browser, params);
     } else if (aTopic == "Tab:Select") {
       this.selectTab(this.getTabForId(parseInt(aData)));
     } else if (aTopic == "Tab:Close") {
       this.closeTab(this.getTabForId(parseInt(aData)));
@@ -802,17 +804,17 @@ var NativeWindow = {
 
       Services.obs.addObserver(this, "Gesture:LongPress", false);
 
       // TODO: These should eventually move into more appropriate classes
       this.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
                this.linkContext,
                function(aTarget) {
                  let url = NativeWindow.contextmenus._getLinkURL(aTarget);
-                 BrowserApp.addTab(url, { selected: false });
+                 BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
                });
 
       this.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"),
                this.SelectorContext("video:not(:-moz-full-screen)"),
                function(aTarget) {
                  aTarget.mozRequestFullScreen();
                });
 
@@ -1015,19 +1017,28 @@ nsBrowserAccess.prototype = {
         case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL:
           aWhere = Services.prefs.getIntPref("browser.link.open_external");
           break;
         default: // OPEN_NEW or an illegal value
           aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
       }
     }
 
+    let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW || aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB);
+
+    let parentId = -1;
+    if (newTab && !isExternal) {
+      let parent = BrowserApp.getTabForBrowser(BrowserApp.getBrowserForWindow(aOpener));
+      if (parent)
+        parentId = parent.id;
+    }
+
     let browser;
-    if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW || aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) {
-      let tab = BrowserApp.addTab("about:blank", { selected: true });
+    if (newTab) {
+      let tab = BrowserApp.addTab("about:blank", { external: isExternal, parentId: parentId, selected: true });
       browser = tab.browser;
     } else { // OPEN_CURRENTWINDOW and illegal values
       browser = BrowserApp.selectedBrowser;
     }
 
     Services.io.offline = false;
     try {
       let referrer;
@@ -1109,16 +1120,18 @@ Tab.prototype = {
 
     this.id = ++gTabIDFactory;
 
     let message = {
       gecko: {
         type: "Tab:Added",
         tabID: this.id,
         uri: aURL,
+        parentId: ("parentId" in aParams) ? aParams.parentId : -1,
+        external: ("external" in aParams) ? aParams.external : false,
         selected: ("selected" in aParams) ? aParams.selected : true
       }
     };
     sendMessageToJava(message);
 
     let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
                 Ci.nsIWebProgress.NOTIFY_LOCATION |
                 Ci.nsIWebProgress.NOTIFY_SECURITY;