jenkins-bot has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/336349 )
Change subject: Prevent user attempting to create a duplicate-titled reading list ...................................................................... Prevent user attempting to create a duplicate-titled reading list Checks the title in the reading list creation dialog against the list of existing list titles, disabling the positive button and displaying error text in the case of a match. My personal preference and sense of what a user intends favor this solution over something like creating a new list and appending a " (1)" to the title. Bug: T156017 Change-Id: I8a46db35724104434053a802408ffc58c4d800b7 --- M app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java M app/src/main/java/org/wikipedia/readinglist/ReadingList.java M app/src/main/java/org/wikipedia/readinglist/ReadingListDetailView.java M app/src/main/java/org/wikipedia/readinglist/ReadingListDialogs.java A app/src/main/java/org/wikipedia/readinglist/ReadingLists.java M app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java M app/src/main/res/layout/dialog_reading_list_edit.xml M app/src/main/res/values-qq/strings.xml M app/src/main/res/values/strings.xml 9 files changed, 193 insertions(+), 88 deletions(-) Approvals: Dbrant: Looks good to me, approved jenkins-bot: Verified diff --git a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java index 69e5784..6e6fa7b 100644 --- a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java +++ b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java @@ -67,7 +67,7 @@ private View onboardingButton; private InvokeSource invokeSource; private CreateButtonClickListener createClickListener = new CreateButtonClickListener(); - private List<ReadingList> readingLists = new ArrayList<>(); + private ReadingLists readingLists = new ReadingLists(); @Nullable private DialogInterface.OnDismissListener dismissListener; public static AddToReadingListDialog newInstance(@NonNull PageTitle title, InvokeSource source) { @@ -161,9 +161,8 @@ ReadingList.DAO.queryMruLists(null, new CallbackTask.Callback<List<ReadingList>>() { @Override public void success(List<ReadingList> rows) { - readingLists = rows; - ReadingList.sortReadingLists(readingLists, - Prefs.getReadingListSortMode(ReadingList.SORT_BY_NAME_ASC)); + readingLists.set(rows); + readingLists.sort(Prefs.getReadingListSortMode(ReadingLists.SORT_BY_NAME_ASC)); adapter.notifyDataSetChanged(); } }); @@ -188,7 +187,7 @@ .description(null) .pages(new ArrayList<ReadingListPage>()) .build(); - AlertDialog dialog = ReadingListDialogs.createEditDialog(getContext(), list, false, + AlertDialog dialog = ReadingListDialogs.createEditDialog(getContext(), list, false, readingLists.getTitles(), new ReadingListDialogs.EditDialogListener() { @Override public void onModify(String newTitle, String newDescription, boolean saveOffline) { diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingList.java b/app/src/main/java/org/wikipedia/readinglist/ReadingList.java index 9bbffd4..ac9a1e6 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingList.java +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingList.java @@ -9,16 +9,9 @@ import org.wikipedia.readinglist.page.ReadingListPage; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; public final class ReadingList extends ReadingListRow { - public static final int SORT_BY_NAME_ASC = 0; - public static final int SORT_BY_NAME_DESC = 1; - public static final int SORT_BY_RECENT_ASC = 2; - public static final int SORT_BY_RECENT_DESC = 3; - @NonNull private final List<ReadingListPage> pages; private boolean emptyListSavePagesState = true; @@ -124,45 +117,6 @@ @Override protected void validate() { super.validate(); Validate.notNull(pages); - } - } - - public static void sortReadingLists(List<ReadingList> readingLists, int sortMode) { - switch (sortMode) { - case SORT_BY_NAME_ASC: - Collections.sort(readingLists, new Comparator<ReadingList>() { - @Override - public int compare(ReadingList lhs, ReadingList rhs) { - return lhs.getTitle().compareTo(rhs.getTitle()); - } - }); - break; - case SORT_BY_NAME_DESC: - Collections.sort(readingLists, new Comparator<ReadingList>() { - @Override - public int compare(ReadingList lhs, ReadingList rhs) { - return rhs.getTitle().compareTo(lhs.getTitle()); - } - }); - break; - case SORT_BY_RECENT_ASC: - Collections.sort(readingLists, new Comparator<ReadingList>() { - @Override - public int compare(ReadingList lhs, ReadingList rhs) { - return ((Long) lhs.atime()).compareTo(rhs.atime()); - } - }); - break; - case SORT_BY_RECENT_DESC: - Collections.sort(readingLists, new Comparator<ReadingList>() { - @Override - public int compare(ReadingList lhs, ReadingList rhs) { - return ((Long) rhs.atime()).compareTo(lhs.atime()); - } - }); - break; - default: - break; } } } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListDetailView.java b/app/src/main/java/org/wikipedia/readinglist/ReadingListDetailView.java index bd53d28..c339280 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListDetailView.java +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListDetailView.java @@ -44,10 +44,10 @@ import butterknife.ButterKnife; import butterknife.OnClick; -import static org.wikipedia.readinglist.ReadingList.SORT_BY_NAME_ASC; -import static org.wikipedia.readinglist.ReadingList.SORT_BY_NAME_DESC; -import static org.wikipedia.readinglist.ReadingList.SORT_BY_RECENT_ASC; -import static org.wikipedia.readinglist.ReadingList.SORT_BY_RECENT_DESC; +import static org.wikipedia.readinglist.ReadingLists.SORT_BY_NAME_ASC; +import static org.wikipedia.readinglist.ReadingLists.SORT_BY_NAME_DESC; +import static org.wikipedia.readinglist.ReadingLists.SORT_BY_RECENT_ASC; +import static org.wikipedia.readinglist.ReadingLists.SORT_BY_RECENT_DESC; public class ReadingListDetailView extends LinearLayout { @BindView(R.id.reading_list_title) TextView titleView; @@ -64,7 +64,6 @@ private String currentSearchQuery; private ReadingListPageItemAdapter adapter = new ReadingListPageItemAdapter(); - private EditButtonClickListener editButtonListener = new EditButtonClickListener(); private Bitmap deleteIcon = getDeleteBitmap(); public interface ReadingListItemActionListener { @@ -100,9 +99,9 @@ init(); } - public void setReadingList(@NonNull ReadingList readingList) { + public void setReadingListInfo(@NonNull ReadingList readingList, @NonNull List<String> otherTitles) { this.readingList = readingList; - editButton.setOnClickListener(editButtonListener); + editButton.setOnClickListener(new EditButtonClickListener(otherTitles)); contentsListView.setLayoutManager(new LinearLayoutManager(getContext())); contentsListView.setAdapter(adapter); @@ -228,9 +227,15 @@ } private class EditButtonClickListener implements OnClickListener { - @Override - public void onClick(View v) { - ReadingListDialogs.createEditDialog(getContext(), readingList, true, new ReadingListDialogs.EditDialogListener() { + @NonNull private List<String> otherTitles; + + EditButtonClickListener(@NonNull List<String> existingTitles) { + this.otherTitles = existingTitles; + } + + @Override public void onClick(View v) { + ReadingListDialogs.createEditDialog(getContext(), readingList, true, otherTitles, + new ReadingListDialogs.EditDialogListener() { @Override public void onModify(String newTitle, String newDescription, boolean saveOffline) { if (actionListener != null) { diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListDialogs.java b/app/src/main/java/org/wikipedia/readinglist/ReadingListDialogs.java index 3881a3f..08a3719 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListDialogs.java +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListDialogs.java @@ -3,8 +3,11 @@ import android.content.Context; import android.content.DialogInterface; import android.support.annotation.NonNull; +import android.support.design.widget.TextInputLayout; import android.support.v7.app.AlertDialog; import android.support.v7.widget.SwitchCompat; +import android.text.Editable; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; @@ -14,6 +17,8 @@ import org.wikipedia.util.DeviceUtil; import org.wikipedia.util.DimenUtil; +import java.util.List; + public final class ReadingListDialogs { public interface EditDialogListener { @@ -21,29 +26,56 @@ void onDelete(); } - public static AlertDialog createEditDialog(Context context, final ReadingList readingList, - boolean showDescription, + private interface TitleTextCallback { + void onMatchesExistingTitle(@NonNull String title); + void onDoesNotMatchExistingTitle(); + } + + private static class TitleTextWatcher implements TextWatcher { + @NonNull private List<String> titles; + @NonNull private TitleTextCallback cb; + + TitleTextWatcher(@NonNull List<String> titles, @NonNull TitleTextCallback cb) { + this.titles = titles; + this.cb = cb; + } + + @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + for (String title : titles) { + if (title.equals(charSequence.toString())) { + cb.onMatchesExistingTitle(title); + return; + } + } + cb.onDoesNotMatchExistingTitle(); + } + + @Override public void afterTextChanged(Editable editable) { + } + } + + public static AlertDialog createEditDialog(final Context context, final ReadingList readingList, + boolean showDescription, @NonNull final List<String> otherTitles, @NonNull final EditDialogListener listener) { final View rootView = LayoutInflater.from(context).inflate(R.layout.dialog_reading_list_edit, null); final EditText titleView = (EditText) rootView.findViewById(R.id.reading_list_title); + final TextInputLayout titleContainer = (TextInputLayout) rootView.findViewById(R.id.reading_list_title_container); final EditText descriptionView = (EditText) rootView.findViewById(R.id.reading_list_description); final View deleteView = rootView.findViewById(R.id.reading_list_delete_link); deleteView.setVisibility(showDescription ? View.VISIBLE : View.GONE); final SwitchCompat saveOfflineSwitch = (SwitchCompat) rootView.findViewById(R.id.reading_list_offline_switch); descriptionView.setVisibility(showDescription ? View.VISIBLE : View.GONE); saveOfflineSwitch.setVisibility(showDescription ? View.VISIBLE : View.GONE); + titleContainer.setErrorEnabled(true); // TODO: move this to XML once the attribute becomes available. final int switchPaddingDp = 4; saveOfflineSwitch.setSwitchPadding((int) (switchPaddingDp * DimenUtil.getDensityScalar())); - titleView.setText(readingList.getTitle()); - titleView.selectAll(); - - descriptionView.setText(readingList.getDescription()); - saveOfflineSwitch.setChecked(readingList.getSaveOffline()); - - AlertDialog.Builder builder = new AlertDialog.Builder(context) + final AlertDialog alertDialog = new AlertDialog.Builder(context) .setView(rootView) .setPositiveButton(R.string.reading_list_edit_ok, new DialogInterface.OnClickListener() { @Override @@ -58,9 +90,35 @@ DeviceUtil.hideSoftKeyboard(titleView); DeviceUtil.hideSoftKeyboard(descriptionView); } - }); + }).create(); - final AlertDialog alertDialog = builder.create(); + alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialogInterface) { + titleView.addTextChangedListener(new TitleTextWatcher(otherTitles, new TitleTextCallback() { + @Override + public void onMatchesExistingTitle(@NonNull String title) { + titleContainer.setError(context.getString(R.string.reading_list_title_exists, title)); + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + + } + + @Override + public void onDoesNotMatchExistingTitle() { + titleContainer.setError(null); + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); + } + })); + + titleView.setText(readingList.getTitle()); + titleView.selectAll(); + + descriptionView.setText(readingList.getDescription()); + saveOfflineSwitch.setChecked(readingList.getSaveOffline()); + + } + }); + alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { @@ -68,6 +126,7 @@ DeviceUtil.hideSoftKeyboard(descriptionView); } }); + if (!showDescription) { alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingLists.java b/app/src/main/java/org/wikipedia/readinglist/ReadingLists.java new file mode 100644 index 0000000..48bb91f --- /dev/null +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingLists.java @@ -0,0 +1,86 @@ +package org.wikipedia.readinglist; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ReadingLists { + public static final int SORT_BY_NAME_ASC = 0; + public static final int SORT_BY_NAME_DESC = 1; + public static final int SORT_BY_RECENT_ASC = 2; + public static final int SORT_BY_RECENT_DESC = 3; + + @NonNull private List<ReadingList> lists = new ArrayList<>(); + + public void set(@NonNull List<ReadingList> lists) { + this.lists = lists; + } + + @NonNull public List<String> getTitles() { + List<String> result = new ArrayList<>(); + for (ReadingList list : lists) { + result.add(list.getTitle()); + } + return result; + } + + @NonNull public List<String> getTitlesExcept(@NonNull String title) { + List<String> result = getTitles(); + result.remove(title); + return result; + } + + public ReadingList get(int pos) { + return lists.get(pos); + } + + public int size() { + return lists.size(); + } + + public boolean isEmpty() { + return lists.isEmpty(); + } + + public void sort(int sortMode) { + switch (sortMode) { + case SORT_BY_NAME_ASC: + Collections.sort(lists, new Comparator<ReadingList>() { + @Override + public int compare(ReadingList lhs, ReadingList rhs) { + return lhs.getTitle().compareTo(rhs.getTitle()); + } + }); + break; + case SORT_BY_NAME_DESC: + Collections.sort(lists, new Comparator<ReadingList>() { + @Override + public int compare(ReadingList lhs, ReadingList rhs) { + return rhs.getTitle().compareTo(lhs.getTitle()); + } + }); + break; + case SORT_BY_RECENT_ASC: + Collections.sort(lists, new Comparator<ReadingList>() { + @Override + public int compare(ReadingList lhs, ReadingList rhs) { + return ((Long) lhs.atime()).compareTo(rhs.atime()); + } + }); + break; + case SORT_BY_RECENT_DESC: + Collections.sort(lists, new Comparator<ReadingList>() { + @Override + public int compare(ReadingList lhs, ReadingList rhs) { + return ((Long) rhs.atime()).compareTo(lhs.atime()); + } + }); + break; + default: + break; + } + } +} diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java index 5fec00f..d2d7aa5 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java @@ -34,7 +34,6 @@ import org.wikipedia.settings.Prefs; import org.wikipedia.util.FeedbackUtil; -import java.util.ArrayList; import java.util.List; import butterknife.BindView; @@ -54,7 +53,7 @@ @BindView(R.id.reading_list_list) RecyclerView readingListView; @BindView(R.id.empty_container) View emptyContainer; @BindView(R.id.pager) ViewPager pager; - private List<ReadingList> readingLists = new ArrayList<>(); + private ReadingLists readingLists = new ReadingLists(); private ReadingListsFunnel funnel = new ReadingListsFunnel(); @BindView(R.id.list_detail_view) ReadingListDetailView listDetailView; @@ -78,8 +77,8 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); - readingListSortMode = Prefs.getReadingListSortMode(ReadingList.SORT_BY_NAME_ASC); - readingListPageSortMode = Prefs.getReadingListPageSortMode(ReadingList.SORT_BY_NAME_ASC); + readingListSortMode = Prefs.getReadingListSortMode(ReadingLists.SORT_BY_NAME_ASC); + readingListPageSortMode = Prefs.getReadingListPageSortMode(ReadingLists.SORT_BY_NAME_ASC); } @Override @@ -151,11 +150,11 @@ MenuItem sortByNameItem = menu.findItem(R.id.menu_sort_by_name); MenuItem sortByRecentItem = menu.findItem(R.id.menu_sort_by_recent); if (pager.getCurrentItem() == PAGE_READING_LISTS) { - sortByNameItem.setTitle(readingListSortMode == ReadingList.SORT_BY_NAME_ASC ? R.string.reading_list_sort_by_name_desc : R.string.reading_list_sort_by_name); - sortByRecentItem.setTitle(readingListSortMode == ReadingList.SORT_BY_RECENT_DESC ? R.string.reading_list_sort_by_recent_desc : R.string.reading_list_sort_by_recent); + sortByNameItem.setTitle(readingListSortMode == ReadingLists.SORT_BY_NAME_ASC ? R.string.reading_list_sort_by_name_desc : R.string.reading_list_sort_by_name); + sortByRecentItem.setTitle(readingListSortMode == ReadingLists.SORT_BY_RECENT_DESC ? R.string.reading_list_sort_by_recent_desc : R.string.reading_list_sort_by_recent); } else { - sortByNameItem.setTitle(readingListPageSortMode == ReadingList.SORT_BY_NAME_ASC ? R.string.reading_list_sort_by_name_desc : R.string.reading_list_sort_by_name); - sortByRecentItem.setTitle(readingListPageSortMode == ReadingList.SORT_BY_RECENT_DESC ? R.string.reading_list_sort_by_recent_desc : R.string.reading_list_sort_by_recent); + sortByNameItem.setTitle(readingListPageSortMode == ReadingLists.SORT_BY_NAME_ASC ? R.string.reading_list_sort_by_name_desc : R.string.reading_list_sort_by_name); + sortByRecentItem.setTitle(readingListPageSortMode == ReadingLists.SORT_BY_RECENT_DESC ? R.string.reading_list_sort_by_recent_desc : R.string.reading_list_sort_by_recent); } } @@ -163,10 +162,10 @@ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_sort_by_name: - setSortMode(ReadingList.SORT_BY_NAME_ASC, ReadingList.SORT_BY_NAME_DESC); + setSortMode(ReadingLists.SORT_BY_NAME_ASC, ReadingLists.SORT_BY_NAME_DESC); return true; case R.id.menu_sort_by_recent: - setSortMode(ReadingList.SORT_BY_RECENT_DESC, ReadingList.SORT_BY_RECENT_ASC); + setSortMode(ReadingLists.SORT_BY_RECENT_DESC, ReadingLists.SORT_BY_RECENT_ASC); return true; case R.id.menu_search_lists: ((AppCompatActivity) getActivity()) @@ -204,7 +203,7 @@ new CallbackTask.Callback<List<ReadingList>>() { @Override public void success(List<ReadingList> rows) { - readingLists = rows; + readingLists.set(rows); sortLists(); updateEmptyMessage(); } @@ -288,7 +287,7 @@ if (actionMode != null) { actionMode.finish(); } - listDetailView.setReadingList(readingList); + listDetailView.setReadingListInfo(readingList, readingLists.getTitlesExcept(readingList.getTitle())); listDetailView.setSort(readingListPageSortMode); pager.setCurrentItem(PAGE_LIST_DETAIL); @@ -413,7 +412,7 @@ } private void sortLists() { - ReadingList.sortReadingLists(readingLists, readingListSortMode); + readingLists.sort(readingListSortMode); adapter.notifyDataSetChanged(); } diff --git a/app/src/main/res/layout/dialog_reading_list_edit.xml b/app/src/main/res/layout/dialog_reading_list_edit.xml index 3d3b10e..a135246 100644 --- a/app/src/main/res/layout/dialog_reading_list_edit.xml +++ b/app/src/main/res/layout/dialog_reading_list_edit.xml @@ -13,6 +13,7 @@ android:orientation="vertical"> <android.support.design.widget.TextInputLayout + android:id="@+id/reading_list_title_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/reading_list_name_hint"> diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 7f80088..1e8e3d5 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -274,9 +274,9 @@ Followed by {{msg-wm|Wikipedia-android-strings-alpha update notification text}}.</string> <string name="wiktionary_no_definitions_found">Message shown when a user requests a definition from Wiktionary but no definition is found.</string> <string name="alpha_update_notification_title">Title for notification when new alpha update is available. - - - + + + Followed by {{msg-wm|Wikipedia-android-strings-alpha update notification text}}.</string> <string name="alpha_update_notification_text">Text for notification when new alpha update is available. @@ -380,6 +380,7 @@ {{Identical|Untitled}}</string> <string name="reading_list_save_to">Menu item label for saving the current article to a reading list.</string> <string name="reading_list_create_new">Dialog title for creating a new reading list.</string> + <string name="reading_list_title_exists">Error text shown when a user attempts to create a new list with the same title as an existing list. The \"%s\" symbol is replaced with the title of the existing list.</string> <string name="reading_list_added_to_named">Message shown when an article is added to a reading list. The \"%s\" symbol is replaced with the name of the reading list.</string> <string name="reading_list_added_to_unnamed">Message shown when an article is added to an untitled reading list.</string> <string name="reading_list_already_exists">Message shown when an article already exists in the selected reading list.</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4178c5c..b9a28ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,7 @@ <string name="reading_list_untitled">Untitled</string> <string name="reading_list_save_to">Save to reading list</string> <string name="reading_list_create_new">Create new</string> + <string name="reading_list_title_exists">Reading list \"%s\" already exists.</string> <string name="reading_list_added_to_named">Added to %s.</string> <string name="reading_list_added_to_unnamed">Added to reading list.</string> <string name="reading_list_already_exists">This article is already in that list.</string> -- To view, visit https://gerrit.wikimedia.org/r/336349 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I8a46db35724104434053a802408ffc58c4d800b7 Gerrit-PatchSet: 5 Gerrit-Project: apps/android/wikipedia Gerrit-Branch: master Gerrit-Owner: Mholloway <mhollo...@wikimedia.org> Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org> Gerrit-Reviewer: Dbrant <dbr...@wikimedia.org> Gerrit-Reviewer: Mholloway <mhollo...@wikimedia.org> Gerrit-Reviewer: Niedzielski <sniedziel...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits