package cgeo.geocaching.list; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.functions.Action1; import android.app.Activity; import android.app.AlertDialog; import android.app.Application; import android.content.DialogInterface; import android.content.res.Resources; import android.support.annotation.NonNull; import android.view.View; import android.widget.ListView; import java.lang.ref.WeakReference; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.lang3.StringUtils; public final class StoredList extends AbstractList { private static final int TEMPORARY_LIST_ID = 0; public static final StoredList TEMPORARY_LIST = new StoredList(TEMPORARY_LIST_ID, "<temporary>", 0); // Never displayed public static final int STANDARD_LIST_ID = 1; private final int count; // this value is only valid as long as the list is not changed by other database operations public StoredList(final int id, final String title, final int count) { super(id, title); this.count = count; } @Override public String getTitleAndCount() { return title + " [" + count + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + id; return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (!(obj instanceof StoredList)) { return false; } return id == ((StoredList) obj).id; } public static class UserInterface { private final WeakReference<Activity> activityRef; private final Application app; private final Resources res; public UserInterface(@NonNull final Activity activity) { this.activityRef = new WeakReference<>(activity); app = CgeoApplication.getInstance(); res = app.getResources(); } public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId) { promptForListSelection(titleId, runAfterwards, onlyConcreteLists, Collections.singleton(exceptListId), ListNameMemento.EMPTY); } public void promptForMultiListSelection(final int titleId, @NonNull final Action1<Set<Integer>> runAfterwards, final boolean onlyConcreteLists, final Set<Integer> currentListIds, final boolean fastStoreOnLastSelection) { promptForMultiListSelection(titleId, runAfterwards, onlyConcreteLists, Collections.<Integer>emptySet(), currentListIds, ListNameMemento.EMPTY, fastStoreOnLastSelection); } public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId, @NonNull final ListNameMemento listNameMemento) { promptForListSelection(titleId, runAfterwards, onlyConcreteLists, Collections.singleton(exceptListId), listNameMemento); } public void promptForMultiListSelection(final int titleId, @NonNull final Action1<Set<Integer>> runAfterwards, final boolean onlyConcreteLists, final Set<Integer> exceptListIds, final Set<Integer> currentListIds, @NonNull final ListNameMemento listNameMemento, final boolean fastStoreOnLastSelection) { final Set<Integer> selectedListIds = new HashSet<>(fastStoreOnLastSelection ? Settings.getLastSelectedLists() : currentListIds); final List<AbstractList> lists = getMenuLists(onlyConcreteLists, exceptListIds, selectedListIds); final CharSequence[] listTitles = new CharSequence[lists.size()]; final boolean[] selectedItems = new boolean[lists.size()]; final Set<Integer> allListIds = new HashSet<>(lists.size()); for (int i = 0 ; i < lists.size() ; i++) { final AbstractList list = lists.get(i); listTitles[i] = list.getTitleAndCount(); selectedItems[i] = selectedListIds.contains(list.id); allListIds.add(list.id); } // remove from selected which are not available anymore selectedListIds.retainAll(allListIds); if (fastStoreOnLastSelection && !selectedListIds.isEmpty()) { runAfterwards.call(selectedListIds); return; } final Activity activity = activityRef.get(); final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(res.getString(titleId)); builder.setMultiChoiceItems(listTitles, selectedItems, new MultiChoiceClickListener(lists, selectedListIds)); builder.setPositiveButton(android.R.string.ok, new OnOkClickListener(selectedListIds, runAfterwards, listNameMemento)); final Set<Integer> lastSelectedLists = Settings.getLastSelectedLists(); if (currentListIds.isEmpty() && !lastSelectedLists.isEmpty()) { // onClickListener is null because it is handled below to prevent closing of the dialog builder.setNeutralButton(R.string.cache_list_select_last, null); } builder.setNegativeButton(android.R.string.cancel, null); final AlertDialog dialog = builder.create(); dialog.show(); dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!selectedListIds.isEmpty()); dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(new OnLastSelectionClickListener(selectedListIds, lastSelectedLists, dialog, lists, selectedItems)); } public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final Set<Integer> exceptListIds, @NonNull final ListNameMemento listNameMemento) { final List<AbstractList> lists = getMenuLists(onlyConcreteLists, exceptListIds, Collections.<Integer>emptySet()); final List<CharSequence> listsTitle = new ArrayList<>(); for (final AbstractList list : lists) { listsTitle.add(list.getTitleAndCount()); } final CharSequence[] items = new CharSequence[listsTitle.size()]; final Activity activity = activityRef.get(); final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(res.getString(titleId)); builder.setItems(listsTitle.toArray(items), new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialogInterface, final int itemId) { final AbstractList list = lists.get(itemId); if (list == PseudoList.NEW_LIST) { // create new list on the fly promptForListCreation(runAfterwards, listNameMemento.getTerm()); } else { runAfterwards.call(lists.get(itemId).id); } } }); builder.create().show(); } public static List<AbstractList> getMenuLists(final boolean onlyConcreteLists, final int exceptListId) { return getMenuLists(onlyConcreteLists, Collections.singleton(exceptListId), Collections.<Integer>emptySet()); } public static List<AbstractList> getMenuLists(final boolean onlyConcreteLists, final Set<Integer> exceptListIds, final Set<Integer> selectedLists) { final List<AbstractList> lists = new ArrayList<AbstractList>(getSortedLists(selectedLists)); if (exceptListIds.contains(STANDARD_LIST_ID)) { lists.remove(DataStore.getList(STANDARD_LIST_ID)); } for (final Integer exceptListId : exceptListIds) { if (exceptListId >= DataStore.customListIdOffset) { lists.remove(DataStore.getList(exceptListId)); } } if (!onlyConcreteLists) { if (!exceptListIds.contains(PseudoList.ALL_LIST.id)) { lists.add(PseudoList.ALL_LIST); } if (!exceptListIds.contains(PseudoList.HISTORY_LIST.id)) { lists.add(PseudoList.HISTORY_LIST); } } if (!exceptListIds.contains(PseudoList.NEW_LIST.id)) { lists.add(PseudoList.NEW_LIST); } return lists; } @NonNull private static List<StoredList> getSortedLists(final Set<Integer> selectedLists) { final Collator collator = Collator.getInstance(); final List<StoredList> lists = DataStore.getLists(); Collections.sort(lists, new Comparator<StoredList>() { @Override public int compare(final StoredList lhs, final StoredList rhs) { if (selectedLists.contains(lhs.id) && !selectedLists.contains(rhs.id)) { return -1; } if (selectedLists.contains(rhs.id) && !selectedLists.contains(lhs.id)) { return 1; } // have the standard list at the top if (lhs.id == STANDARD_LIST_ID) { return -1; } if (rhs.id == STANDARD_LIST_ID) { return 1; } // otherwise sort alphabetical return collator.compare(lhs.getTitle(), rhs.getTitle()); } }); return lists; } public void promptForListCreation(@NonNull final Action1<Integer> runAfterwards, final String newListName) { handleListNameInput(newListName, R.string.list_dialog_create_title, R.string.list_dialog_create, new Action1<String>() { // We need to update the list cache by creating a new StoredList object here. @SuppressWarnings("unused") @Override public void call(final String listName) { final Activity activity = activityRef.get(); if (activity == null) { return; } final int newId = DataStore.createList(listName); new StoredList(newId, listName, 0); if (newId >= DataStore.customListIdOffset) { runAfterwards.call(newId); } else { ActivityMixin.showToast(activity, res.getString(R.string.list_dialog_create_err)); } } }); } public void promptForListCreation(@NonNull final Action1<Set<Integer>> runAfterwards, final Set<Integer> selectedLists, final String newListName) { handleListNameInput(newListName, R.string.list_dialog_create_title, R.string.list_dialog_create, new Action1<String>() { // We need to update the list cache by creating a new StoredList object here. @SuppressWarnings("unused") @Override public void call(final String listName) { final Activity activity = activityRef.get(); if (activity == null) { return; } final int newId = DataStore.createList(listName); new StoredList(newId, listName, 0); if (newId >= DataStore.customListIdOffset) { selectedLists.remove(PseudoList.NEW_LIST.id); selectedLists.add(newId); Settings.setLastSelectedLists(selectedLists); runAfterwards.call(selectedLists); } else { ActivityMixin.showToast(activity, res.getString(R.string.list_dialog_create_err)); } } }); } private void handleListNameInput(final String defaultValue, final int dialogTitle, final int buttonTitle, final Action1<String> runnable) { final Activity activity = activityRef.get(); if (activity == null) { return; } Dialogs.input(activity, dialogTitle, defaultValue, buttonTitle, new Action1<String>() { @Override public void call(final String input) { // remove whitespaces added by autocompletion of Android keyboard final String listName = StringUtils.trim(input); if (StringUtils.isNotBlank(listName)) { runnable.call(listName); } } }); } public void promptForListRename(final int listId, @NonNull final Runnable runAfterRename) { final StoredList list = DataStore.getList(listId); handleListNameInput(list.title, R.string.list_dialog_rename_title, R.string.list_dialog_rename, new Action1<String>() { @Override public void call(final String listName) { DataStore.renameList(listId, listName); runAfterRename.run(); } }); } private static class MultiChoiceClickListener implements DialogInterface.OnMultiChoiceClickListener { private final List<AbstractList> lists; private final Set<Integer> selectedListIds; public MultiChoiceClickListener(final List<AbstractList> lists, final Set<Integer> selectedListIds) { this.lists = lists; this.selectedListIds = selectedListIds; } @Override public void onClick(final DialogInterface dialog, final int itemId, final boolean isChecked) { final AbstractList list = lists.get(itemId); if (isChecked) { selectedListIds.add(list.id); } else { selectedListIds.remove(list.id); } ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!selectedListIds.isEmpty()); } } private static class OnLastSelectionClickListener implements View.OnClickListener { private final Set<Integer> selectedListIds; private final Set<Integer> lastSelectedLists; private final AlertDialog dialog; private final List<AbstractList> lists; private final boolean[] selectedItems; public OnLastSelectionClickListener(final Set<Integer> selectedListIds, final Set<Integer> lastSelectedLists, final AlertDialog dialog, final List<AbstractList> lists, final boolean[] selectedItems) { this.selectedListIds = selectedListIds; this.lastSelectedLists = lastSelectedLists; this.dialog = dialog; this.lists = lists; this.selectedItems = selectedItems; } @Override public void onClick(final View v) { selectedListIds.clear(); selectedListIds.addAll(lastSelectedLists); final ListView listView = dialog.getListView(); for (int i = 0; i < lists.size() ; i++) { selectedItems[i] = selectedListIds.contains(lists.get(i).id); listView.setItemChecked(i, selectedItems[i]); } dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!selectedListIds.isEmpty()); } } private class OnOkClickListener implements DialogInterface.OnClickListener { private final Set<Integer> selectedListIds; private final Action1<Set<Integer>> runAfterwards; private final ListNameMemento listNameMemento; public OnOkClickListener(final Set<Integer> selectedListIds, final Action1<Set<Integer>> runAfterwards, final ListNameMemento listNameMemento) { this.selectedListIds = selectedListIds; this.runAfterwards = runAfterwards; this.listNameMemento = listNameMemento; } @Override public void onClick(final DialogInterface dialog, final int id) { if (selectedListIds.contains(PseudoList.NEW_LIST.id)) { // create new list on the fly promptForListCreation(runAfterwards, selectedListIds, listNameMemento.getTerm()); } else { Settings.setLastSelectedLists(selectedListIds); runAfterwards.call(selectedListIds); } dialog.cancel(); } } } /** * Get the list title. */ @Override @NonNull public String getTitle() { return title; } @Override public int getNumberOfCaches() { return count; } /** * Return the given list, if it is a concrete list. Return the default list otherwise. */ public static int getConcreteList(final int listId) { if (listId == PseudoList.ALL_LIST.id || listId == TEMPORARY_LIST.id || listId == PseudoList.HISTORY_LIST.id) { return STANDARD_LIST_ID; } return listId; } @Override public boolean isConcrete() { return true; } }