Bug 1270535 - Add "undo" action when bookmark is edited or removed; r=petru

Differential Revision: https://phabricator.services.mozilla.com/D32308

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Andrei Teslovan 2019-06-06 08:50:30 +00:00
parent 834812f141
commit fdbd67ec21
14 changed files with 304 additions and 14 deletions

View File

@ -72,9 +72,11 @@ import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.bookmarks.EditBookmarkCallback;
import org.mozilla.gecko.bookmarks.BookmarkEditFragment;
import org.mozilla.gecko.bookmarks.BookmarkUtils;
import org.mozilla.gecko.bookmarks.EditBookmarkTask;
import org.mozilla.gecko.bookmarks.UndoEditBookmarkTask;
import org.mozilla.gecko.cleanup.FileCleanupController;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
@ -94,12 +96,14 @@ import org.mozilla.gecko.home.HomeBanner;
import org.mozilla.gecko.home.HomeConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.HomeConfigPrefsBackend;
import org.mozilla.gecko.home.HomeContextMenuInfo;
import org.mozilla.gecko.home.HomeFragment;
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.HomeScreen;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.home.UndoRemoveBookmarkTask;
import org.mozilla.gecko.icons.Icons;
import org.mozilla.gecko.icons.IconsHelper;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
@ -203,7 +207,8 @@ public class BrowserApp extends GeckoApp
PropertyAnimator.PropertyAnimationListener,
TabsPanel.TabsLayoutChangeListener,
View.OnKeyListener,
OnboardingHelper.OnboardingListener {
OnboardingHelper.OnboardingListener,
EditBookmarkCallback {
private static final String LOGTAG = "GeckoBrowserApp";
private static final int TABS_ANIMATION_DURATION = 450;
@ -811,10 +816,18 @@ public class BrowserApp extends GeckoApp
mSearchEngineManager = new SearchEngineManager(this, distribution);
// Init suggested sites engine in BrowserDB.
final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
final BrowserDB db = BrowserDB.from(profile);
final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
db.setSuggestedSites(suggestedSites);
// Remove bookmarks that were marked as soft delete
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
db.removeSoftDeleteBookmarks(getContentResolver());
}
});
mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
mReadingListHelper = new ReadingListHelper(appContext, profile);
mAccountsHelper = new AccountsHelper(appContext, profile);
@ -4177,7 +4190,7 @@ public class BrowserApp extends GeckoApp
@Override
public void onEditBookmark(@NonNull Bundle bundle) {
new EditBookmarkTask(this, bundle).execute();
new EditBookmarkTask(this, bundle, this).execute();
}
@Override
@ -4236,4 +4249,14 @@ public class BrowserApp extends GeckoApp
private boolean isShutDownOrAbort() {
return mIsAbortingAppLaunch || mShutdownOnDestroy;
}
@Override
public void onUndoEditBookmark(Bundle bundle) {
new UndoEditBookmarkTask(this, bundle).execute();
}
@Override
public void onUndoRemoveBookmark(HomeContextMenuInfo info, int position) {
new UndoRemoveBookmarkTask(this, info, position).execute();
}
}

View File

@ -146,6 +146,9 @@ public class BookmarkEditFragment extends DialogFragment implements SelectFolder
bundle.putString(Bookmarks.TITLE, newTitle);
bundle.putString(Bookmarks.URL, newUrl);
bundle.putString(Bookmarks.KEYWORD, newKeyword);
bundle.putString(Bookmarks.OLD_TITLE, bookmark.originalTitle);
bundle.putString(Bookmarks.OLD_URL, bookmark.originalUrl);
bundle.putString(Bookmarks.OLD_KEYWORD, bookmark.originalKeyword);
if (bookmark.parentId != bookmark.originalParentId) {
bundle.putLong(Bookmarks.PARENT, bookmark.parentId);
bundle.putLong(BrowserContract.PARAM_OLD_BOOKMARK_PARENT, bookmark.originalParentId);

View File

@ -0,0 +1,24 @@
/* -*- 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.os.Bundle;
import org.mozilla.gecko.home.HomeContextMenuInfo;
public interface EditBookmarkCallback {
/**
* A callback method to tell caller that undo action after editing a bookmark has been pressed.
* Caller takes charge for the change(e.g. update database).
*/
void onUndoEditBookmark(Bundle bundle);
/**
* A callback method to tell caller that undo action after deleting a bookmark has been pressed.
* Caller takes charge for the change(e.g. update database).
*/
void onUndoRemoveBookmark(HomeContextMenuInfo info, int position);
}

