Bug 1329131 - Part 2: Implement selecting bookmark folder page. r=Grisha

MozReview-Commit-ID: 6uEC9iauvZj

--HG--
extra : rebase_source : fd022ccdc3841c697aef7e5b2305afd04fd1ab5e
This commit is contained in:
Jing-wei Wu 2017-04-24 00:41:46 +08:00
Родитель a381669023
Коммит e1fe7cc7e3
15 изменённых файлов: 602 добавлений и 5 удалений

Просмотреть файл

@ -38,7 +38,7 @@ import java.lang.ref.WeakReference;
/**
* A dialog fragment that allows editing bookmark's url, title and changing the parent."
*/
public class BookmarkEditFragment extends DialogFragment {
public class BookmarkEditFragment extends DialogFragment implements SelectFolderCallback {
private static final String ARG_ID = "id";
private static final String ARG_URL = "url";
@ -161,6 +161,19 @@ public class BookmarkEditFragment extends DialogFragment {
}
});
folderText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (bookmark == null) {
return;
}
final SelectFolderFragment dialog = SelectFolderFragment.newInstance(bookmark.parentId, bookmark.id);
dialog.setTargetFragment(BookmarkEditFragment.this, 0);
dialog.show(getActivity().getSupportFragmentManager(), "select-bookmark-folder");
}
});
return view;
}
@ -196,6 +209,18 @@ public class BookmarkEditFragment extends DialogFragment {
super.onSaveInstanceState(outState);
}
@Override
public void onFolderChanged(long parentId, String title) {
if (bookmark == null) {
// Don't update view if bookmark isn't initialized yet.
return;
}
bookmark.parentId = parentId;
bookmark.folder = title;
invalidateView(bookmark);
}
private void invalidateView(Bookmark bookmark) {
this.bookmark = bookmark;

Просмотреть файл

@ -0,0 +1,10 @@
/* -*- 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.bookmarks;
interface SelectFolderCallback {
void onFolderChanged(long folderId, String title);
}

Просмотреть файл

@ -0,0 +1,434 @@
/* -*- 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.bookmarks;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.widget.FadedSingleColorTextView;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A dialog fragment that allows selecting a bookmark folder.
*/
public class SelectFolderFragment extends DialogFragment {
private static final String ARG_PARENT_ID = "parentId";
private static final String ARG_BOOKMARK_ID = "bookmarkId";
private static final int LOADER_ID_FOLDERS = 0;
private long parentId;
private long bookmarkId;
private Toolbar toolbar;
private RecyclerView folderView;
private FolderListAdapter foldersAdapter;
private SelectFolderCallback callback;
public static SelectFolderFragment newInstance(long parentId, long bookmarkId) {
final Bundle args = new Bundle();
args.putLong(ARG_PARENT_ID, parentId);
args.putLong(ARG_BOOKMARK_ID, bookmarkId);
final SelectFolderFragment fragment = new SelectFolderFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
final Fragment fragment = getTargetFragment();
if (fragment != null && fragment instanceof SelectFolderCallback) {
callback = (SelectFolderCallback) fragment;
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.Bookmark_Gecko);
final Bundle args = getArguments();
if (args != null) {
parentId = args.getLong(ARG_PARENT_ID);
bookmarkId = args.getLong(ARG_BOOKMARK_ID);
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.bookmark_folder_select, container);
toolbar = (Toolbar) view.findViewById(R.id.toolbar);
folderView = (RecyclerView) view.findViewById(R.id.folder_recycler_view);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
foldersAdapter = new FolderListAdapter();
folderView.setAdapter(foldersAdapter);
folderView.setHasFixedSize(true);
final LinearLayoutManager llm = new LinearLayoutManager(getContext());
folderView.setLayoutManager(llm);
return view;
}
@Override
public void onResume() {
super.onResume();
getLoaderManager().initLoader(LOADER_ID_FOLDERS, null, new FoldersLoaderCallbacks());
}
@Override
public void onPause() {
super.onPause();
getLoaderManager().destroyLoader(LOADER_ID_FOLDERS);
}
public void onFolderSelected(final Folder folder) {
// Ignore fake folders.
if (folder.id == BrowserContract.Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
return;
}
final String title;
if (BrowserContract.Bookmarks.MOBILE_FOLDER_GUID.equals(folder.guid)) {
title = getString(R.string.bookmarks_folder_mobile);
} else {
title = folder.title;
}
if (callback != null) {
callback.onFolderChanged(folder.id, title);
}
dismiss();
}
/**
* A private struct to make it easier to pass bookmark data across threads
*/
private static class Folder {
private final long id;
private final String title;
private final String guid;
int paddingLevel = 0;
private Folder(long id, String title, String guid) {
this.id = id;
this.title = title;
this.guid = guid;
}
}
private class FoldersLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<Folder>> {
@Override
public Loader<List<Folder>> onCreateLoader(int id, Bundle args) {
return new FoldersLoader(getContext(), bookmarkId);
}
@Override
public void onLoadFinished(Loader<List<Folder>> loader, final List<Folder> folders) {
if (folders == null) {
return;
}
foldersAdapter.addAll(folders);
}
@Override
public void onLoaderReset(Loader<List<Folder>> loader) {
}
}
/**
* An AsyncTaskLoader to load {@link Folder} from a cursor.
*/
private static class FoldersLoader extends AsyncTaskLoader<List<Folder>> {
private final ContentResolver cr;
private final BrowserDB db;
private long bookmarkId;
private List<Folder> folders;
private FoldersLoader(Context context, long bookmarkId) {
super(context);
cr = context.getContentResolver();
db = BrowserDB.from(context);
this.bookmarkId = bookmarkId;
}
@Override
public List<Folder> loadInBackground() {
// The data order in cursor is 'ASC TYPE, ASC POSITION, ASC ID'.
final Cursor cursor = db.getAllBookmarkFolders(cr);
if (cursor == null) {
return null;
}
final int capacity = cursor.getCount();
@SuppressLint("UseSparseArrays") final Map<Long, List<Folder>> folderMap = new HashMap<>(capacity);
try {
while (cursor.moveToNext()) {
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE));
final String guid = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.GUID));
final long parentId;
switch (guid) {
case BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID:
case BrowserContract.Bookmarks.UNFILED_FOLDER_GUID:
case BrowserContract.Bookmarks.MENU_FOLDER_GUID:
parentId = BrowserContract.Bookmarks.FAKE_DESKTOP_FOLDER_ID;
break;
case BrowserContract.Bookmarks.MOBILE_FOLDER_GUID:
case BrowserContract.Bookmarks.FAKE_DESKTOP_FOLDER_GUID:
parentId = BrowserContract.Bookmarks.FIXED_ROOT_ID;
break;
default:
parentId = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.PARENT));
break;
}
List<Folder> childFolders = folderMap.get(parentId);
if (childFolders == null) {
childFolders = new ArrayList<>();
}
childFolders.add(new Folder(id, title, guid));
folderMap.put(parentId, childFolders);
}
} finally {
cursor.close();
}
folders = flattenChildFolders(folderMap, capacity);
return folders;
}
private List<Folder> flattenChildFolders(Map<Long, List<Folder>> map, int capacity) {
final List<Folder> result = new ArrayList<>(capacity);
// Here we use ArrayDeque as a stack because it's considered faster than java.util.Stack.
// https://developer.android.com/reference/java/util/ArrayDeque.html
final ArrayDeque<Folder> folderQueue = new ArrayDeque<>(capacity);
// Add "mobile bookmarks" and "desktop bookmarks" into arrayDeque.
final List<Folder> first2Children = map.get((long) BrowserContract.Bookmarks.FIXED_ROOT_ID);
for (Folder child : first2Children) {
if (!BrowserContract.Bookmarks.MOBILE_FOLDER_GUID.equals(child.guid) &&
!BrowserContract.Bookmarks.FAKE_DESKTOP_FOLDER_GUID.equals(child.guid)) {
continue;
}
folderQueue.add(child);
}
while (!folderQueue.isEmpty()) {
final Folder folder = folderQueue.pop();
result.add(folder);
final List<Folder> children = map.get(folder.id);
if (children == null || children.size() == 0) {
continue;
}
for (int i = children.size() - 1; i >= 0; --i) {
final Folder child = children.get(i);
// Ignore itself and descendant folders because we don't allow to select itself or its descendants.
if (child.id == bookmarkId) {
continue;
}
child.paddingLevel = folder.paddingLevel + 1;
folderQueue.push(child);
}
}
return result;
}
@Override
public void deliverResult(List<Folder> folderList) {
if (isReset()) {
folders = null;
return;
}
folders = folderList;
if (isStarted()) {
super.deliverResult(folderList);
}
}
@Override
protected void onStartLoading() {
if (folders != null) {
deliverResult(folders);
}
if (takeContentChanged() || folders == null) {
forceLoad();
}
}
@Override
protected void onStopLoading() {
cancelLoad();
}
@Override
public void onCanceled(List<Folder> folderList) {
folders = null;
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped.
onStopLoading();
folders = null;
}
}
private class FolderListAdapter extends RecyclerView.Adapter<FolderListAdapter.ViewHolder> {
private final int firstStartPadding;
private final int startPadding;
private final List<Folder> folders;
private FolderListAdapter() {
final Resources res = getResources();
firstStartPadding = (int) res.getDimension(R.dimen.bookmark_folder_first_child_padding);
startPadding = (int) res.getDimension(R.dimen.bookmark_folder_child_padding);
folders = new ArrayList<>();
}
private void addAll(List<Folder> folderList) {
folders.clear();
folders.addAll(folderList);
notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.bookmark_folder_item, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Folder folder = folders.get(position);
if (BrowserContract.Bookmarks.MOBILE_FOLDER_GUID.equals(folder.guid)) {
holder.titleView.setText(R.string.bookmarks_folder_mobile);
} else if (folder.id == BrowserContract.Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
holder.titleView.setText(R.string.bookmarks_folder_desktop);
} else {
holder.titleView.setText(folder.title);
}
final int startPadding;
if (folder.paddingLevel == 0) {
startPadding = 0;
} else {
startPadding = firstStartPadding + (folder.paddingLevel - 1) * this.startPadding;
}
ViewCompat.setPaddingRelative(holder.container,
startPadding,
holder.container.getPaddingTop(),
ViewCompat.getPaddingEnd(holder.container),
holder.container.getPaddingBottom());
final boolean isSelected = (folder.id == parentId);
holder.selectView.setVisibility(isSelected ? View.VISIBLE : View.GONE);
if (folder.paddingLevel == 0) {
holder.divider.setVisibility(View.VISIBLE);
holder.itemView.setBackgroundResource(R.color.about_page_header_grey);
} else {
holder.divider.setVisibility(View.GONE);
holder.itemView.setBackgroundResource(R.color.bookmark_folder_bg_color);
}
}
@Override
public int getItemCount() {
return folders.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private final View divider;
private final View container;
private final FadedSingleColorTextView titleView;
private final ImageView selectView;
public ViewHolder(View itemView) {
super(itemView);
divider = itemView.findViewById(R.id.divider);
container = itemView.findViewById(R.id.container);
titleView = (FadedSingleColorTextView) itemView.findViewById(R.id.title);
selectView = (ImageView) itemView.findViewById(R.id.select);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Folder folder = folders.get(getAdapterPosition());
onFolderSelected(folder);
}
});
}
}
}
}

