package com.boardgamegeek.ui; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar.OnMenuItemClickListener; import android.text.TextUtils; import android.util.Pair; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.boardgamegeek.R; import com.boardgamegeek.auth.Authenticator; import com.boardgamegeek.events.CollectionCountChangedEvent; import com.boardgamegeek.events.CollectionSortChangedEvent; import com.boardgamegeek.events.CollectionViewRequestedEvent; import com.boardgamegeek.events.GameSelectedEvent; import com.boardgamegeek.events.GameShortcutCreatedEvent; import com.boardgamegeek.filterer.CollectionFilterer; import com.boardgamegeek.filterer.CollectionFiltererFactory; import com.boardgamegeek.provider.BggContract; import com.boardgamegeek.provider.BggContract.Collection; import com.boardgamegeek.provider.BggContract.CollectionViewFilters; import com.boardgamegeek.provider.BggContract.CollectionViews; import com.boardgamegeek.provider.BggContract.Games; import com.boardgamegeek.service.SyncService; import com.boardgamegeek.sorter.CollectionSorter; import com.boardgamegeek.sorter.CollectionSorterFactory; import com.boardgamegeek.sorter.Sorter; import com.boardgamegeek.ui.dialog.CollectionFilterDialog; import com.boardgamegeek.ui.dialog.CollectionFilterDialogFactory; import com.boardgamegeek.ui.dialog.CollectionFilterDialogFragment; import com.boardgamegeek.ui.dialog.CollectionSortDialogFragment; import com.boardgamegeek.ui.dialog.DeleteViewDialogFragment; import com.boardgamegeek.ui.dialog.SaveViewDialogFragment; import com.boardgamegeek.ui.widget.TimestampView; import com.boardgamegeek.ui.widget.ToolbarActionItemTarget; import com.boardgamegeek.util.ActivityUtils; import com.boardgamegeek.util.DialogUtils; import com.boardgamegeek.util.HelpUtils; import com.boardgamegeek.util.PreferencesUtils; import com.boardgamegeek.util.PresentationUtils; import com.boardgamegeek.util.RandomUtils; import com.boardgamegeek.util.ResolverUtils; import com.boardgamegeek.util.ShowcaseViewWizard; import com.boardgamegeek.util.StringUtils; import com.boardgamegeek.util.UIUtils; import com.boardgamegeek.util.fabric.CollectionViewManipulationEvent; import com.boardgamegeek.util.fabric.FilterEvent; import com.boardgamegeek.util.fabric.SortEvent; import com.crashlytics.android.answers.Answers; import com.crashlytics.android.answers.CustomEvent; import com.github.amlcurran.showcaseview.targets.Target; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.Unbinder; import hugo.weaving.DebugLog; import icepick.Icepick; import icepick.State; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import timber.log.Timber; public class CollectionFragment extends StickyHeaderListFragment implements LoaderCallbacks<Cursor>, MultiChoiceModeListener, CollectionFilterDialog.OnFilterChangedListener, SaveViewDialogFragment.OnViewSavedListener, DeleteViewDialogFragment.OnViewDeletedListener { private static final int HELP_VERSION = 2; private Unbinder unbinder; @BindView(R.id.frame_container) ViewGroup frameContainer; @BindView(R.id.footer_container) ViewGroup footerContainer; @BindView(R.id.filter_linear_layout) LinearLayout filterButtonContainer; @BindView(R.id.filter_scroll_view) View filterButtonScroll; @BindView(R.id.toolbar_footer) Toolbar footerToolbar; @BindView(R.id.row_count) TextView rowCountView; @BindView(R.id.sort_description) TextView sortDescriptionView; private CollectionAdapter adapter; @State int selectedCollectionId; @State long viewId; @State @Nullable String viewName = ""; @State int sortType; @State ArrayList<Integer> types; @State ArrayList<String> data; private CollectionSorter sorter; private final List<CollectionFilterer> filters = new ArrayList<>(); private String defaultWhereClause; private boolean isCreatingShortcut; private final LinkedHashSet<Integer> selectedPositions = new LinkedHashSet<>(); private android.view.MenuItem logPlayMenuItem; private android.view.MenuItem logPlayQuickMenuItem; private android.view.MenuItem bggLinkMenuItem; private CollectionSorterFactory collectionSorterFactory; private ShowcaseViewWizard showcaseViewWizard; @Override @DebugLog public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); if (savedInstanceState != null) { Icepick.restoreInstanceState(this, savedInstanceState); filters.clear(); if (types != null && data != null) { if (types.size() != data.size()) { Timber.w("Mismatched size of arrays: types.size() = %1$s; data.size() = %2$s", types.size(), data.size()); } else { CollectionFiltererFactory factory = new CollectionFiltererFactory(getActivity()); for (int i = 0; i < types.size(); i++) { final Integer filterType = types.get(i); CollectionFilterer filterer = factory.create(filterType); if (filterer == null) { Timber.w("Couldn't create filterer with type %s", filterType); } else { filterer.setData(data.get(i)); filters.add(filterer); } } } } } final Intent intent = UIUtils.fragmentArgumentsToIntent(getArguments()); isCreatingShortcut = "android.intent.action.CREATE_SHORTCUT".equals(intent.getAction()); } @Override @DebugLog public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_collection, container, false); unbinder = ButterKnife.bind(this, view); return view; } @Override @DebugLog public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (isCreatingShortcut) { Snackbar.make(frameContainer, R.string.msg_shortcut_create, Snackbar.LENGTH_LONG).show(); } footerToolbar.inflateMenu(R.menu.collection_fragment); footerToolbar.setOnMenuItemClickListener(footerMenuListener); invalidateMenu(); setEmptyText(); createShowcaseViewWizard(); showcaseViewWizard.maybeShowHelp(); } @Override protected boolean shouldPadForFab() { return true; } private void createShowcaseViewWizard() { showcaseViewWizard = new ShowcaseViewWizard(getActivity(), HelpUtils.HELP_COLLECTION_KEY, HELP_VERSION); showcaseViewWizard.addTarget(R.string.help_collection, Target.NONE); showcaseViewWizard.addTarget(R.string.help_collection_sort, new ToolbarActionItemTarget(R.id.menu_collection_sort, footerToolbar)); showcaseViewWizard.addTarget(R.string.help_collection_filter, new ToolbarActionItemTarget(R.id.menu_collection_filter, footerToolbar)); showcaseViewWizard.addTarget(R.string.help_collection_save, new ToolbarActionItemTarget(R.id.menu_collection_view_save, footerToolbar)); showcaseViewWizard.addTarget(R.string.help_collection_random, new ToolbarActionItemTarget(R.id.menu_collection_random_game, footerToolbar)); showcaseViewWizard.addTarget(R.string.help_collection_long_click, Target.NONE); } @Override @DebugLog public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); sorter = getCollectionSorter(sortType); if (savedInstanceState != null || isCreatingShortcut) { requery(); } if (getListView() != null) { final ListView listView = getListView().getWrappedList(); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); listView.setMultiChoiceModeListener(this); } } @Override public void onDestroyView() { super.onDestroyView(); if (unbinder != null) unbinder.unbind(); } private CollectionSorter getCollectionSorter(int sortType) { if (collectionSorterFactory == null) { collectionSorterFactory = new CollectionSorterFactory(getActivity()); } return collectionSorterFactory.create(sortType); } @DebugLog private void requery() { getLoaderManager().restartLoader(Query._TOKEN, null, this); } @Override @DebugLog public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); sortType = sorter == null ? CollectionSorterFactory.TYPE_UNKNOWN : sorter.getType(); types = new ArrayList<>(); data = new ArrayList<>(); for (CollectionFilterer filterer : filters) { types.add(filterer.getType()); data.add(filterer.flatten()); } Icepick.saveInstanceState(this, outState); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.help, menu); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.menu_help) { showcaseViewWizard.showHelp(); return true; } return super.onOptionsItemSelected(item); } @Override @DebugLog public void onListItemClick(View view, int position, long id) { final Cursor cursor = (Cursor) adapter.getItem(position); final int gameId = cursor.getInt(Query.GAME_ID); final String gameName = cursor.getString(Query.COLLECTION_NAME); final String thumbnailUrl = cursor.getString(Query.THUMBNAIL_URL); if (isCreatingShortcut) { EventBus.getDefault().post(new GameShortcutCreatedEvent(gameId, gameName, thumbnailUrl)); } else { EventBus.getDefault().post(new GameSelectedEvent(gameId, gameName)); setSelectedGameId(gameId); } } @DebugLog private void invalidateMenu() { final Menu menu = footerToolbar.getMenu(); if (isCreatingShortcut) { menu.findItem(R.id.menu_collection_random_game).setVisible(false); menu.findItem(R.id.menu_collection_view_save).setVisible(false); menu.findItem(R.id.menu_collection_view_delete).setVisible(false); } else { menu.findItem(R.id.menu_collection_random_game).setVisible(true); menu.findItem(R.id.menu_collection_view_save).setVisible(true); menu.findItem(R.id.menu_collection_view_delete).setVisible(true); final boolean hasFiltersApplied = (filters.size() > 0); final boolean hasSortApplied = sorter != null && sorter.getType() != CollectionSorterFactory.TYPE_DEFAULT; final boolean hasViews = getActivity() != null && ResolverUtils.getCount(getActivity().getContentResolver(), CollectionViews.CONTENT_URI) > 0; final boolean hasItems = adapter != null && adapter.getCount() > 0; menu.findItem(R.id.menu_collection_view_save).setEnabled(hasFiltersApplied || hasSortApplied); menu.findItem(R.id.menu_collection_view_delete).setEnabled(hasViews); menu.findItem(R.id.menu_collection_random_game).setEnabled(hasItems); } } @NonNull final OnMenuItemClickListener footerMenuListener = new OnMenuItemClickListener() { @DebugLog @Override public boolean onMenuItemClick(@NonNull MenuItem item) { switch (item.getItemId()) { case R.id.menu_collection_random_game: Answers.getInstance().logCustom(new CustomEvent("RandomGame")); final Cursor cursor = (Cursor) adapter.getItem(RandomUtils.getRandom().nextInt(adapter.getCount())); ActivityUtils.launchGame(getActivity(), cursor.getInt(Query.GAME_ID), cursor.getString(Query.COLLECTION_NAME)); return true; case R.id.menu_collection_view_save: SaveViewDialogFragment dialog = SaveViewDialogFragment.newInstance(getActivity(), viewName, createViewDescription(sorter, filters)); dialog.setOnViewSavedListener(CollectionFragment.this); dialog.show(getFragmentManager(), "save_view"); return true; case R.id.menu_collection_view_delete: DeleteViewDialogFragment ddf = DeleteViewDialogFragment.newInstance(getActivity()); ddf.setOnViewDeletedListener(CollectionFragment.this); ddf.show(getFragmentManager(), "delete_view"); return true; case R.id.menu_collection_sort: final CollectionSortDialogFragment sortFragment = CollectionSortDialogFragment.newInstance(swipeRefreshLayout, new CollectionSortDialogFragment.Listener() { @Override public void onSortSelected(int sortType) { setSort(sortType); } }); sortFragment.setSelection(sorter.getType()); DialogUtils.show(sortFragment, getFragmentManager(), "sort"); return true; case R.id.menu_collection_filter: final CollectionFilterDialogFragment filterFragment = CollectionFilterDialogFragment.newInstance(swipeRefreshLayout, new CollectionFilterDialogFragment.Listener() { @Override public void onFilterSelected(int filterType) { launchFilterDialog(filterType); } }); for (CollectionFilterer filter : filters) { filterFragment.addEnabledFilter(filter.getType()); } DialogUtils.show(filterFragment, getFragmentManager(), "filter"); return true; } return launchFilterDialog(item.getItemId()); } }; @DebugLog @Override protected void triggerRefresh() { SyncService.sync(getActivity(), SyncService.FLAG_SYNC_COLLECTION); } @Override protected int getSyncType() { return SyncService.FLAG_SYNC_COLLECTION; } @Nullable @Override @DebugLog public Loader<Cursor> onCreateLoader(int id, Bundle data) { CursorLoader loader = null; if (id == Query._TOKEN) { StringBuilder where = new StringBuilder(); String[] args = {}; if (viewId == 0 && filters.size() == 0) { where.append(buildDefaultWhereClause()); } else { for (CollectionFilterer filter : filters) { if (filter != null) { if (!TextUtils.isEmpty(filter.getSelection())) { if (where.length() > 0) { where.append(" AND "); } where.append("(").append(filter.getSelection()).append(")"); args = StringUtils.concatenate(args, filter.getSelectionArgs()); } } } } loader = new CursorLoader(getActivity(), Collection.CONTENT_URI, sorter == null ? Query.PROJECTION : StringUtils.unionArrays(Query.PROJECTION, sorter.getColumns()), where.toString(), args, sorter == null ? null : sorter.getOrderByClause()); } else if (id == ViewQuery._TOKEN) { if (viewId > 0) { loader = new CursorLoader(getActivity(), CollectionViews.buildViewFilterUri(viewId), ViewQuery.PROJECTION, null, null, null); } } return loader; } private String buildDefaultWhereClause() { if (!TextUtils.isEmpty(defaultWhereClause)) { return defaultWhereClause; } String[] statuses = PreferencesUtils.getSyncStatuses(getActivity()); if (statuses == null) { defaultWhereClause = ""; return defaultWhereClause; } StringBuilder where = new StringBuilder(); for (String status : statuses) { if (TextUtils.isEmpty(status)) { continue; } if (where.length() > 0) { where.append(" OR "); } switch (status) { case "own": where.append(Collection.STATUS_OWN).append("=1"); break; case "played": where.append(Games.NUM_PLAYS).append(">0"); break; case "rated": where.append(Collection.RATING).append(">0"); break; case "comment": where.append(Collection.COMMENT).append("='' OR ").append(Collection.COMMENT).append(" IS NULL"); break; case "prevowned": where.append(Collection.STATUS_PREVIOUSLY_OWNED).append("=1"); break; case "trade": where.append(Collection.STATUS_FOR_TRADE).append("=1"); break; case "want": where.append(Collection.STATUS_WANT).append("=1"); break; case "wanttobuy": where.append(Collection.STATUS_WANT_TO_BUY).append("=1"); break; case "wishlist": where.append(Collection.STATUS_WISHLIST).append("=1"); break; case "wanttoplay": where.append(Collection.STATUS_WANT_TO_PLAY).append("=1"); break; case "preordered": where.append(Collection.STATUS_PREORDERED).append("=1"); break; case "hasparts": where.append(Collection.HASPARTS_LIST).append("='' OR ").append(Collection.HASPARTS_LIST).append(" IS NULL"); break; case "wantparts": where.append(Collection.WANTPARTS_LIST).append("='' OR ").append(Collection.WANTPARTS_LIST).append(" IS NULL"); break; } } defaultWhereClause = where.toString(); return defaultWhereClause; } @Override @DebugLog public void onLoadFinished(@NonNull Loader<Cursor> loader, @NonNull Cursor cursor) { if (getActivity() == null) { return; } int token = loader.getId(); if (token == Query._TOKEN) { if (adapter == null) { adapter = new CollectionAdapter(getActivity()); setListAdapter(adapter); } else { setProgressShown(false); } adapter.changeCursor(cursor); restoreScrollState(); final int rowCount = cursor.getCount(); final String sortDescription = sorter == null ? "" : String.format(getActivity().getString(R.string.sort_description), sorter.getDescription()); rowCountView.setText(String.format(Locale.getDefault(), "%,d", rowCount)); sortDescriptionView.setText(sortDescription); EventBus.getDefault().post(new CollectionCountChangedEvent(rowCount)); EventBus.getDefault().post(new CollectionSortChangedEvent(sortDescription)); bindFilterButtons(); invalidateMenu(); } else if (token == ViewQuery._TOKEN) { if (cursor.moveToFirst()) { viewName = cursor.getString(ViewQuery.NAME); sorter = getCollectionSorter(cursor.getInt(ViewQuery.SORT_TYPE)); filters.clear(); do { CollectionFilterer filter = new CollectionFiltererFactory(getActivity()).create(cursor.getInt(ViewQuery.TYPE)); if (filter != null) { filter.setData(cursor.getString(ViewQuery.DATA)); filters.add(filter); } } while (cursor.moveToNext()); setEmptyText(); requery(); } } else { Timber.d("Query complete, Not Actionable: %s", token); cursor.close(); } } @Override @DebugLog public void onLoaderReset(Loader<Cursor> loader) { if (adapter != null) { adapter.changeCursor(null); } } @Override @DebugLog protected void onScrollDown() { if (footerContainer.getVisibility() != View.VISIBLE) { footerContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.slide_up)); footerContainer.setVisibility(View.VISIBLE); } } @Override @DebugLog protected void onScrollUp() { if (footerContainer.getVisibility() != View.GONE) { footerContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.slide_down)); footerContainer.setVisibility(View.GONE); } } @DebugLog private void setSelectedGameId(int id) { selectedCollectionId = id; if (adapter != null) { adapter.notifyDataSetChanged(); } } @Override @DebugLog public void removeFilter(int type) { for (CollectionFilterer filter : filters) { if (filter.getType() == type) { filters.remove(filter); break; } } setEmptyText(); resetScrollState(); requery(); } @Override @DebugLog public void addFilter(@NonNull CollectionFilterer filter) { filters.remove(filter); if (filter.isValid()) { filters.add(filter); } FilterEvent.log("Collection", String.valueOf(filter.getType())); setEmptyText(); resetScrollState(); requery(); } @DebugLog private void setEmptyText() { if (emptyButton == null) return; @StringRes int resId = R.string.empty_collection; if (!hasEverSynced()) { resId = R.string.empty_collection_sync_never; emptyButton.setVisibility(View.GONE); } else if (hasFiltersApplied()) { resId = R.string.empty_collection_filter_on; emptyButton.setVisibility(View.VISIBLE); } else if (!hasSyncSettings()) { resId = R.string.empty_collection_sync_off; emptyButton.setVisibility(View.VISIBLE); } setEmptyText(getString(resId)); } private boolean hasEverSynced() { return Authenticator.getLong(getContext(), SyncService.TIMESTAMP_COLLECTION_COMPLETE) != 0; } private boolean hasFiltersApplied() { return filters.size() > 0; } private boolean hasSyncSettings() { String[] statuses = PreferencesUtils.getSyncStatuses(getActivity()); return statuses != null && statuses.length > 0; } @DebugLog private void setSort(int sortType) { if (sortType == CollectionSorterFactory.TYPE_UNKNOWN) { sortType = CollectionSorterFactory.TYPE_DEFAULT; } SortEvent.log("Collection", String.valueOf(sortType)); sorter = getCollectionSorter(sortType); resetScrollState(); requery(); } @DebugLog public void createView(long id, String name) { CollectionViewManipulationEvent.log("Create", name); Toast.makeText(getActivity(), R.string.msg_saved, Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new CollectionViewRequestedEvent(id)); } @DebugLog private CollectionFilterer findFilter(int type) { for (CollectionFilterer filter : filters) { if (filter != null && filter.getType() == type) { return filter; } } return null; } @DebugLog private void bindFilterButtons() { filterButtonContainer.removeAllViews(); final LayoutInflater layoutInflater = getLayoutInflater(null); for (CollectionFilterer filter : filters) { if (filter != null && !TextUtils.isEmpty(filter.getDisplayText())) { filterButtonContainer.addView(createFilterButton(layoutInflater, filter.getType(), filter.getDisplayText())); } } filterButtonScroll.setVisibility(filterButtonContainer.getChildCount() > 0 ? View.VISIBLE : View.GONE); } @NonNull @DebugLog private Button createFilterButton(@NonNull LayoutInflater layoutInflater, final int type, String text) { final Button button = (Button) layoutInflater.inflate(R.layout.widget_button_filter, filterButtonContainer, false); button.setText(text); button.setTag(type); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); int margin = getResources().getDimensionPixelSize(R.dimen.padding_half); params.setMargins(0, margin, margin, 0); button.setLayoutParams(params); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(@NonNull View v) { launchFilterDialog((Integer) v.getTag()); } }); button.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { removeFilter(type); return true; } }); return button; } @DebugLog private boolean launchFilterDialog(int filterType) { CollectionFilterDialogFactory factory = new CollectionFilterDialogFactory(); CollectionFilterDialog dialog = factory.create(getActivity(), filterType); if (dialog != null) { dialog.createDialog(getActivity(), this, findFilter(filterType)); return true; } else { Timber.w("Couldn't find a filter dialog of type %s", filterType); return false; } } @DebugLog public long getViewId() { return viewId; } @DebugLog public void setView(long viewId) { if (this.viewId != viewId) { setProgressShown(true); this.viewId = viewId; resetScrollState(); getLoaderManager().restartLoader(ViewQuery._TOKEN, null, this); } } @DebugLog public void clearView() { if (viewId != 0) { setProgressShown(true); viewId = 0; viewName = ""; resetScrollState(); } filters.clear(); sorter = getCollectionSorter(CollectionSorterFactory.TYPE_DEFAULT); requery(); } public class CollectionAdapter extends CursorAdapter implements StickyListHeadersAdapter { @NonNull private final LayoutInflater inflater; @DebugLog public CollectionAdapter(Context context) { super(context, null, false); inflater = getActivity().getLayoutInflater(); } @Override @DebugLog public View newView(Context context, Cursor cursor, ViewGroup parent) { View row = inflater.inflate(R.layout.row_collection, parent, false); ViewHolder holder = new ViewHolder(row); row.setTag(holder); return row; } @Override @DebugLog public void bindView(@NonNull View view, Context context, @NonNull Cursor cursor) { ViewHolder holder = (ViewHolder) view.getTag(); int collectionId = cursor.getInt(Query.COLLECTION_ID); int year = cursor.getInt(Query.COLLECTION_YEAR_PUBLISHED); if (year == 0) { year = cursor.getInt(Query.YEAR_PUBLISHED); } String collectionThumbnailUrl = cursor.getString(Query.COLLECTION_THUMBNAIL_URL); String thumbnailUrl = cursor.getString(Query.THUMBNAIL_URL); final long timestamp = sorter.getTimestamp(cursor); UIUtils.setActivatedCompat(view, collectionId == selectedCollectionId); holder.nameView.setText(cursor.getString(Query.COLLECTION_NAME)); holder.yearView.setText(PresentationUtils.describeYear(getActivity(), year)); holder.timestampView.setTimestamp(timestamp); PresentationUtils.setTextOrHide(holder.infoView, sorter == null ? "" : sorter.getDisplayInfo(cursor)); loadThumbnail(!TextUtils.isEmpty(collectionThumbnailUrl) ? collectionThumbnailUrl : thumbnailUrl, holder.thumbnailView); } @Override public long getHeaderId(int position) { if (position < 0) { return 0; } return sorter.getHeaderId(getCursor(), position); } @Nullable @Override public View getHeaderView(int position, @Nullable View convertView, ViewGroup parent) { HeaderViewHolder holder; if (convertView == null) { holder = new HeaderViewHolder(); convertView = inflater.inflate(R.layout.row_header, parent, false); holder.text = (TextView) convertView.findViewById(android.R.id.title); convertView.setTag(holder); } else { holder = (HeaderViewHolder) convertView.getTag(); } holder.text.setText(sorter.getHeaderText(getCursor(), position)); return convertView; } public class ViewHolder { @BindView(R.id.name) TextView nameView; @BindView(R.id.year) TextView yearView; @BindView(R.id.info) TextView infoView; @BindView(R.id.timestamp) TimestampView timestampView; @BindView(R.id.thumbnail) ImageView thumbnailView; public ViewHolder(@NonNull View view) { ButterKnife.bind(this, view); } } class HeaderViewHolder { TextView text; } } private interface Query { int _TOKEN = 0x01; String[] PROJECTION = { Collection._ID, Collection.COLLECTION_ID, Collection.COLLECTION_NAME, Collection.YEAR_PUBLISHED, Collection.GAME_NAME, Games.GAME_ID, Collection.COLLECTION_THUMBNAIL_URL, Collection.THUMBNAIL_URL, Collection.IMAGE_URL, Collection.COLLECTION_YEAR_PUBLISHED, Games.CUSTOM_PLAYER_SORT }; // int _ID = 0; int COLLECTION_ID = 1; int COLLECTION_NAME = 2; int YEAR_PUBLISHED = 3; int GAME_NAME = 4; int GAME_ID = 5; int COLLECTION_THUMBNAIL_URL = 6; int THUMBNAIL_URL = 7; int IMAGE_URL = 8; int COLLECTION_YEAR_PUBLISHED = 9; int CUSTOM_PLAYER_SORT = 10; } private interface ViewQuery { int _TOKEN = 0x02; String[] PROJECTION = { CollectionViewFilters._ID, CollectionViewFilters.NAME, CollectionViewFilters.SORT_TYPE, CollectionViewFilters.TYPE, CollectionViewFilters.DATA, }; // int _ID = 0; int NAME = 1; int SORT_TYPE = 2; int TYPE = 3; int DATA = 4; } @Override @DebugLog public boolean onCreateActionMode(@NonNull ActionMode mode, @NonNull android.view.Menu menu) { android.view.MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.game_context, menu); logPlayMenuItem = menu.findItem(R.id.menu_log_play); logPlayQuickMenuItem = menu.findItem(R.id.menu_log_play_quick); bggLinkMenuItem = menu.findItem(R.id.menu_link); selectedPositions.clear(); return true; } @Override @DebugLog public boolean onPrepareActionMode(ActionMode mode, android.view.Menu menu) { return false; } @Override @DebugLog public void onDestroyActionMode(ActionMode mode) { } @Override @DebugLog public void onItemCheckedStateChanged(@NonNull ActionMode mode, int position, long id, boolean checked) { if (!isAdded()) { return; } if (checked) { selectedPositions.add(position); } else { selectedPositions.remove(position); } int count = selectedPositions.size(); mode.setTitle(getResources().getQuantityString(R.plurals.msg_games_selected, count, count)); logPlayMenuItem.setVisible(count == 1 && PreferencesUtils.showLogPlay(getActivity())); logPlayQuickMenuItem.setVisible(PreferencesUtils.showQuickLogPlay(getActivity())); bggLinkMenuItem.setVisible(count == 1); } @Override @DebugLog public boolean onActionItemClicked(@NonNull ActionMode mode, @NonNull android.view.MenuItem item) { if (!selectedPositions.iterator().hasNext()) { return false; } Cursor cursor = (Cursor) adapter.getItem(selectedPositions.iterator().next()); int gameId = cursor.getInt(Query.GAME_ID); String gameName = cursor.getString(Query.GAME_NAME); String thumbnailUrl = cursor.getString(Query.THUMBNAIL_URL); String imageUrl = cursor.getString(Query.IMAGE_URL); boolean customPlayerSort = (cursor.getInt(Query.CUSTOM_PLAYER_SORT) == 1); switch (item.getItemId()) { case R.id.menu_log_play: mode.finish(); ActivityUtils.logPlay(getActivity(), gameId, gameName, thumbnailUrl, imageUrl, customPlayerSort); return true; case R.id.menu_log_play_quick: mode.finish(); String text = getResources().getQuantityString(R.plurals.msg_logging_plays, selectedPositions.size()); Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show(); for (int position : selectedPositions) { Cursor c = (Cursor) adapter.getItem(position); ActivityUtils.logQuickPlay(getActivity(), c.getInt(Query.GAME_ID), c.getString(Query.GAME_NAME)); } return true; case R.id.menu_share: mode.finish(); final String shareMethod = "Collection"; if (selectedPositions.size() == 1) { ActivityUtils.shareGame(getActivity(), gameId, gameName, shareMethod); } else { List<Pair<Integer, String>> games = new ArrayList<>(selectedPositions.size()); for (int position : selectedPositions) { Cursor c = (Cursor) adapter.getItem(position); games.add(new Pair<>(c.getInt(Query.GAME_ID), c.getString(Query.GAME_NAME))); } ActivityUtils.shareGames(getActivity(), games, shareMethod); } return true; case R.id.menu_link: mode.finish(); ActivityUtils.linkBgg(getActivity(), gameId); return true; } return false; } private String createViewDescription(Sorter sort, List<CollectionFilterer> filters) { StringBuilder text = new StringBuilder(); for (CollectionFilterer filter : filters) { if (filter != null) { if (text.length() > 0) { text.append("\n"); } text.append(filter.getDisplayText()); } } if (text.length() > 0) { text.append("\n"); } text.append(getString(R.string.by_prefix, sort.getDescription())); return text.toString(); } @Override public void onInsertRequested(String name, boolean isDefault) { long id = insertView(name, sorter.getType(), filters); setDefault(id, isDefault); createView(id, name); } @Override public void onUpdateRequested(String name, boolean isDefault, long viewId) { updateView(viewId, sorter.getType(), filters); setDefault(viewId, isDefault); createView(viewId, name); } @Override public void onDeleteRequested(long viewId) { CollectionViewManipulationEvent.log("Delete"); Toast.makeText(getActivity(), R.string.msg_collection_view_deleted, Toast.LENGTH_SHORT).show(); if (viewId == this.viewId) { EventBus.getDefault().post(new CollectionViewRequestedEvent(PreferencesUtils.getViewDefaultId(getActivity()))); } } private void setDefault(long viewId, boolean isDefault) { if (isDefault) { // TODO: prompt the user if replacing a default PreferencesUtils.putViewDefaultId(getActivity(), viewId); } else { if (viewId == PreferencesUtils.getViewDefaultId(getActivity())) { PreferencesUtils.removeViewDefaultId(getActivity()); } } } private long insertView(String name, int sortType, final List<CollectionFilterer> filters) { ContentResolver resolver = getActivity().getContentResolver(); ContentValues values = new ContentValues(); values.put(CollectionViews.NAME, name); values.put(CollectionViews.STARRED, false); values.put(CollectionViews.SORT_TYPE, sortType); Uri filterUri = resolver.insert(CollectionViews.CONTENT_URI, values); int filterId = CollectionViews.getViewId(filterUri); Uri uri = CollectionViews.buildViewFilterUri(filterId); insertDetails(uri, filters); if (filterUri == null) { return BggContract.INVALID_ID; } return StringUtils.parseLong(filterUri.getLastPathSegment()); } private void updateView(long viewId, int sortType, final List<CollectionFilterer> filters) { ContentResolver resolver = getActivity().getContentResolver(); ContentValues values = new ContentValues(); values.put(CollectionViews.SORT_TYPE, sortType); resolver.update(CollectionViews.buildViewUri(viewId), values, null, null); Uri uri = CollectionViews.buildViewFilterUri(viewId); resolver.delete(uri, null, null); insertDetails(uri, filters); } private void insertDetails(Uri viewFiltersUri, final List<CollectionFilterer> filters) { List<ContentValues> cvs = new ArrayList<>(filters.size()); for (CollectionFilterer filter : filters) { if (filter != null) { ContentValues cv = new ContentValues(); cv.put(CollectionViewFilters.TYPE, filter.getType()); cv.put(CollectionViewFilters.DATA, filter.flatten()); cvs.add(cv); } } if (cvs.size() > 0) { getActivity().getContentResolver().bulkInsert(viewFiltersUri, cvs.toArray(new ContentValues[cvs.size()])); } } }