[WIP] Bug 1232439 (972193) - Create base classes for use in bookmark edit dialogs. draft
authorTom Klein <twointofive@gmail.com>
Sun, 25 Dec 2016 19:00:56 -0600
changeset 453959 7f4641fd4743840368d56320e99739d5dd066591
parent 453954 2a7c251816cd42ae8eb877649fc9ac5532b2c6f3
child 453960 3fecda8a0f25d06e05d4d74032259d815867a6fb
push id39777
push userbmo:twointofive@gmail.com
push dateMon, 26 Dec 2016 21:30:36 +0000
bugs1232439, 972193
milestone53.0a1
[WIP] Bug 1232439 (972193) - Create base classes for use in bookmark edit dialogs. BookmarkToolbarDialog creates just the toolbar; its subclass BookmarkFolderTreeDialog additionally includes a FolderTreeView for displaying a list of folders. MozReview-Commit-ID: HxYcd9AB9OY
mobile/android/base/java/org/mozilla/gecko/BookmarkFolderTreeDialog.java
mobile/android/base/java/org/mozilla/gecko/BookmarkToolbarDialog.java
mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/resources/layout/bookmark_dialog_toolbar.xml
mobile/android/base/resources/layout/bookmark_edit.xml
mobile/android/base/resources/menu/bookmarks_dialog_menu_save.xml
mobile/android/base/resources/menu/bookmarks_dialog_menu_save_and_remove.xml
mobile/android/base/resources/menu/edit_bookmarks_menu.xml
mobile/android/base/strings.xml.in
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/BookmarkFolderTreeDialog.java
@@ -0,0 +1,44 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+import android.view.View;
+
+public abstract class BookmarkFolderTreeDialog extends BookmarkToolbarDialog {
+    protected abstract int currentFolderId();
+
+    protected void setupFolderTree(View parentView, boolean nestedScroll) {
+        final FolderTreeView folderTreeView = (FolderTreeView) parentView.findViewById(R.id.folder_chooser);
+
+        if (nestedScroll) {
+            // If we're nested inside another NestedScrollView then we need to set the following to
+            // get scrolling flings to work.
+            folderTreeView.setNestedScrollingEnabled(false);
+        }
+
+        final BrowserDB db = BrowserDB.from(getContext());
+        (new UIAsyncTask.WithoutParams<BrowserDB.BookmarkFolderTree>(ThreadUtils.getBackgroundHandler()) {
+            @Override
+            public BrowserDB.BookmarkFolderTree doInBackground() {
+                return db.getBookmarkFolders(getContext().getContentResolver());
+            }
+
+            @Override
+            public void onPostExecute(BrowserDB.BookmarkFolderTree folderTree) {
+                if (folderTree == null) {
+                    return;
+                }
+
+                folderTreeView.setFolderTree(folderTree, currentFolderId());
+                folderTreeView.setFolderSelectedListener(BookmarkFolderTreeDialog.this);
+            }
+        }).execute();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/BookmarkToolbarDialog.java
@@ -0,0 +1,125 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.db.BrowserContract;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.support.annotation.MenuRes;
+import android.support.annotation.StringRes;
+import android.support.design.widget.TextInputLayout;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.MenuItem;
+import android.view.View;
+
+public abstract class BookmarkToolbarDialog extends DialogFragment
+                                            implements FolderTreeView.FolderSelectedListener  {
+    protected static final String ARG_FOLDER_ID = "folder_id";
+    protected static final String ARG_FOLDER_NAME = "folder_name";
+
+    // The fake Dekstop folder isn't selectable, so use its ID as our invalid folder ID value (the
+    // desktop folder doesn't actually exist in the database, so attempting to use it in the
+    // database as a parent folder ID will crash on a failed foreign key constraint rather than
+    // leading to invalid parent data).
+    protected static int INVALID_FOLDER_ID = BrowserContract.Bookmarks.FAKE_DESKTOP_FOLDER_ID;
+
+    protected abstract @StringRes int toolbarTitle();
+
+    protected void setupToolbar(View parentView, @MenuRes Integer menu) {
+        final Toolbar toolbar = (Toolbar) parentView.findViewById(R.id.bookmarks_toolbar);
+        toolbar.setTitle(toolbarTitle());
+
+        toolbar.setNavigationIcon(R.drawable.tabs_panel_nav_back);
+        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                // The back button cancels and returns.
+                getFragmentManager().popBackStack();
+            }
+        });
+
+        setupToolbarMenu(toolbar, menu);
+    }
+
+    private void setupToolbarMenu(Toolbar toolbar, @MenuRes Integer menu) {
+        if (menu == null) {
+            return;
+        }
+
+        toolbar.inflateMenu(menu);
+        toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                final int id = item.getItemId();
+                return toolbarMenuItemClicked(id);
+            }
+        });
+    }
+
+    /*
+     * @return true to consume the click and prevent others from executing.
+     */
+    protected boolean toolbarMenuItemClicked(int menuItemId) {
+        return false;
+    }
+
+    /**
+     *  Pops the fragment back stack.
+     */
+    @Override
+    public void folderSelected(String folderName, int folderId) {
+        final Fragment targetFragment = getTargetFragment();
+        if (targetFragment == null) {
+            getFragmentManager().popBackStack();
+            return;
+        }
+
+        // Send on the selected folder data to this fragment's target fragment.
+        final Intent intent = new Intent();
+        intent.putExtra(ARG_FOLDER_NAME, folderName);
+        intent.putExtra(ARG_FOLDER_ID, folderId);
+        targetFragment.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, intent);
+        getFragmentManager().popBackStack();
+    }
+
+    protected static class EmptyTextWatcher implements TextWatcher {
+        private final TextInputLayout textInputLayout;
+        private final String errorText;
+
+        EmptyTextWatcher(TextInputLayout textInputLayout, String errorText) {
+            this.textInputLayout = textInputLayout;
+            this.errorText = errorText;
+        }
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            final boolean fieldIsEmpty = (s.toString().trim().length() > 0);
+
+            if (fieldIsEmpty) {
+                // This completely removes the layout space otherwise taken up by the error message.
+                textInputLayout.setErrorEnabled(false);
+                // setErrorEnabled destroys or creates the error view, but doesn't set the error
+                // view's text; setError sets the error view's text (creating and displaying the
+                // view first if needed), but only if you're not setting the same error text you set
+                // last time.  So set the error text to null here so that the next time we set the
+                // actual error text it will update (i.e. recreate) the error view's text.
+                textInputLayout.setError(null);
+            } else {
+                textInputLayout.setError(errorText);
+            }
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {}
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
+++ b/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
@@ -10,39 +10,39 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v4.app.DialogFragment;
+import android.support.annotation.StringRes;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentTransaction;
-import android.support.v7.widget.Toolbar;
 import android.support.design.widget.Snackbar;
 import android.support.design.widget.TextInputLayout;
-import android.text.Editable;
-import android.view.MenuItem;
-import android.text.TextWatcher;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.EditText;
 
 /**
- * A dialog that allows editing a bookmarks url, title, or keywords
+ * A dialog that allows editing a bookmark's url, title, folder, or keywords.
  * <p>
  * Invoked by calling one of the {@link org.mozilla.gecko.EditBookmarkDialog#show}
  * methods.
  */
-public class EditBookmarkDialog extends DialogFragment {
+public class EditBookmarkDialog extends BookmarkToolbarDialog {
     private Bookmark mBookmark;
 
+    private ViewGroup rootView;
+    private EditText nameText;
+    private EditText locationText;
+    private EditText keywordText;
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
         mBookmark.saveToBundle(outState);
     }
 
     @Override
@@ -56,153 +56,146 @@ public class EditBookmarkDialog extends 
 
     /**
      * A private struct to make it easier to pass bookmark data across threads
      */
     private static class Bookmark {
         private static final String SAVED_STATE_ID = "id";
         private static final String SAVED_STATE_TITLE = "title";
         private static final String SAVED_STATE_URL = "url";
+        private static final String SAVED_STATE_FOLDER_ID = "folder_id";
+        private static final String SAVED_STATE_FOLDER_NAME = "folder_name";
         private static final String SAVED_STATE_KEYWORD = "keyword";
 
         final int id;
         final String title;
         final String url;
+        int folderId;
+        String folderName;
         final String keyword;
 
-        public Bookmark(int aId, String aTitle, String aUrl, String aKeyword) {
+        public Bookmark(int aId, String aTitle, String aUrl, int aFolderId, String aFolderName, String aKeyword) {
             id = aId;
             title = aTitle;
             url = aUrl;
+            folderId = aFolderId;
+            folderName = aFolderName;
             keyword = aKeyword;
         }
 
         static Bookmark newFromBundle(Bundle bundle) {
             return new Bookmark(bundle.getInt(SAVED_STATE_ID),
                                 bundle.getString(SAVED_STATE_TITLE),
                                 bundle.getString(SAVED_STATE_URL),
+                                bundle.getInt(SAVED_STATE_FOLDER_ID),
+                                bundle.getString(SAVED_STATE_FOLDER_NAME),
                                 bundle.getString(SAVED_STATE_KEYWORD));
         }
 
         void saveToBundle(Bundle bundle) {
             bundle.putInt(SAVED_STATE_ID, id);
             bundle.putString(SAVED_STATE_TITLE, title);
             bundle.putString(SAVED_STATE_URL, url);
             bundle.putString(SAVED_STATE_KEYWORD, keyword);
         }
     }
 
-    private class BookmarkUriWatcher implements TextWatcher {
-        private TextInputLayout textInputLayout;
-
-        BookmarkUriWatcher(TextInputLayout textInputLayout) {
-            this.textInputLayout = textInputLayout;
+    private void toolbarSaveClicked() {
+        if (locationText.getText().toString().trim().isEmpty()) {
+            // locationText is already showing an error message when it is empty - we give it focus
+            // to make it obvious to the user that they can't save a bookmark without a URL.
+            locationText.requestFocus();
+            return;
         }
 
-        @Override
-        public void onTextChanged(CharSequence s, int start, int before, int count) {
-            final boolean fieldIsEmpty = (s.toString().trim().length() > 0);
+        (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+            @Override
+            public Void doInBackground() {
+                final String newName = nameText.getText().toString().trim();
+                final String newUrl = locationText.getText().toString().trim();
+                final String newKeyword = keywordText.getText().toString().trim();
+                final BrowserDB db = BrowserDB.from(getContext());
+                db.updateBookmark(getContext().getContentResolver(),
+                        mBookmark.id, newUrl, newName, newKeyword, mBookmark.folderId);
+                return null;
+            }
+
+            @Override
+            public void onPostExecute(Void result) {
+                Snackbar.make(rootView, R.string.bookmark_updated, Snackbar.LENGTH_SHORT).show();
+                getFragmentManager().popBackStack();
+            }
+        }).execute();
+    }
 
-            if (fieldIsEmpty) {
-                // This completely removes the layout space otherwise taken up by the error message.
-                textInputLayout.setErrorEnabled(false);
-                // setErrorEnabled destroys or creates the error view, but doesn't set the error
-                // view's text; setError sets the error view's text (creating and displaying the
-                // view first if needed), but only if you're not setting the same error text you set
-                // last time.  So set the error text to null here so that the next time we set the
-                // actual error text it will update (i.e. recreate) the error view's text.
-                textInputLayout.setError(null);
-            } else {
-                textInputLayout.setError(getString(R.string.bookmark_edit_location_empty_error));
+    private void toolbarRemoveClicked() {
+        final BrowserDB db = BrowserDB.from(getContext());
+        (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+            @Override
+            public Void doInBackground() {
+                db.removeBookmarkWithID(getContext().getContentResolver(), mBookmark.id);
+                return null;
             }
+
+            @Override
+            public void onPostExecute(Void result) {
+                Snackbar.make(rootView, R.string.bookmark_removed, Snackbar.LENGTH_SHORT).show();
+                getFragmentManager().popBackStack();
+            }
+        }).execute();
+    }
+
+    @Override
+    protected boolean toolbarMenuItemClicked(int menuItemId) {
+        if (menuItemId == R.id.save) {
+            toolbarSaveClicked();
+            return true;
+        } else if (menuItemId == R.id.bin) {
+            toolbarRemoveClicked();
+            return true;
         }
 
-        @Override
-        public void afterTextChanged(Editable s) {}
-        @Override
-        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+        return false;
+    }
+
+    @Override
+    protected @StringRes int toolbarTitle() {
+        return R.string.bookmark_edit_title;
     }
 
     @Nullable
     @Override
     public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) {
         final View editView = inflater.inflate(R.layout.bookmark_edit, container, false);
 
-        final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
-        final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
-        final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
+        rootView = container;
+        nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
+        locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
+        keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
+        final EditText folderText = ((EditText) editView.findViewById(R.id.edit_bookmark_folder));
 
-        final Toolbar toolbar = (Toolbar) editView.findViewById(R.id.toolbar);
-        toolbar.setTitle(R.string.bookmark_edit_title);
-        toolbar.setNavigationIcon(R.drawable.tabs_panel_nav_back);
-        toolbar.inflateMenu(R.menu.edit_bookmarks_menu);
+        setupToolbar(editView, R.menu.bookmarks_dialog_menu_save_and_remove);
 
         // Insert text BEFORE the cursor - when the user opens the dialog we want the cursor to be at the end of the name
         // field. We've already requested focus in the layout file using <requestFocus/>, however using setText
         // results in the cursor staying at the start of the text, whereas insert(0,...) results in the text being
         // inserted in front of the cursor.
         nameText.getText().insert(0, mBookmark.title);
         locationText.setText(mBookmark.url);
         keywordText.setText(mBookmark.keyword);
 
-        final BrowserDB db = BrowserDB.from(getContext());
-        toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
-            @Override
-            public boolean onMenuItemClick(MenuItem item) {
-                int id = item.getItemId();
-                if (id == R.id.bin) {
-                    (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
-                        @Override
-                        public Void doInBackground() {
-                            db.removeBookmarkWithID(getContext().getContentResolver(), mBookmark.id);
-                            return null;
-                        }
-
-                        @Override
-                        public void onPostExecute(Void result) {
-                            Snackbar.make(container, R.string.bookmark_removed, Snackbar.LENGTH_SHORT).show();
-                            getFragmentManager().popBackStack();
-                        }
-                    }).execute();
-                }
-                return false;
+        folderText.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                // TODO: launch new folder chooser dialog.
             }
         });
 
-        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                if (locationText.getText().toString().trim().isEmpty()) {
-                    // locationText is already showing an error message when it is empty - we give it focus
-                    // to make it obvious to the user that they can't save a bookmark without a URL.
-                    locationText.requestFocus();
-                    return;
-                }
-
-                (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
-                    @Override
-                    public Void doInBackground() {
-                        String newUrl = locationText.getText().toString().trim();
-                        String newKeyword = keywordText.getText().toString().trim();
-
-                        db.updateBookmark(getContext().getContentResolver(), mBookmark.id, newUrl, nameText.getText().toString().trim(), newKeyword);
-                        return null;
-                    }
-
-                    @Override
-                    public void onPostExecute(Void result) {
-                        Snackbar.make(container, R.string.bookmark_updated, Snackbar.LENGTH_SHORT).show();
-                        getFragmentManager().popBackStack();
-                    }
-                }).execute();
-            }
-        });
-
-        BookmarkUriWatcher locationTextWatcher = new BookmarkUriWatcher((TextInputLayout) locationText.getParent());
-        locationText.addTextChangedListener(locationTextWatcher);
+        final TextInputLayout locationTextLayout = (TextInputLayout) editView.findViewById(R.id.edit_bookmark_location_layout);
+        final String emptyURLErrorText = getString(R.string.bookmark_edit_location_empty_error);
+        locationText.addTextChangedListener(new EmptyTextWatcher(locationTextLayout, emptyURLErrorText));
 
         return editView;
     }
 
     /**
      * Show the Edit bookmark dialog for a particular url. If the url is bookmarked multiple times
      * this will just edit the first instance it finds.
      *
@@ -221,19 +214,23 @@ public class EditBookmarkDialog extends 
                 final Cursor cursor = db.getBookmarkForUrl(cr, url);
                 if (cursor == null) {
                     return null;
                 }
 
                 Bookmark bookmark = null;
                 try {
                     cursor.moveToFirst();
+                    final int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.PARENT));
+                    final String folderName = db.getBookmarkFolderName(cr, folderId);
                     bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)),
                                                           cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)),
                                                           cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)),
+                                                          folderId,
+                                                          folderName == null ? "" : folderName,
                                                           cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.KEYWORD)));
                 } finally {
                     cursor.close();
                 }
                 return bookmark;
             }
 
             @Override
@@ -245,13 +242,13 @@ public class EditBookmarkDialog extends 
                 show(fm, bookmark);
             }
         }).execute();
     }
 
     private void show(final FragmentManager fm, final Bookmark bookmark) {
         this.mBookmark = bookmark;
 
-        FragmentTransaction transaction = fm.beginTransaction();
+        final FragmentTransaction transaction = fm.beginTransaction();
         transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
         transaction.add(android.R.id.content, this).addToBackStack(null).commitAllowingStateLoss();
     }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -70,26 +70,38 @@
 <!ENTITY bookmark_added "Bookmark added">
 <!-- Localization note (bookmark_already_added) : This string is
      used as a label in a toast. It is the verb "to bookmark", not
      the noun "a bookmark". -->
 <!ENTITY bookmark_already_added "Already bookmarked">
 <!ENTITY bookmark_removed "Bookmark removed">
 <!ENTITY bookmark_updated "Bookmark updated">
 <!ENTITY bookmark_options "Options">
-<!-- Localization note (selected_folder): Indicates which Bookmarks folder is currently selected;
-     selected is an adjective and not a verb here. -->
+<!-- Localization note (selected_folder): Alternate text for accessibility; not UI visible.
+     Indicates which Bookmarks folder is currently selected (selected is an adjective and not a verb
+     here). -->
 <!ENTITY bookmarks_selected_folder "Selected folder">
 <!ENTITY screenshot_added_to_bookmarks "Screenshot added to bookmarks">
 <!-- Localization note (screenshot_folder_label_in_bookmarks): We save links to screenshots
      the user takes. The folder we store these links in is located in the bookmarks list
      and is labeled by this String. -->
 <!ENTITY screenshot_folder_label_in_bookmarks "Screenshots">
 <!ENTITY readinglist_smartfolder_label_in_bookmarks "Reading List">
 
+<!-- Localization note (bookmark_dialog_save_and_return): Alternate text for accessibility; not UI
+     visible.  Save the data input in the current dialog and return to the previous dialog. -->
+<!ENTITY bookmark_dialog_save_and_return "Save and return">
+<!ENTITY bookmark_dialog_folder "Folder">
+
+<!ENTITY bookmark_edit_title "Edit Bookmark">
+<!ENTITY bookmark_edit_name "Name">
+<!ENTITY bookmark_edit_location "Location">
+<!ENTITY bookmark_edit_keyword "Keyword">
+<!ENTITY bookmark_edit_location_empty_error "Bookmark location must contain a URL">
+
 <!-- Localization note (bookmark_folder_items): The variable is replaced by the number of items
      in the folder. -->
 <!ENTITY bookmark_folder_items "&formatD; items">
 <!ENTITY bookmark_folder_one_item "1 item">
 
 <!ENTITY reader_saved_offline "Saved offline">
 <!-- Localization note (reader_switch_to_bookmarks) : This
      string is used as an action in a snackbar - it lets you
@@ -545,22 +557,16 @@ size. -->
 <!ENTITY pref_compact_tabs "Compact tabs">
 <!ENTITY pref_compact_tabs_summary2 "Arrange tabs in two columns in portrait mode">
 
 <!-- Localization note (page_removed): This string appears in a toast message when
      any page is removed frome about:home. This includes pages that are in history,
      bookmarks, or reading list. -->
 <!ENTITY page_removed "Page removed">
 
-<!ENTITY bookmark_edit_title "Edit Bookmark">
-<!ENTITY bookmark_edit_name "Name">
-<!ENTITY bookmark_edit_location "Location">
-<!ENTITY bookmark_edit_keyword "Keyword">
-<!ENTITY bookmark_edit_location_empty_error "Bookmark location must contain a URL">
-
 <!-- Localization note (site_settings_*) : These strings are used in the "Site Settings"
      dialog that appears after selecting the "Edit Site Settings" context menu item. -->
 <!ENTITY site_settings_title3       "Site Settings">
 <!ENTITY site_settings_cancel       "Cancel">
 <!ENTITY site_settings_clear        "Clear">
 
 <!-- Localization note : These strings are used as alternate text for accessibility.
      They are not visible in the UI. -->
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -337,16 +337,18 @@ gbjar.sources += ['java/org/mozilla/geck
     'activitystream/ActivityStream.java',
     'adjust/AdjustBrowserAppDelegate.java',
     'animation/AnimationUtils.java',
     'animation/HeightChangeAnimation.java',
     'animation/PropertyAnimator.java',
     'animation/Rotate3DAnimation.java',
     'animation/ViewHelper.java',
     'ANRReporter.java',
+    'BookmarkFolderTreeDialog.java',
+    'BookmarkToolbarDialog.java',
     'BootReceiver.java',
     'BrowserApp.java',
     'BrowserLocaleManager.java',
     'cleanup/FileCleanupController.java',
     'cleanup/FileCleanupService.java',
     'CustomEditText.java',
     'customtabs/CustomTabsActivity.java',
     'customtabs/GeckoCustomTabsService.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_dialog_toolbar.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/bookmarks_toolbar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_alignParentTop="true"
+    android:background="?attr/colorPrimary"
+    android:minHeight="?attr/actionBarSize"
+    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
--- a/mobile/android/base/resources/layout/bookmark_edit.xml
+++ b/mobile/android/base/resources/layout/bookmark_edit.xml
@@ -1,30 +1,22 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               xmlns:app="http://schemas.android.com/apk/res-auto"
               android:layout_width="match_parent"
               android:orientation="vertical"
               android:layout_height="match_parent"
               android:background="@android:color/white"
               android:theme="@style/GeckoAlertDialog">
 
-    <android.support.v7.widget.Toolbar
-            android:id="@+id/toolbar"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="?attr/colorPrimary"
-            android:minHeight="?attr/actionBarSize"
-            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
-            android:title="@string/bookmark_edit_title"/>
+    <include layout="@layout/bookmark_dialog_toolbar"/>
 
     <ScrollView
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:id="@+id/scrollview"
             android:layout_gravity="center_horizontal">
 
         <LinearLayout android:layout_width="match_parent"
@@ -45,30 +37,49 @@
                         android:hint="@string/bookmark_edit_name">
                     <requestFocus/>
                 </android.support.design.widget.TextInputEditText>
             </android.support.design.widget.TextInputLayout>
 
             <android.support.design.widget.TextInputLayout
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    android:id="@+id/edit_bookmark_location_layout"
                     app:hintAnimationEnabled="true">
 
                 <android.support.design.widget.TextInputEditText
                         android:id="@+id/edit_bookmark_location"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:maxLines="1"
                         android:hint="@string/bookmark_edit_location"
                         android:inputType="textNoSuggestions"/>
             </android.support.design.widget.TextInputLayout>
 
             <android.support.design.widget.TextInputLayout
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    app:hintAnimationEnabled="false">
+
+                <!-- Use focusableInTouchMode="false" so that the text edit receives a click event
+                     the first time it's clicked - otherwise the first tap only focuses the field
+                     and you don't get a click event until the second tap. -->
+                <android.support.design.widget.TextInputEditText
+                        android:id="@+id/edit_bookmark_folder"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:hint="@string/bookmark_dialog_folder"
+                        android:focusableInTouchMode="false"
+                        android:maxLines="1"
+                        android:editable="false"/>
+            </android.support.design.widget.TextInputLayout>
+
+            <android.support.design.widget.TextInputLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
                     app:hintAnimationEnabled="true">
 
                 <android.support.design.widget.TextInputEditText
                         android:id="@+id/edit_bookmark_keyword"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:maxLines="1"
                         android:hint="@string/bookmark_edit_keyword"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu/bookmarks_dialog_menu_save.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item android:id="@+id/save"
+          android:icon="@drawable/img_check"
+          android:title="@string/bookmark_dialog_save_and_return"
+          app:showAsAction="ifRoom"/>
+
+</menu>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu/bookmarks_dialog_menu_save_and_remove.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item android:id="@+id/bin"
+          android:icon="@android:drawable/ic_menu_delete"
+          android:title="@string/bookmark_remove"
+          app:showAsAction="ifRoom"/>
+
+    <item android:id="@+id/save"
+          android:icon="@drawable/img_check"
+          android:title="@string/bookmark_dialog_save_and_return"
+          app:showAsAction="ifRoom"/>
+
+</menu>
deleted file mode 100644
--- a/mobile/android/base/resources/menu/edit_bookmarks_menu.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item android:id="@+id/bin"
-          android:icon="@android:drawable/ic_menu_delete"
-          android:title="@string/bookmark_remove"
-          app:showAsAction="always"/>
-</menu>
\ No newline at end of file
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -96,16 +96,26 @@
   <string name="bookmark_already_added">&bookmark_already_added;</string>
   <string name="bookmark_removed">&bookmark_removed;</string>
   <string name="bookmark_updated">&bookmark_updated;</string>
   <string name="bookmark_options">&bookmark_options;</string>
   <string name="bookmarks_selected_folder">&bookmarks_selected_folder;</string>
   <string name="screenshot_added_to_bookmarks">&screenshot_added_to_bookmarks;</string>
   <string name="screenshot_folder_label_in_bookmarks">&screenshot_folder_label_in_bookmarks;</string>
   <string name="readinglist_smartfolder_label_in_bookmarks">&readinglist_smartfolder_label_in_bookmarks;</string>
+
+  <string name="bookmark_dialog_save_and_return">&bookmark_dialog_save_and_return;</string>
+  <string name="bookmark_dialog_folder">&bookmark_dialog_folder;</string>
+
+  <string name="bookmark_edit_title">&bookmark_edit_title;</string>
+  <string name="bookmark_edit_name">&bookmark_edit_name;</string>
+  <string name="bookmark_edit_location">&bookmark_edit_location;</string>
+  <string name="bookmark_edit_keyword">&bookmark_edit_keyword;</string>
+  <string name="bookmark_edit_location_empty_error">&bookmark_edit_location_empty_error;</string>
+
   <string name="bookmark_folder_items">&bookmark_folder_items;</string>
   <string name="bookmark_folder_one_item">&bookmark_folder_one_item;</string>
 
   <string name="reader_saved_offline">&reader_saved_offline;</string>
   <string name="reader_switch_to_bookmarks">&reader_switch_to_bookmarks;</string>
 
   <string name="history_today_section">&history_today_section;</string>
   <string name="history_yesterday_section">&history_yesterday_section;</string>
@@ -431,22 +441,16 @@
   <string name="pref_scroll_title_bar2">&pref_scroll_title_bar2;</string>
   <string name="pref_scroll_title_bar_summary">&pref_scroll_title_bar_summary2;</string>
 
   <string name="pref_compact_tabs">&pref_compact_tabs;</string>
   <string name="pref_compact_tabs_summary">&pref_compact_tabs_summary2;</string>
 
   <string name="page_removed">&page_removed;</string>
 
-  <string name="bookmark_edit_title">&bookmark_edit_title;</string>
-  <string name="bookmark_edit_name">&bookmark_edit_name;</string>
-  <string name="bookmark_edit_location">&bookmark_edit_location;</string>
-  <string name="bookmark_edit_keyword">&bookmark_edit_keyword;</string>
-  <string name="bookmark_edit_location_empty_error">&bookmark_edit_location_empty_error;</string>
-
   <string name="pref_use_master_password">&pref_use_master_password;</string>
   <string name="masterpassword_create_title">&masterpassword_create_title;</string>
   <string name="masterpassword_remove_title">&masterpassword_remove_title;</string>
   <string name="masterpassword_password">&masterpassword_password;</string>
   <string name="masterpassword_confirm">&masterpassword_confirm;</string>
 
   <string name="button_ok">&button_ok;</string>
   <string name="button_cancel">&button_cancel;</string>