Просмотреть файл

@ -105,8 +105,9 @@ public class BookmarksPanel extends HomeFragment implements BookmarkEditFragment
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
if (type == Bookmarks.TYPE_FOLDER) {
// We don't show a context menu for folders
final boolean enableFullBookmarkManagement = BookmarkUtils.isEnabled(getContext());
if (!enableFullBookmarkManagement && type == Bookmarks.TYPE_FOLDER) {
// We don't show a context menu for folders if full bookmark management isn't enabled.
return null;
}
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
@ -114,6 +115,11 @@ public class BookmarksPanel extends HomeFragment implements BookmarkEditFragment
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE));
info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
info.itemType = RemoveItemType.BOOKMARKS;
if (type == Bookmarks.TYPE_FOLDER) {
info.isFolder = true;
info.url = "";
}
return info;
}
});

Просмотреть файл

@ -17,6 +17,7 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.bookmarks.BookmarkUtils;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
@ -164,8 +165,9 @@ public abstract class HomeFragment extends Fragment {
HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
// Don't show the context menu for folders.
if (info.isFolder) {
// Don't show the context menu for folders if full bookmark management isn't enabled.
final boolean enableFullBookmarkManagement = BookmarkUtils.isEnabled(getContext());
if (info.isFolder && !enableFullBookmarkManagement) {
return;
}
@ -199,6 +201,16 @@ public abstract class HomeFragment extends Fragment {
}
final boolean distSetAsHomepage = GeckoSharedPrefs.forProfile(view.getContext()).getBoolean(GeckoPreferences.PREFS_SET_AS_HOMEPAGE, false);
menu.findItem(R.id.home_set_as_homepage).setVisible(distSetAsHomepage);
// Hide unused menu items for bookmark folder.
if (info.isFolder) {
menu.findItem(R.id.home_open_new_tab).setVisible(false);
menu.findItem(R.id.home_open_private_tab).setVisible(false);
menu.findItem(R.id.home_copyurl).setVisible(false);
menu.findItem(R.id.home_share).setVisible(false);
menu.findItem(R.id.home_add_to_launcher).setVisible(false);
menu.findItem(R.id.home_set_as_homepage).setVisible(false);
}
}
@Override

Просмотреть файл

@ -547,6 +547,7 @@
<!ENTITY bookmark_edit_name "Name">
<!ENTITY bookmark_edit_location "Location">
<!ENTITY bookmark_edit_keyword "Keyword">
<!ENTITY bookmark_select_folder "Select folder">
<!-- Localization note (site_settings_*) : These strings are used in the "Site Settings"
dialog that appears after selecting the "Edit Site Settings" context menu item. -->

Просмотреть файл

@ -504,6 +504,8 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'bookmarks/BookmarkEditFragment.java',
'bookmarks/BookmarkUtils.java',
'bookmarks/EditBookmarkTask.java',
'bookmarks/SelectFolderCallback.java',
'bookmarks/SelectFolderFragment.java',
'BootReceiver.java',
'BrowserApp.java',
'BrowserLocaleManager.java',

Двоичные данные
mobile/android/base/resources/drawable-hdpi/orange_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/orange_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Двоичные данные
mobile/android/base/resources/drawable-xxhdpi/orange_check.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Просмотреть файл

@ -0,0 +1,71 @@
<?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/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/toolbar_divider_grey"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="?attr/selectableItemBackground"
android:minHeight="@dimen/page_row_height"
android:orientation="horizontal">
<View
android:id="@+id/padding_start"
android:layout_width="@dimen/bookmark_folder_child_padding"
android:layout_height="1dp"/>
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/folder_closed"/>
<org.mozilla.gecko.widget.FadedSingleColorTextView
android:id="@+id/title"
style="@style/Widget.FolderTitle.OneLine"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:clickable="false"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:singleLine="true"
android:textColor="@color/text_and_tabs_tray_grey"
android:textSize="18sp"
gecko:fadeWidth="90dp"
tools:text="This is a long test title"/>
<ImageView
android:id="@+id/select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/orange_check"
android:visibility="gone"
tools:visibility="visible"/>
<View
android:id="@+id/padding_end"
android:layout_width="@dimen/bookmark_folder_child_padding"
android:layout_height="1dp"/>
</LinearLayout>
</RelativeLayout>

Просмотреть файл

@ -0,0 +1,30 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="@color/text_and_tabs_tray_grey"
app:navigationIcon="@drawable/abc_ic_ab_back_mtrl_am_alpha"
app:subtitleTextColor="@android:color/white"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:title="@string/bookmark_select_folder" />
<android.support.v7.widget.RecyclerView
android:id="@+id/folder_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:listitem="@layout/bookmark_folder_item"/>
</LinearLayout>

Просмотреть файл

@ -147,4 +147,5 @@
<color name="tablet_tab_strip_divider_color">#555555</color>
<color name="bookmark_folder_bg_color">#FCFCFC</color>
</resources>

Просмотреть файл

@ -235,4 +235,8 @@
<!-- Custom tabs -->
<dimen name="custom_tab_action_button_size">56dp</dimen>
<dimen name="custom_tab_action_button_padding">16dp</dimen>
<!-- Full bookmark management -->
<dimen name="bookmark_folder_first_child_padding">40dp</dimen>
<dimen name="bookmark_folder_child_padding">16dp</dimen>
</resources>

Просмотреть файл

@ -435,6 +435,7 @@
<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_select_folder">&bookmark_select_folder;</string>
<string name="pref_use_master_password">&pref_use_master_password;</string>
<string name="masterpassword_create_title">&masterpassword_create_title;</string>