View File

@ -10,6 +10,7 @@ import android.content.ContentResolver;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.view.View;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarBuilder;
@ -17,6 +18,7 @@ import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
@ -28,13 +30,16 @@ public class EditBookmarkTask extends UIAsyncTask.WithoutParams<Integer> {
private final ContentResolver contentResolver;
private final Bundle bundle;
public EditBookmarkTask(Activity activity, @NonNull Bundle bundle) {
private EditBookmarkCallback editBookmarkCallback;
public EditBookmarkTask(Activity activity, @NonNull Bundle bundle, EditBookmarkCallback editBookmarkCallback) {
super(ThreadUtils.getBackgroundHandler());
this.activityWeakReference = new WeakReference<>(activity);
this.db = BrowserDB.from(activity);
this.contentResolver = activity.getContentResolver();
this.bundle = bundle;
this.editBookmarkCallback = editBookmarkCallback;
}
@Override
@ -85,6 +90,13 @@ public class EditBookmarkTask extends UIAsyncTask.WithoutParams<Integer> {
}
SnackbarBuilder.builder(activity)
.message(messageResId)
.callback(new SnackbarBuilder.SnackbarCallback() {
@Override
public void onClick(View v) {
editBookmarkCallback.onUndoEditBookmark(bundle);
}
})
.action(R.string.bookmark_edit_undo)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
}

View File

@ -0,0 +1,92 @@
/* -*- 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.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import java.lang.ref.WeakReference;
public class UndoEditBookmarkTask extends UIAsyncTask.WithoutParams<Integer> {
private final WeakReference<Activity> activityWeakReference;
private final BrowserDB db;
private final ContentResolver contentResolver;
private final Bundle bundle;
public UndoEditBookmarkTask(Activity activity, @NonNull Bundle bundle) {
super(ThreadUtils.getBackgroundHandler());
this.activityWeakReference = new WeakReference<>(activity);
this.db = BrowserDB.from(activity);
this.contentResolver = activity.getContentResolver();
this.bundle = bundle;
}
@Override
public Integer doInBackground() {
final long bookmarkId = bundle.getLong(BrowserContract.Bookmarks._ID);
final String url = bundle.getString(BrowserContract.Bookmarks.OLD_URL);
final String title = bundle.getString(BrowserContract.Bookmarks.OLD_TITLE);
final String keyword = bundle.getString(BrowserContract.Bookmarks.OLD_KEYWORD);
final int type = bundle.getInt(BrowserContract.Bookmarks.TYPE);
boolean parentChanged = false;
if (bundle.containsKey(BrowserContract.Bookmarks.PARENT) &&
bundle.containsKey(BrowserContract.PARAM_OLD_BOOKMARK_PARENT)) {
final long newParentId = bundle.getLong(BrowserContract.Bookmarks.PARENT);
final long oldParentId = bundle.getLong(BrowserContract.PARAM_OLD_BOOKMARK_PARENT);
db.updateBookmark(contentResolver, bookmarkId, url, title, keyword, oldParentId, newParentId);
parentChanged = true;
} else {
db.updateBookmark(contentResolver, bookmarkId, url, title, keyword);
}
String extras;
if (type == BrowserContract.Bookmarks.TYPE_FOLDER) {
extras = "bookmark_folder";
} else {
extras = "bookmark";
}
if (parentChanged) {
extras += "_parent_changed";
}
Telemetry.sendUIEvent(TelemetryContract.Event.EDIT, TelemetryContract.Method.DIALOG, extras);
return type;
}
@Override
public void onPostExecute(Integer type) {
final Activity activity = activityWeakReference.get();
if (activity == null || activity.isFinishing()) {
return;
}
final int messageResId;
if (type == BrowserContract.Bookmarks.TYPE_FOLDER) {
messageResId = R.string.bookmark_folder_updated;
} else {
messageResId = R.string.bookmark_updated;
}
SnackbarBuilder.builder(activity)
.message(messageResId)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
}
}

View File

@ -311,6 +311,11 @@ public class BrowserContract {
public static final String ANNOTATION_KEY = "annotation_key";
public static final String ANNOTATION_VALUE = "annotation_value";
public static final String OLD_TITLE = "old_title";
public static final String OLD_URL = "old_url";
public static final String OLD_KEYWORD = "old_keyword";
}
@RobocopTarget

View File

@ -113,6 +113,8 @@ public abstract class BrowserDB {
@Nullable public abstract Cursor getAllBookmarkFolders(ContentResolver cr);
public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
public abstract void removeBookmarkWithId(ContentResolver cr, long id);
public abstract void removeSoftDeleteBookmarks(ContentResolver cr);
public abstract void updateSoftDeleteForBookmarkWithId(ContentResolver cr, long id, boolean softDelete);
public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);
public abstract void updateBookmark(ContentResolver cr, long id, String uri, String title, String keyword);
public abstract void updateBookmark(ContentResolver cr, long id, String uri, String title, String keyword, long newParentId, long oldParentId);

View File

@ -1223,6 +1223,25 @@ public class LocalBrowserDB extends BrowserDB {
new String[] { String.valueOf(id), String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
}
@Override
public void removeSoftDeleteBookmarks(ContentResolver cr) {
// BrowserProvider will bump parent's lastModified timestamp after successful deletion.
cr.delete(mBookmarksUriWithProfile,
Bookmarks.IS_DELETED + " = 1 AND " + Bookmarks.PARENT + " != ? ",
new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
}
public void updateSoftDeleteForBookmarkWithId(ContentResolver cr, long id, boolean safeDelete) {
ContentValues values = new ContentValues();
values.put(Bookmarks.IS_DELETED, safeDelete ? 1 : 0);
// No need to update Bookmarks.SYNC_VERSION at this time. The user might revert this operation.
cr.update(mBookmarksUriWithProfile,
values,
"_id = ?",
new String[] { String.valueOf(id) });
}
@Override
public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
cr.registerContentObserver(mBookmarksUriWithProfile, false, observer);

View File

@ -240,7 +240,7 @@ public class BookmarksPanel extends HomeFragment implements BookmarkEditFragment
@Override
public void onEditBookmark(@NonNull Bundle bundle) {
new EditBookmarkTask(getActivity(), bundle).execute();
new EditBookmarkTask(getActivity(), bundle, this).execute();
}
private void updateUiFromCursor(Cursor c) {

View File

@ -455,7 +455,7 @@ public class BrowserSearch extends HomeFragment
// Position for Top Sites grid items, but will always be -1 since this is only for BrowserSearch result
final int position = -1;
new RemoveItemTask(getActivity(), info, position).execute();
new RemoveItemTask(getActivity(), info, position, getContext(), this).execute();
return true;
}

View File

@ -18,7 +18,9 @@ import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.bookmarks.EditBookmarkCallback;
import org.mozilla.gecko.bookmarks.BookmarkUtils;
import org.mozilla.gecko.bookmarks.UndoEditBookmarkTask;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
@ -59,7 +61,7 @@ import android.view.View;
* <p>
* The containing activity <b>must</b> implement {@link OnUrlOpenListener}.
*/
public abstract class HomeFragment extends Fragment {
public abstract class HomeFragment extends Fragment implements EditBookmarkCallback {
// Log Tag.
private static final String LOGTAG = "GeckoHomeFragment";
@ -348,7 +350,7 @@ public abstract class HomeFragment extends Fragment {
if (info.hasPartnerBookmarkId()) {
new RemovePartnerBookmarkTask(getActivity(), info.bookmarkId).execute();
} else {
new RemoveItemTask(getActivity(), info, position).execute();
new RemoveItemTask(getActivity(), info, position, getContext(), this).execute();
}
return true;
}
@ -440,6 +442,16 @@ public abstract class HomeFragment extends Fragment {
mIsLoaded = true;
}
@Override
public void onUndoEditBookmark(Bundle bundle) {
new UndoEditBookmarkTask(getActivity(), bundle).execute();
}
@Override
public void onUndoRemoveBookmark(HomeContextMenuInfo info, int position) {
new UndoRemoveBookmarkTask(getActivity(), info, position).execute();
}
private static class ToggleASPinTask extends UIAsyncTask.WithoutParams<Void> {
private final WeakReference<Activity> activityWeakReference;
private final Context context;
@ -500,11 +512,13 @@ public abstract class HomeFragment extends Fragment {
private final int position;
private final BrowserDB db;
private EditBookmarkCallback editBookmarkCallback;
/**
* Remove bookmark/history/reading list type item, and also unpin the
* Top Sites grid item at index <code>position</code>.
*/
RemoveItemTask(Activity activity, HomeContextMenuInfo info, int position) {
RemoveItemTask(Activity activity, HomeContextMenuInfo info, int position, Context context, EditBookmarkCallback editBookmarkCallback) {
super(ThreadUtils.getBackgroundHandler());
this.activityWeakReference = new WeakReference<>(activity);
@ -512,6 +526,7 @@ public abstract class HomeFragment extends Fragment {
this.info = info;
this.position = position;
this.db = BrowserDB.from(context);
this.editBookmarkCallback = editBookmarkCallback;
}
@Override
@ -559,10 +574,26 @@ public abstract class HomeFragment extends Fragment {
} else {
message = R.string.page_removed;
}
SnackbarBuilder.builder(activity)
.message(message)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
if (RemoveItemType.BOOKMARKS == info.itemType) {
SnackbarBuilder.builder(activity)
.message(message)
.action(R.string.bookmark_edit_undo)
.callback(new SnackbarBuilder.SnackbarCallback() {
@Override
public void onClick(View v) {
editBookmarkCallback.onUndoRemoveBookmark(info, position);
}
})
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
} else {
SnackbarBuilder.builder(activity)
.message(message)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
}
}
private void removeBookmark(ContentResolver cr) {
@ -579,7 +610,7 @@ public abstract class HomeFragment extends Fragment {
}
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, extra);
db.removeBookmarkWithId(cr, info.bookmarkId);
db.updateSoftDeleteForBookmarkWithId(cr, info.bookmarkId, true);
if (isReaderViewPage) {
ReadingListHelper.removeCachedReaderItem(info.url, context);

View File

@ -0,0 +1,77 @@
/* -*- 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.home;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.support.design.widget.Snackbar;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.reader.SavedReaderViewHelper;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import java.lang.ref.WeakReference;
public class UndoRemoveBookmarkTask extends UIAsyncTask.WithoutParams<Void> {
private static final int NONEXISTENT_POSITION = -1;
private final WeakReference<Activity> activityWeakReference;
private final Context context;
private final HomeContextMenuInfo info;
private final int position;
private final BrowserDB db;
/**
* Remove bookmark/history/reading list type item, and also unpin the
* Top Sites grid item at index <code>position</code>.
*/
public UndoRemoveBookmarkTask(Activity activity, HomeContextMenuInfo info, int position) {
super(ThreadUtils.getBackgroundHandler());
this.activityWeakReference = new WeakReference<>(activity);
this.context = activity.getApplicationContext();
this.info = info;
this.position = position;
this.db = BrowserDB.from(context);
}
@Override
public Void doInBackground() {
ContentResolver cr = context.getContentResolver();
if (position > NONEXISTENT_POSITION) {
db.pinSite(cr, info.url, info.title, position);
if (!db.hideSuggestedSite(info.url)) {
cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
}
}
db.updateSoftDeleteForBookmarkWithId(cr, info.bookmarkId, false);
return null;
}
@Override
public void onPostExecute(Void result) {
final Activity activity = activityWeakReference.get();
if (activity == null || activity.isFinishing()) {
return;
}
SnackbarBuilder.builder(activity)
.message(R.string.bookmark_added)
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
}
}

View File

@ -588,6 +588,7 @@ shown from Android O while a tab is being queued.-->
<!ENTITY bookmark_edit_location "Location">
<!ENTITY bookmark_edit_keyword "Keyword">
<!ENTITY bookmark_select_folder "Select folder">
<!ENTITY bookmark_edit_undo "Undo">
<!-- Localization note (site_settings_*) : These strings are used in the "Site Settings"
dialog that appears after selecting the "Edit Site Settings" context menu item. -->

View File

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