package cgeo.geocaching;
import cgeo.geocaching.activity.AbstractActivity;
import cgeo.geocaching.activity.AbstractListActivity;
import cgeo.geocaching.activity.ActivityMixin;
import cgeo.geocaching.activity.FilteredActivity;
import cgeo.geocaching.activity.Progress;
import cgeo.geocaching.apps.cachelist.CacheListApp;
import cgeo.geocaching.apps.cachelist.CacheListAppUtils;
import cgeo.geocaching.apps.cachelist.CacheListApps;
import cgeo.geocaching.apps.cachelist.ListNavigationSelectionActionProvider;
import cgeo.geocaching.apps.navi.NavigationAppFactory;
import cgeo.geocaching.command.AbstractCachesCommand;
import cgeo.geocaching.command.CopyToListCommand;
import cgeo.geocaching.command.DeleteListCommand;
import cgeo.geocaching.command.MakeListUniqueCommand;
import cgeo.geocaching.command.MoveToListCommand;
import cgeo.geocaching.command.RenameListCommand;
import cgeo.geocaching.compatibility.Compatibility;
import cgeo.geocaching.enumerations.CacheListType;
import cgeo.geocaching.enumerations.CacheType;
import cgeo.geocaching.enumerations.LoadFlags;
import cgeo.geocaching.enumerations.StatusCode;
import cgeo.geocaching.export.FieldNoteExport;
import cgeo.geocaching.export.GpxExport;
import cgeo.geocaching.export.PersonalNoteExport;
import cgeo.geocaching.files.GPXImporter;
import cgeo.geocaching.files.GpxFileListActivity;
import cgeo.geocaching.filter.FilterActivity;
import cgeo.geocaching.filter.IFilter;
import cgeo.geocaching.list.AbstractList;
import cgeo.geocaching.list.ListNameMemento;
import cgeo.geocaching.list.PseudoList;
import cgeo.geocaching.list.StoredList;
import cgeo.geocaching.loaders.AbstractSearchLoader;
import cgeo.geocaching.loaders.AbstractSearchLoader.CacheListLoaderType;
import cgeo.geocaching.loaders.CoordsGeocacheListLoader;
import cgeo.geocaching.loaders.FinderGeocacheListLoader;
import cgeo.geocaching.loaders.HistoryGeocacheListLoader;
import cgeo.geocaching.loaders.KeywordGeocacheListLoader;
import cgeo.geocaching.loaders.NextPageGeocacheListLoader;
import cgeo.geocaching.loaders.OfflineGeocacheListLoader;
import cgeo.geocaching.loaders.OwnerGeocacheListLoader;
import cgeo.geocaching.loaders.PocketGeocacheListLoader;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.log.LoggingUI;
import cgeo.geocaching.maps.DefaultMap;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.models.PocketQuery;
import cgeo.geocaching.network.Cookies;
import cgeo.geocaching.network.DownloadProgress;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.network.Send2CgeoDownloader;
import cgeo.geocaching.sensors.GeoData;
import cgeo.geocaching.sensors.GeoDirHandler;
import cgeo.geocaching.sensors.Sensors;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.settings.SettingsActivity;
import cgeo.geocaching.sorting.CacheComparator;
import cgeo.geocaching.sorting.SortActionProvider;
import cgeo.geocaching.storage.DataStore;
import cgeo.geocaching.ui.CacheListAdapter;
import cgeo.geocaching.ui.WeakReferenceHandler;
import cgeo.geocaching.ui.dialog.Dialogs;
import cgeo.geocaching.utils.AndroidRxUtils;
import cgeo.geocaching.utils.AngleUtils;
import cgeo.geocaching.utils.CalendarUtils;
import cgeo.geocaching.utils.DisposableHandler;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.functions.Action1;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBar;
import android.text.TextUtils;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ListView;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import butterknife.ButterKnife;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.Disposables;
import io.reactivex.functions.Action;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
public class CacheListActivity extends AbstractListActivity implements FilteredActivity, LoaderManager.LoaderCallbacks<SearchResult> {
private static final int MAX_LIST_ITEMS = 1000;
private static final int REFRESH_WARNING_THRESHOLD = 100;
private static final int REQUEST_CODE_IMPORT_GPX = 1;
private static final int REQUEST_CODE_RESTART = 2;
private static final String STATE_FILTER = "currentFilter";
private static final String STATE_INVERSE_SORT = "currentInverseSort";
private static final String STATE_LIST_TYPE = "currentListType";
private static final String STATE_LIST_ID = "currentListId";
private static final String BUNDLE_ACTION_KEY = "afterLoadAction";
private CacheListType type = null;
private Geopoint coords = null;
private SearchResult search = null;
/** The list of shown caches shared with Adapter. Don't manipulate outside of main thread only with Handler */
private final List<Geocache> cacheList = new ArrayList<>();
private CacheListAdapter adapter = null;
private View listFooter = null;
private TextView listFooterText = null;
private final Progress progress = new Progress();
private String title = "";
private int detailTotal = 0;
private final AtomicInteger detailProgress = new AtomicInteger(0);
private long detailProgressTime = 0L;
private int listId = StoredList.TEMPORARY_LIST.id; // Only meaningful for the OFFLINE type
private final GeoDirHandler geoDirHandler = new GeoDirHandler() {
@Override
public void updateDirection(final float direction) {
if (Settings.isLiveList()) {
adapter.setActualHeading(AngleUtils.getDirectionNow(direction));
}
}
@Override
public void updateGeoData(final GeoData geoData) {
adapter.setActualCoordinates(geoData.getCoords());
}
};
private ContextMenuInfo lastMenuInfo;
private String contextMenuGeocode = "";
private final CompositeDisposable resumeDisposables = new CompositeDisposable();
private final ListNameMemento listNameMemento = new ListNameMemento();
private final Handler loadCachesHandler = new LoadCachesHandler(this);
private final DisposableHandler clearOfflineLogsHandler = new ClearOfflineLogsHandler(this);
private final Handler importGpxAttachementFinishedHandler = new ImportGpxAttachementFinishedHandler(this);
private AbstractSearchLoader currentLoader;
private static class LoadCachesHandler extends WeakReferenceHandler<CacheListActivity> {
protected LoadCachesHandler(final CacheListActivity activity) {
super(activity);
}
@Override
public void handleMessage(final Message msg) {
final CacheListActivity activity = getReference();
if (activity == null) {
return;
}
activity.handleCachesLoaded();
}
}
// FIXME: This method has mostly been replaced by the loaders. But it still contains a license agreement check.
public void handleCachesLoaded() {
try {
updateAdapter();
updateTitle();
showFooterMoreCaches();
if (search != null && search.getError() == StatusCode.UNAPPROVED_LICENSE) {
showLicenseConfirmationDialog();
} else if (search != null && search.getError() != StatusCode.NO_ERROR) {
showToast(res.getString(R.string.err_download_fail) + ' ' + search.getError().getErrorString(res) + '.');
hideLoading();
showProgress(false);
finish();
return;
}
setAdapterCurrentCoordinates(false);
} catch (final Exception e) {
showToast(res.getString(R.string.err_detail_cache_find_any));
Log.e("CacheListActivity.loadCachesHandler", e);
hideLoading();
showProgress(false);
finish();
return;
}
try {
hideLoading();
showProgress(false);
} catch (final Exception e2) {
Log.e("CacheListActivity.loadCachesHandler.2", e2);
}
adapter.setSelectMode(false);
}
private void showLicenseConfirmationDialog() {
final AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(res.getString(R.string.license));
dialog.setMessage(res.getString(R.string.err_license));
dialog.setCancelable(true);
dialog.setNegativeButton(res.getString(R.string.license_dismiss), new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
Cookies.clearCookies();
dialog.cancel();
}
});
dialog.setPositiveButton(res.getString(R.string.license_show), new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
Cookies.clearCookies();
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.geocaching.com/software/agreement.aspx?ID=0")));
}
});
final AlertDialog alert = dialog.create();
alert.show();
}
/**
* Loads the caches and fills the {@link #cacheList} according to {@link #search} content.
*
* If {@link #search} is {@code null}, this does nothing.
*/
private void replaceCacheListFromSearch() {
if (search != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
cacheList.clear();
// The database search was moved into the UI call intentionally. If this is done before the runOnUIThread,
// then we have 2 sets of caches in memory. This can lead to OOM for huge cache lists.
final Set<Geocache> cachesFromSearchResult = search.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB);
cacheList.addAll(cachesFromSearchResult);
adapter.reFilter();
updateTitle();
showFooterMoreCaches();
}
});
}
}
private static String getCacheNumberString(final Resources res, final int count) {
return res.getQuantityString(R.plurals.cache_counts, count, count);
}
protected void updateTitle() {
setTitle(title);
getSupportActionBar().setSubtitle(getCurrentSubtitle());
refreshSpinnerAdapter();
}
private static final class LoadDetailsHandler extends DisposableHandler {
private final WeakReference<CacheListActivity> activityRef;
LoadDetailsHandler(final CacheListActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
protected void handleDispose() {
final CacheListActivity activity = activityRef.get();
if (activity != null) {
super.handleDispose();
activity.replaceCacheListFromSearch();
}
}
@Override
public void handleRegularMessage(final Message msg) {
final CacheListActivity activity = activityRef.get();
if (activity != null) {
activity.updateAdapter();
final Progress progress = activity.progress;
if (msg.what == DownloadProgress.MSG_LOADED) {
((Geocache) msg.obj).setStatusChecked(false);
final CacheListAdapter adapter = activity.adapter;
adapter.notifyDataSetChanged();
final int dp = activity.detailProgress.get();
final int secondsElapsed = (int) ((System.currentTimeMillis() - activity.detailProgressTime) / 1000);
final int minutesRemaining = (activity.detailTotal - dp) * secondsElapsed / (dp > 0 ? dp : 1) / 60;
final Resources res = activity.res;
progress.setProgress(dp);
if (minutesRemaining < 1) {
progress.setMessage(res.getString(R.string.caches_downloading) + " " + res.getString(R.string.caches_eta_ltm));
} else {
progress.setMessage(res.getString(R.string.caches_downloading) + " " + res.getQuantityString(R.plurals.caches_eta_mins, minutesRemaining, minutesRemaining));
}
} else {
new AsyncTask<Void, Void, Set<Geocache>>() {
@Override
protected Set<Geocache> doInBackground(final Void... params) {
final SearchResult search = activity.search;
return search != null ? search.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB) : null;
}
@Override
protected void onPostExecute(final Set<Geocache> result) {
if (CollectionUtils.isNotEmpty(result)) {
final List<Geocache> cacheList = activity.cacheList;
cacheList.clear();
cacheList.addAll(result);
activity.adapter.reFilter();
}
activity.setAdapterCurrentCoordinates(false);
activity.showProgress(false);
progress.dismiss();
}
}.execute();
}
}
}
}
/**
* TODO Possibly parts should be a Thread not a Handler
*/
private static final class DownloadFromWebHandler extends DisposableHandler {
private final WeakReference<CacheListActivity> activityRef;
DownloadFromWebHandler(final CacheListActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleRegularMessage(final Message msg) {
final CacheListActivity activity = activityRef.get();
if (activity != null) {
activity.updateAdapter();
final CacheListAdapter adapter = activity.adapter;
adapter.notifyDataSetChanged();
final Progress progress = activity.progress;
switch (msg.what) {
case DownloadProgress.MSG_WAITING: //no caches
progress.setMessage(activity.res.getString(R.string.web_import_waiting));
break;
case DownloadProgress.MSG_LOADING: { //cache downloading
final Resources res = activity.res;
progress.setMessage(res.getString(R.string.web_downloading) + ' ' + msg.obj + res.getString(R.string.ellipsis));
break;
}
case DownloadProgress.MSG_LOADED: { //Cache downloaded
final Resources res = activity.res;
progress.setMessage(res.getString(R.string.web_downloaded) + ' ' + msg.obj + res.getString(R.string.ellipsis));
activity.refreshCurrentList();
break;
}
case DownloadProgress.MSG_SERVER_FAIL:
progress.dismiss();
activity.showToast(activity.res.getString(R.string.sendToCgeo_download_fail));
activity.finish();
break;
case DownloadProgress.MSG_NO_REGISTRATION:
progress.dismiss();
activity.showToast(activity.res.getString(R.string.sendToCgeo_no_registration));
activity.finish();
break;
default: // MSG_DONE
adapter.setSelectMode(false);
activity.replaceCacheListFromSearch();
progress.dismiss();
break;
}
}
}
}
private static final class ClearOfflineLogsHandler extends DisposableHandler {
private final WeakReference<CacheListActivity> activityRef;
ClearOfflineLogsHandler(final CacheListActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleRegularMessage(final Message msg) {
final CacheListActivity activity = activityRef.get();
if (activity != null) {
activity.adapter.setSelectMode(false);
activity.refreshCurrentList();
activity.replaceCacheListFromSearch();
activity.progress.dismiss();
}
}
}
private static final class ImportGpxAttachementFinishedHandler extends WeakReferenceHandler<CacheListActivity> {
ImportGpxAttachementFinishedHandler(final CacheListActivity activity) {
super(activity);
}
@Override
public void handleMessage(final Message msg) {
final CacheListActivity activity = getReference();
if (activity != null) {
activity.refreshCurrentList();
}
}
}
public CacheListActivity() {
super(true);
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme();
setContentView(R.layout.cacheslist_activity);
// get parameters
Bundle extras = getIntent().getExtras();
if (extras != null) {
type = Intents.getListType(getIntent());
coords = extras.getParcelable(Intents.EXTRA_COORDS);
} else {
extras = new Bundle();
}
if (isInvokedFromAttachment()) {
type = CacheListType.OFFLINE;
if (coords == null) {
coords = Geopoint.ZERO;
}
}
if (type == CacheListType.NEAREST) {
coords = Sensors.getInstance().currentGeo().getCoords();
}
setTitle(title);
// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
// Restore value of members from saved state
currentFilter = savedInstanceState.getParcelable(STATE_FILTER);
currentInverseSort = savedInstanceState.getBoolean(STATE_INVERSE_SORT);
type = CacheListType.values()[savedInstanceState.getInt(STATE_LIST_TYPE, type.ordinal())];
listId = savedInstanceState.getInt(STATE_LIST_ID);
}
initAdapter();
prepareFilterBar();
if (type.canSwitch) {
initActionBarSpinner();
}
currentLoader = (AbstractSearchLoader) getSupportLoaderManager().initLoader(type.getLoaderId(), extras, this);
// init
if (CollectionUtils.isNotEmpty(cacheList)) {
// currentLoader can be null if this activity is created from a map, as onCreateLoader() will return null.
if (currentLoader != null && currentLoader.isStarted()) {
showFooterLoadingCaches();
} else {
showFooterMoreCaches();
}
}
if (isInvokedFromAttachment()) {
listNameMemento.rememberTerm(extras.getString(Intents.EXTRA_NAME));
importGpxAttachement();
}
}
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
// Save the current Filter
savedInstanceState.putParcelable(STATE_FILTER, currentFilter);
savedInstanceState.putBoolean(STATE_INVERSE_SORT, adapter.getInverseSort());
savedInstanceState.putInt(STATE_LIST_TYPE, type.ordinal());
savedInstanceState.putInt(STATE_LIST_ID, listId);
}
/**
* Action bar spinner adapter. {@code null} for list types that don't allow switching (search results, ...).
*/
CacheListSpinnerAdapter mCacheListSpinnerAdapter;
/**
* remember current filter when switching between lists, so it can be re-applied afterwards
*/
private IFilter currentFilter = null;
private boolean currentInverseSort = false;
private SortActionProvider sortProvider;
private void initActionBarSpinner() {
mCacheListSpinnerAdapter = new CacheListSpinnerAdapter(this, R.layout.support_simple_spinner_dropdown_item);
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setListNavigationCallbacks(mCacheListSpinnerAdapter, new ActionBar.OnNavigationListener() {
@Override
public boolean onNavigationItemSelected(final int i, final long l) {
final int newListId = mCacheListSpinnerAdapter.getItem(i).id;
if (newListId != listId) {
switchListById(newListId);
}
return true;
}
});
}
private void refreshSpinnerAdapter() {
/* If the activity does not use the Spinner this will be null */
if (mCacheListSpinnerAdapter == null) {
return;
}
mCacheListSpinnerAdapter.clear();
final AbstractList list = AbstractList.getListById(listId);
for (final AbstractList l: StoredList.UserInterface.getMenuLists(false, PseudoList.NEW_LIST.id)) {
mCacheListSpinnerAdapter.add(l);
}
getSupportActionBar().setSelectedNavigationItem(mCacheListSpinnerAdapter.getPosition(list));
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (currentLoader != null && currentLoader.isLoading()) {
showFooterLoadingCaches();
}
}
private boolean isConcreteList() {
return type == CacheListType.OFFLINE &&
(listId == StoredList.STANDARD_LIST_ID || listId >= DataStore.customListIdOffset);
}
private boolean isInvokedFromAttachment() {
final Intent intent = getIntent();
return Intent.ACTION_VIEW.equals(intent.getAction()) && intent.getData() != null;
}
private void importGpxAttachement() {
new StoredList.UserInterface(this).promptForListSelection(R.string.gpx_import_select_list_title, new Action1<Integer>() {
@Override
public void call(final Integer listId) {
new GPXImporter(CacheListActivity.this, listId, importGpxAttachementFinishedHandler).importGPX();
switchListById(listId);
}
}, true, 0, listNameMemento);
}
@Override
public void onResume() {
super.onResume();
resumeDisposables.add(geoDirHandler.start(GeoDirHandler.UPDATE_GEODATA | GeoDirHandler.UPDATE_DIRECTION | GeoDirHandler.LOW_POWER, 250, TimeUnit.MILLISECONDS));
adapter.setSelectMode(false);
setAdapterCurrentCoordinates(true);
if (search != null) {
replaceCacheListFromSearch();
loadCachesHandler.sendEmptyMessage(0);
}
// refresh standard list if it has changed (new caches downloaded)
if (type == CacheListType.OFFLINE && (listId >= StoredList.STANDARD_LIST_ID || listId == PseudoList.ALL_LIST.id) && search != null) {
final SearchResult newSearch = DataStore.getBatchOfStoredCaches(coords, Settings.getCacheType(), listId);
if (newSearch.getTotalCountGC() != search.getTotalCountGC()) {
refreshCurrentList();
}
}
// always refresh history, an offline log might have been deleted
if (type == CacheListType.HISTORY) {
new LastPositionHelper(this).refreshListAtLastPosition();
}
}
private void setAdapterCurrentCoordinates(final boolean forceSort) {
adapter.setActualCoordinates(Sensors.getInstance().currentGeo().getCoords());
if (forceSort) {
adapter.forceSort();
}
}
@Override
public void onPause() {
resumeDisposables.clear();
super.onPause();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.cache_list_options, menu);
sortProvider = (SortActionProvider) MenuItemCompat.getActionProvider(menu.findItem(R.id.menu_sort));
assert sortProvider != null; // We set it in the XML file
sortProvider.setSelection(adapter.getCacheComparator());
sortProvider.setIsEventsOnly(adapter.isEventsOnly());
sortProvider.setClickListener(new Action1<CacheComparator>() {
@Override
public void call(final CacheComparator selectedComparator) {
final CacheComparator oldComparator = adapter.getCacheComparator();
// selecting the same sorting twice will toggle the order
if (selectedComparator != null && oldComparator != null && selectedComparator.getClass().equals(oldComparator.getClass())) {
adapter.toggleInverseSort();
} else {
// always reset the inversion for a new sorting criteria
adapter.resetInverseSort();
}
setComparator(selectedComparator);
sortProvider.setSelection(selectedComparator);
}
});
ListNavigationSelectionActionProvider.initialize(menu.findItem(R.id.menu_cache_list_app_provider), new ListNavigationSelectionActionProvider.Callback() {
@Override
public void onListNavigationSelected(final CacheListApp app) {
app.invoke(CacheListAppUtils.filterCoords(cacheList), CacheListActivity.this, getFilteredSearch());
}
});
return true;
}
private static void setVisible(final Menu menu, final int itemId, final boolean visible) {
menu.findItem(itemId).setVisible(visible);
}
@Override
public boolean onPrepareOptionsMenu(final Menu menu) {
super.onPrepareOptionsMenu(menu);
final boolean isHistory = type == CacheListType.HISTORY;
final boolean isOffline = type == CacheListType.OFFLINE;
final boolean isEmpty = cacheList.isEmpty();
final boolean isConcrete = isConcreteList();
try {
if (adapter.isSelectMode()) {
menu.findItem(R.id.menu_switch_select_mode).setTitle(res.getString(R.string.caches_select_mode_exit))
.setIcon(R.drawable.ic_menu_clear_playlist);
} else {
menu.findItem(R.id.menu_switch_select_mode).setTitle(res.getString(R.string.caches_select_mode))
.setIcon(R.drawable.ic_menu_agenda);
}
menu.findItem(R.id.menu_invert_selection).setVisible(adapter.isSelectMode());
setVisible(menu, R.id.menu_show_on_map, !isEmpty);
setVisible(menu, R.id.menu_filter, search != null && search.getCount() > 0);
setVisible(menu, R.id.menu_switch_select_mode, !isEmpty);
setVisible(menu, R.id.menu_create_list, isOffline);
setVisible(menu, R.id.menu_sort, !isEmpty && !isHistory);
setVisible(menu, R.id.menu_refresh_stored, !isEmpty);
setVisible(menu, R.id.menu_drop_caches, !isEmpty && (isHistory || isOffline));
setVisible(menu, R.id.menu_delete_events, isConcrete && !isEmpty && containsPastEvents());
setVisible(menu, R.id.menu_move_to_list, isOffline && !isEmpty);
setVisible(menu, R.id.menu_copy_to_list, isOffline && !isEmpty);
setVisible(menu, R.id.menu_remove_from_history, !isEmpty && isHistory);
setVisible(menu, R.id.menu_clear_offline_logs, !isEmpty && (isHistory || isOffline) && containsOfflineLogs());
setVisible(menu, R.id.menu_import, isOffline);
setVisible(menu, R.id.menu_import_web, isOffline);
setVisible(menu, R.id.menu_import_gpx, isOffline);
setVisible(menu, R.id.menu_export, !isEmpty && (isHistory || isOffline));
setVisible(menu, R.id.menu_make_list_unique, !isEmpty && isOffline && listId != PseudoList.ALL_LIST.id);
if (!isOffline && !isHistory) {
menu.findItem(R.id.menu_refresh_stored).setTitle(R.string.caches_store_offline);
}
final boolean isNonDefaultList = isConcrete && listId != StoredList.STANDARD_LIST_ID;
if (isOffline || type == CacheListType.HISTORY) { // only offline list
setMenuItemLabel(menu, R.id.menu_drop_caches, R.string.caches_remove_selected, R.string.caches_remove_all);
setMenuItemLabel(menu, R.id.menu_refresh_stored, R.string.caches_refresh_selected, R.string.caches_refresh_all);
setMenuItemLabel(menu, R.id.menu_move_to_list, R.string.caches_move_selected, R.string.caches_move_all);
setMenuItemLabel(menu, R.id.menu_copy_to_list, R.string.caches_copy_selected, R.string.caches_copy_all);
} else { // search and global list (all other than offline and history)
setMenuItemLabel(menu, R.id.menu_refresh_stored, R.string.caches_store_selected, R.string.caches_store_offline);
}
menu.findItem(R.id.menu_drop_list).setVisible(isNonDefaultList);
menu.findItem(R.id.menu_rename_list).setVisible(isNonDefaultList);
setMenuItemLabel(menu, R.id.menu_remove_from_history, R.string.cache_remove_from_history, R.string.cache_clear_history);
menu.findItem(R.id.menu_import_android).setVisible(Compatibility.isStorageAccessFrameworkAvailable() && isOffline);
final List<CacheListApp> listNavigationApps = CacheListApps.getActiveApps();
menu.findItem(R.id.menu_cache_list_app_provider).setVisible(!isEmpty && listNavigationApps.size() > 1);
menu.findItem(R.id.menu_cache_list_app).setVisible(!isEmpty && listNavigationApps.size() == 1);
} catch (final RuntimeException e) {
Log.e("CacheListActivity.onPrepareOptionsMenu", e);
}
return true;
}
private boolean containsPastEvents() {
for (final Geocache cache : adapter.getCheckedOrAllCaches()) {
if (CalendarUtils.isPastEvent(cache)) {
return true;
}
}
return false;
}
private boolean containsOfflineLogs() {
for (final Geocache cache : adapter.getCheckedOrAllCaches()) {
if (cache.isLogOffline()) {
return true;
}
}
return false;
}
private void setMenuItemLabel(final Menu menu, final int menuId, @StringRes final int resIdSelection, @StringRes final int resId) {
final MenuItem menuItem = menu.findItem(menuId);
if (menuItem == null) {
return;
}
final boolean hasSelection = adapter != null && adapter.getCheckedCount() > 0;
if (hasSelection) {
menuItem.setTitle(res.getString(resIdSelection) + " (" + adapter.getCheckedCount() + ")");
} else {
menuItem.setTitle(res.getString(resId));
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_show_on_map:
goMap();
return true;
case R.id.menu_switch_select_mode:
adapter.switchSelectMode();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_refresh_stored:
refreshStored(adapter.getCheckedOrAllCaches());
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_drop_caches:
deleteCaches(adapter.getCheckedOrAllCaches());
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_import_gpx:
importGpx();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_import_android:
importGpxFromAndroid();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_create_list:
new StoredList.UserInterface(this).promptForListCreation(getListSwitchingRunnable(), StringUtils.EMPTY);
refreshSpinnerAdapter();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_drop_list:
removeList();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_rename_list:
renameList();
return true;
case R.id.menu_invert_selection:
adapter.invertSelection();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_filter:
showFilterMenu(null);
return true;
case R.id.menu_import_web:
importWeb();
return true;
case R.id.menu_export_gpx:
new GpxExport().export(adapter.getCheckedOrAllCaches(), this);
return true;
case R.id.menu_export_fieldnotes:
new FieldNoteExport().export(adapter.getCheckedOrAllCaches(), this);
return true;
case R.id.menu_export_persnotes:
new PersonalNoteExport().export(adapter.getCheckedOrAllCaches(), this);
return true;
case R.id.menu_remove_from_history:
removeFromHistoryCheck();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_move_to_list:
moveCachesToOtherList(adapter.getCheckedOrAllCaches());
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_copy_to_list:
copyCachesToOtherList(adapter.getCheckedOrAllCaches());
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_delete_events:
deletePastEvents();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_clear_offline_logs:
clearOfflineLogs();
invalidateOptionsMenuCompatible();
return true;
case R.id.menu_cache_list_app:
if (cacheToShow()) {
CacheListApps.getActiveApps().get(0).invoke(CacheListAppUtils.filterCoords(cacheList), this, getFilteredSearch());
}
return true;
case R.id.menu_make_list_unique:
new MakeListUniqueCommand(this, listId) {
@Override
protected void onFinished() {
refreshSpinnerAdapter();
}
@Override
protected void onFinishedUndo() {
refreshSpinnerAdapter();
}
}.execute();
return true;
}
return super.onOptionsItemSelected(item);
}
private void checkIfEmptyAndRemoveAfterConfirm() {
final boolean isNonDefaultList = isConcreteList() && listId != StoredList.STANDARD_LIST_ID;
// Check local cacheList first, and Datastore only if needed (because of filtered lists)
// Checking is done in this order for performance reasons
if (isNonDefaultList && CollectionUtils.isEmpty(cacheList)
&& DataStore.getAllStoredCachesCount(CacheType.ALL, listId) == 0) {
// ask user, if he wants to delete the now empty list
Dialogs.confirmYesNo(this, R.string.list_dialog_remove_title, R.string.list_dialog_remove_nowempty, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int whichButton) {
removeListInternal();
}
});
}
}
private boolean cacheToShow() {
if (search == null || CollectionUtils.isEmpty(cacheList)) {
showToast(res.getString(R.string.warn_no_cache_coord));
return false;
}
return true;
}
private SearchResult getFilteredSearch() {
return new SearchResult(Geocache.getGeocodes(adapter.getFilteredList()));
}
private void deletePastEvents() {
final List<Geocache> deletion = new ArrayList<>();
for (final Geocache cache : adapter.getCheckedOrAllCaches()) {
if (CalendarUtils.isPastEvent(cache)) {
deletion.add(cache);
}
}
deleteCaches(deletion);
}
private void clearOfflineLogs() {
Dialogs.confirmYesNo(this, R.string.caches_clear_offlinelogs, R.string.caches_clear_offlinelogs_message, new OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
progress.show(CacheListActivity.this, null, res.getString(R.string.caches_clear_offlinelogs_progress), true, clearOfflineLogsHandler.disposeMessage());
clearOfflineLogs(clearOfflineLogsHandler, adapter.getCheckedOrAllCaches());
}
});
}
/**
* called from the filter bar view
*/
@Override
public void showFilterMenu(final View view) {
if (view != null && Settings.getCacheType() != CacheType.ALL) {
Dialogs.selectGlobalTypeFilter(this, new Action1<CacheType>() {
@Override
public void call(final CacheType cacheType) {
refreshCurrentList();
prepareFilterBar();
}
});
} else {
FilterActivity.selectFilter(this);
}
}
private void setComparator(final CacheComparator comparator) {
adapter.setComparator(comparator);
currentInverseSort = adapter.getInverseSort();
}
@Override
public void onCreateContextMenu(final ContextMenu menu, final View view, final ContextMenu.ContextMenuInfo info) {
super.onCreateContextMenu(menu, view, info);
AdapterContextMenuInfo adapterInfo = null;
try {
adapterInfo = (AdapterContextMenuInfo) info;
} catch (final Exception e) {
Log.w("CacheListActivity.onCreateContextMenu", e);
}
if (adapterInfo == null || adapterInfo.position >= adapter.getCount()) {
return;
}
final Geocache cache = adapter.getItem(adapterInfo.position);
menu.setHeaderTitle(StringUtils.defaultIfBlank(cache.getName(), cache.getGeocode()));
contextMenuGeocode = cache.getGeocode();
getMenuInflater().inflate(R.menu.cache_list_context, menu);
menu.findItem(R.id.menu_default_navigation).setTitle(NavigationAppFactory.getDefaultNavigationApplication().getName());
final boolean hasCoords = cache.getCoords() != null;
menu.findItem(R.id.menu_default_navigation).setVisible(hasCoords);
menu.findItem(R.id.menu_navigate).setVisible(hasCoords);
menu.findItem(R.id.menu_cache_details).setVisible(hasCoords);
final boolean isOffline = cache.isOffline();
menu.findItem(R.id.menu_drop_cache).setVisible(isOffline);
menu.findItem(R.id.menu_move_to_list).setVisible(isOffline);
menu.findItem(R.id.menu_copy_to_list).setVisible(isOffline);
menu.findItem(R.id.menu_refresh).setVisible(isOffline);
menu.findItem(R.id.menu_store_cache).setVisible(!isOffline);
LoggingUI.onPrepareOptionsMenu(menu, cache);
}
private void moveCachesToOtherList(final Collection<Geocache> caches) {
new MoveToListCommand(this, caches, listId) {
private LastPositionHelper lastPositionHelper;
@Override
protected void doCommand() {
lastPositionHelper = new LastPositionHelper(CacheListActivity.this);
super.doCommand();
}
@Override
protected void onFinished() {
lastPositionHelper.refreshListAtLastPosition();
}
}.execute();
}
private void copyCachesToOtherList(final Collection<Geocache> caches) {
new CopyToListCommand(this, caches, listId) {
@Override
protected void onFinished() {
adapter.setSelectMode(false);
refreshCurrentList(AfterLoadAction.CHECK_IF_EMPTY);
}
}.execute();
}
@Override
public boolean onContextItemSelected(final MenuItem item) {
ContextMenu.ContextMenuInfo info = item.getMenuInfo();
// restore menu info for sub menu items, see
// https://code.google.com/p/android/issues/detail?id=7139
if (info == null) {
info = lastMenuInfo;
lastMenuInfo = null;
}
AdapterContextMenuInfo adapterInfo = null;
try {
adapterInfo = (AdapterContextMenuInfo) info;
} catch (final Exception e) {
Log.w("CacheListActivity.onContextItemSelected", e);
}
final Geocache cache = adapterInfo != null ? getCacheFromAdapter(adapterInfo) : null;
// just in case the list got resorted while we are executing this code
if (cache == null || adapterInfo == null) {
return true;
}
switch (item.getItemId()) {
case R.id.menu_default_navigation:
NavigationAppFactory.startDefaultNavigationApplication(1, this, cache);
break;
case R.id.menu_navigate:
NavigationAppFactory.showNavigationMenu(this, cache, null, null);
break;
case R.id.menu_cache_details:
CacheDetailActivity.startActivity(this, cache.getGeocode(), cache.getName());
break;
case R.id.menu_drop_cache:
deleteCaches(Collections.singletonList(cache));
break;
case R.id.menu_move_to_list:
moveCachesToOtherList(Collections.singletonList(cache));
break;
case R.id.menu_copy_to_list:
copyCachesToOtherList(Collections.singletonList(cache));
break;
case R.id.menu_store_cache:
case R.id.menu_refresh:
refreshStored(Collections.singletonList(cache));
break;
default:
// we must remember the menu info for the sub menu, there is a bug
// in Android:
// https://code.google.com/p/android/issues/detail?id=7139
lastMenuInfo = info;
final View selectedView = adapterInfo.targetView;
LoggingUI.onMenuItemSelected(item, this, cache, new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(final DialogInterface dialog) {
if (selectedView != null) {
final CacheListAdapter.ViewHolder holder = (CacheListAdapter.ViewHolder) selectedView.getTag();
if (holder != null) {
CacheListAdapter.updateViewHolder(holder, cache, res);
}
}
}
});
}
return true;
}
/**
* Extract a cache from adapter data.
*
* @param adapterInfo
* an adapterInfo
* @return the pointed cache
*/
private Geocache getCacheFromAdapter(final AdapterContextMenuInfo adapterInfo) {
final Geocache cache = adapter.getItem(adapterInfo.position);
if (cache.getGeocode().equalsIgnoreCase(contextMenuGeocode)) {
return cache;
}
return adapter.findCacheByGeocode(contextMenuGeocode);
}
private void setFilter(final IFilter filter) {
currentFilter = filter;
adapter.setFilter(filter);
prepareFilterBar();
updateTitle();
invalidateOptionsMenuCompatible();
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && adapter.isSelectMode()) {
adapter.setSelectMode(false);
return true;
}
return super.onKeyDown(keyCode, event);
}
private void initAdapter() {
final ListView listView = getListView();
registerForContextMenu(listView);
adapter = new CacheListAdapter(this, cacheList, type);
adapter.setFilter(currentFilter);
if (listFooter == null) {
listFooter = getLayoutInflater().inflate(R.layout.cacheslist_footer, listView, false);
listFooter.setClickable(true);
listFooter.setOnClickListener(new MoreCachesListener());
listFooterText = ButterKnife.findById(listFooter, R.id.more_caches);
listView.addFooterView(listFooter);
}
setListAdapter(adapter);
adapter.setInverseSort(currentInverseSort);
adapter.forceSort();
}
private void updateAdapter() {
adapter.notifyDataSetChanged();
adapter.reFilter();
adapter.checkSpecialSortOrder();
adapter.forceSort();
}
private void showFooterLoadingCaches() {
// no footer for offline lists
if (listFooter == null) {
return;
}
listFooterText.setText(res.getString(R.string.caches_more_caches_loading));
listFooter.setClickable(false);
listFooter.setOnClickListener(null);
}
private void showFooterMoreCaches() {
// no footer in offline lists
if (listFooter == null) {
return;
}
boolean enableMore = type != CacheListType.OFFLINE && cacheList.size() < MAX_LIST_ITEMS;
if (enableMore && search != null) {
final int count = search.getTotalCountGC();
enableMore = count > 0 && cacheList.size() < count;
}
listFooter.setClickable(enableMore);
if (enableMore) {
listFooterText.setText(res.getString(R.string.caches_more_caches) + " (" + res.getString(R.string.caches_more_caches_currently) + ": " + cacheList.size() + ")");
listFooter.setOnClickListener(new MoreCachesListener());
} else if (type != CacheListType.OFFLINE) {
listFooterText.setText(res.getString(CollectionUtils.isEmpty(cacheList) ? R.string.caches_no_cache : R.string.caches_more_caches_no));
listFooter.setOnClickListener(null);
} else {
// hiding footer for offline list is not possible, it must be removed instead
// http://stackoverflow.com/questions/7576099/hiding-footer-in-listview
getListView().removeFooterView(listFooter);
}
}
private void importGpx() {
GpxFileListActivity.startSubActivity(this, listId, REQUEST_CODE_RESTART);
}
private void importGpxFromAndroid() {
Compatibility.importGpxFromStorageAccessFramework(this, REQUEST_CODE_IMPORT_GPX);
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_IMPORT_GPX && resultCode == Activity.RESULT_OK) {
// The document selected by the user won't be returned in the intent.
// Instead, a URI to that document will be contained in the return intent
// provided to this method as a parameter. Pull that uri using "resultData.getData()"
if (data != null) {
final Uri uri = data.getData();
new GPXImporter(this, listId, importGpxAttachementFinishedHandler).importGPX(uri, null, getDisplayName(uri));
}
} else if (requestCode == FilterActivity.REQUEST_SELECT_FILTER && resultCode == Activity.RESULT_OK) {
final int[] filterIndex = data.getIntArrayExtra(FilterActivity.EXTRA_FILTER_RESULT);
setFilter(FilterActivity.getFilterFromPosition(filterIndex[0], filterIndex[1]));
} else if (requestCode == REQUEST_CODE_RESTART && resultCode == Activity.RESULT_OK) {
restartActivity();
}
if (type == CacheListType.OFFLINE && requestCode != REQUEST_CODE_RESTART) {
refreshCurrentList();
}
}
private String getDisplayName(final Uri uri) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
if (cursor != null) {
cursor.close(); // no Closable Cursor below sdk 16
}
}
return null;
}
public void refreshStored(final List<Geocache> caches) {
if (type == CacheListType.OFFLINE && caches.size() > REFRESH_WARNING_THRESHOLD) {
Dialogs.confirmYesNo(this, R.string.caches_refresh_all, R.string.caches_refresh_all_warning, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
refreshStoredConfirmed(caches);
dialog.cancel();
}
});
} else {
refreshStoredConfirmed(caches);
}
}
private void refreshStoredConfirmed(final List<Geocache> caches) {
detailTotal = caches.size();
if (detailTotal == 0) {
return;
}
if (!Network.isConnected()) {
showToast(getString(R.string.err_server));
return;
}
if (Settings.getChooseList() && type != CacheListType.OFFLINE && type != CacheListType.HISTORY) {
// let user select list to store cache in
new StoredList.UserInterface(this).promptForMultiListSelection(R.string.list_title,
new Action1<Set<Integer>>() {
@Override
public void call(final Set<Integer> selectedListIds) {
refreshStoredInternal(caches, selectedListIds);
}
}, true, Collections.singleton(StoredList.TEMPORARY_LIST.id), Collections.<Integer>emptySet(), listNameMemento, false);
} else {
final Set<Integer> additionalListIds = new HashSet<>();
if (type != CacheListType.OFFLINE && type != CacheListType.HISTORY) {
additionalListIds.add(StoredList.STANDARD_LIST_ID);
}
refreshStoredInternal(caches, additionalListIds);
}
}
private void refreshStoredInternal(final List<Geocache> caches, final Set<Integer> additionalListIds) {
detailProgress.set(0);
showProgress(false);
final int etaTime = detailTotal * 25 / 60;
final String message;
if (etaTime < 1) {
message = res.getString(R.string.caches_downloading) + " " + res.getString(R.string.caches_eta_ltm);
} else {
message = res.getString(R.string.caches_downloading) + " " + res.getQuantityString(R.plurals.caches_eta_mins, etaTime, etaTime);
}
final LoadDetailsHandler loadDetailsHandler = new LoadDetailsHandler(this);
progress.show(this, null, message, ProgressDialog.STYLE_HORIZONTAL, loadDetailsHandler.disposeMessage());
progress.setMaxProgressAndReset(detailTotal);
detailProgressTime = System.currentTimeMillis();
loadDetails(loadDetailsHandler, caches, additionalListIds);
}
public void removeFromHistoryCheck() {
final int message = (adapter != null && adapter.getCheckedCount() > 0) ? R.string.cache_remove_from_history
: R.string.cache_clear_history;
Dialogs.confirmYesNo(this, R.string.caches_removing_from_history, message, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
removeFromHistory();
dialog.cancel();
}
});
}
private void removeFromHistory() {
final List<Geocache> caches = adapter.getCheckedOrAllCaches();
final Collection<String> geocodes = new ArrayList<>(caches.size());
for (final Geocache cache : caches) {
geocodes.add(cache.getGeocode());
}
DataStore.clearVisitDate(geocodes);
DataStore.clearLogsOffline(caches);
refreshCurrentList();
}
private void importWeb() {
// menu is also shown with no device connected
if (!Settings.isRegisteredForSend2cgeo()) {
Dialogs.confirm(this, R.string.web_import_title, R.string.init_sendToCgeo_description, new OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
SettingsActivity.openForScreen(R.string.preference_screen_sendtocgeo, CacheListActivity.this);
}
});
return;
}
detailProgress.set(0);
showProgress(false);
final DownloadFromWebHandler downloadFromWebHandler = new DownloadFromWebHandler(this);
progress.show(this, null, res.getString(R.string.web_import_waiting), true, downloadFromWebHandler.disposeMessage());
Send2CgeoDownloader.loadFromWeb(downloadFromWebHandler, listId);
}
private void deleteCaches(@NonNull final Collection<Geocache> caches) {
new DeleteCachesFromListCommand(this, caches, listId).execute();
}
/**
* Method to asynchronously refresh the caches details.
*/
private void loadDetails(final DisposableHandler handler, final List<Geocache> caches, final Set<Integer> additionalListIds) {
final Observable<Geocache> allCaches;
if (Settings.isStoreOfflineMaps()) {
allCaches = Observable.create(new ObservableOnSubscribe<Geocache>() {
private final Disposable disposable = Disposables.empty();
@Override
public void subscribe(final ObservableEmitter<Geocache> emitter) throws Exception {
emitter.setDisposable(disposable);
final Deque<Geocache> withStaticMaps = new LinkedList<>();
for (final Geocache cache : caches) {
if (disposable.isDisposed()) {
return;
}
if (cache.hasStaticMap()) {
withStaticMaps.push(cache);
} else {
emitter.onNext(cache);
}
}
for (final Geocache cache : withStaticMaps) {
if (disposable.isDisposed()) {
return;
}
emitter.onNext(cache);
}
emitter.onComplete();
}
}).subscribeOn(Schedulers.io());
} else {
allCaches = Observable.fromIterable(caches);
}
final Observable<Geocache> loaded = allCaches.flatMap(new Function<Geocache, Observable<Geocache>>() {
@Override
public Observable<Geocache> apply(final Geocache cache) {
return Observable.create(new ObservableOnSubscribe<Geocache>() {
@Override
public void subscribe(final ObservableEmitter<Geocache> emitter) throws Exception {
cache.refreshSynchronous(null, additionalListIds);
detailProgress.incrementAndGet();
handler.obtainMessage(DownloadProgress.MSG_LOADED, cache).sendToTarget();
emitter.onComplete();
}
}).subscribeOn(AndroidRxUtils.refreshScheduler);
}
}).doOnComplete(new Action() {
@Override
public void run() {
handler.sendEmptyMessage(DownloadProgress.MSG_DONE);
}
});
handler.add(loaded.subscribe());
}
private static final class LastPositionHelper {
private final WeakReference<CacheListActivity> activityRef;
private final int lastListPosition;
LastPositionHelper(@NonNull final CacheListActivity context) {
super();
this.lastListPosition = context.getListView().getFirstVisiblePosition();
this.activityRef = new WeakReference<>(context);
}
public void refreshListAtLastPosition() {
final CacheListActivity activity = activityRef.get();
if (activity != null) {
activity.adapter.setSelectMode(false);
activity.refreshCurrentList(AfterLoadAction.CHECK_IF_EMPTY);
activity.replaceCacheListFromSearch();
if (lastListPosition > 0) {
activity.getListView().setSelection(lastListPosition);
}
}
}
}
private static final class DeleteCachesFromListCommand extends AbstractCachesCommand {
private final LastPositionHelper lastPositionHelper;
private final int listId;
private final Map<String, Set<Integer>> oldCachesLists = new HashMap<>();
DeleteCachesFromListCommand(@NonNull final CacheListActivity context, final Collection<Geocache> caches, final int listId) {
super(context, caches, R.string.command_delete_caches_progress);
this.lastPositionHelper = new LastPositionHelper(context);
this.listId = listId;
}
@Override
public void onFinished() {
lastPositionHelper.refreshListAtLastPosition();
}
@Override
protected void doCommand() {
if (appliesToAllLists()) {
oldCachesLists.putAll(DataStore.markDropped(getCaches()));
} else {
DataStore.removeFromList(getCaches(), listId);
}
}
private boolean appliesToAllLists() {
return listId == PseudoList.ALL_LIST.id || listId == PseudoList.HISTORY_LIST.id || listId == StoredList.TEMPORARY_LIST.id;
}
@Override
protected void undoCommand() {
if (appliesToAllLists()) {
DataStore.addToLists(getCaches(), oldCachesLists);
} else {
DataStore.addToList(getCaches(), listId);
}
}
@Override
@NonNull
protected String getResultMessage() {
final int size = getCaches().size();
return getContext().getResources().getQuantityString(R.plurals.command_delete_caches_result, size, size);
}
}
private static void clearOfflineLogs(final Handler handler, final List<Geocache> selectedCaches) {
Schedulers.io().scheduleDirect(new Runnable() {
@Override
public void run() {
DataStore.clearLogsOffline(selectedCaches);
handler.sendEmptyMessage(DownloadProgress.MSG_DONE);
}
});
}
private class MoreCachesListener implements View.OnClickListener {
@Override
public void onClick(final View arg0) {
showProgress(true);
showFooterLoadingCaches();
getSupportLoaderManager().restartLoader(CacheListLoaderType.NEXT_PAGE.getLoaderId(), null, CacheListActivity.this);
}
}
private void hideLoading() {
final ListView list = getListView();
if (list.getVisibility() == View.GONE) {
list.setVisibility(View.VISIBLE);
final View loading = findViewById(R.id.loading);
loading.setVisibility(View.GONE);
}
}
@NonNull
private Action1<Integer> getListSwitchingRunnable() {
return new Action1<Integer>() {
@Override
public void call(final Integer selectedListId) {
switchListById(selectedListId);
}
};
}
private void switchListById(final int id) {
switchListById(id, AfterLoadAction.NO_ACTION);
}
private void switchListById(final int id, @NonNull final AfterLoadAction action) {
if (id < 0) {
return;
}
final Bundle extras = new Bundle();
extras.putSerializable(BUNDLE_ACTION_KEY, action);
if (id == PseudoList.HISTORY_LIST.id) {
type = CacheListType.HISTORY;
getSupportLoaderManager().destroyLoader(CacheListType.OFFLINE.getLoaderId());
currentLoader = (AbstractSearchLoader) getSupportLoaderManager().restartLoader(CacheListType.HISTORY.getLoaderId(), extras, this);
} else {
if (id == PseudoList.ALL_LIST.id) {
listId = id;
title = res.getString(R.string.list_all_lists);
} else {
final StoredList list = DataStore.getList(id);
listId = list.id;
title = list.title;
}
type = CacheListType.OFFLINE;
getSupportLoaderManager().destroyLoader(CacheListType.HISTORY.getLoaderId());
extras.putAll(OfflineGeocacheListLoader.getBundleForList(listId));
currentLoader = (OfflineGeocacheListLoader) getSupportLoaderManager().restartLoader(CacheListType.OFFLINE.getLoaderId(), extras, this);
Settings.setLastDisplayedList(listId);
}
initAdapter();
showProgress(true);
showFooterLoadingCaches();
adapter.setSelectMode(false);
invalidateOptionsMenuCompatible();
}
private void renameList() {
(new RenameListCommand(this, listId) {
@Override
protected void onFinished() {
refreshCurrentList();
}
}).execute();
}
private void removeListInternal() {
new DeleteListCommand(this, listId) {
private String oldListName;
@Override
protected boolean canExecute() {
oldListName = DataStore.getList(listId).getTitle();
return super.canExecute();
}
@Override
protected void onFinished() {
refreshSpinnerAdapter();
switchListById(StoredList.STANDARD_LIST_ID);
}
@Override
protected void onFinishedUndo() {
refreshSpinnerAdapter();
for (final StoredList list : DataStore.getLists()) {
if (oldListName.equals(list.getTitle())) {
switchListById(list.id);
}
}
}
}.execute();
}
private void removeList() {
// if there are no caches on this list, don't bother the user with questions.
// there is no harm in deleting the list, he could recreate it easily
if (CollectionUtils.isEmpty(cacheList)) {
removeListInternal();
return;
}
// ask him, if there are caches on the list
Dialogs.confirm(this, R.string.list_dialog_remove_title, R.string.list_dialog_remove_description, R.string.list_dialog_remove, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int whichButton) {
removeListInternal();
}
});
}
public void goMap() {
if (!cacheToShow()) {
return;
}
// apply filter settings (if there's a filter)
final SearchResult searchToUse = getFilteredSearch();
DefaultMap.startActivitySearch(this, searchToUse, title);
}
private void refreshCurrentList() {
refreshCurrentList(AfterLoadAction.NO_ACTION);
}
private void refreshCurrentList(@NonNull final AfterLoadAction action) {
// do not refresh any of the dynamic search result lists but history, which might have been cleared
if (type != CacheListType.OFFLINE && type != CacheListType.HISTORY) {
return;
}
refreshSpinnerAdapter();
switchListById(listId, action);
}
public static void startActivityOffline(final Context context) {
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(cachesIntent, CacheListType.OFFLINE);
context.startActivity(cachesIntent);
}
public static void startActivityOwner(final Activity context, final String userName) {
if (!checkNonBlankUsername(context, userName)) {
return;
}
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(cachesIntent, CacheListType.OWNER);
cachesIntent.putExtra(Intents.EXTRA_USERNAME, userName);
context.startActivity(cachesIntent);
}
/**
* Check if a given username is valid (non blank), and show a toast if it isn't.
*
* @param context an activity
* @param username the username to check
* @return <tt>true</tt> if the username is not blank, <tt>false</tt> otherwise
*/
private static boolean checkNonBlankUsername(final Activity context, final String username) {
if (StringUtils.isBlank(username)) {
ActivityMixin.showToast(context, R.string.warn_no_username);
return false;
}
return true;
}
public static void startActivityFinder(final Activity context, final String userName) {
if (!checkNonBlankUsername(context, userName)) {
return;
}
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(cachesIntent, CacheListType.FINDER);
cachesIntent.putExtra(Intents.EXTRA_USERNAME, userName);
context.startActivity(cachesIntent);
}
private void prepareFilterBar() {
final List<String> filterNames = getFilterNames();
if (filterNames.isEmpty()) {
findViewById(R.id.filter_bar).setVisibility(View.GONE);
} else {
final TextView filterTextView = ButterKnife.findById(this, R.id.filter_text);
filterTextView.setText(TextUtils.join(", ", filterNames));
findViewById(R.id.filter_bar).setVisibility(View.VISIBLE);
}
}
@NonNull
private List<String> getFilterNames() {
final List<String> filters = new ArrayList<>();
if (Settings.getCacheType() != CacheType.ALL) {
filters.add(Settings.getCacheType().getL10n());
}
if (adapter.isFiltered()) {
filters.add(adapter.getFilterName());
}
return filters;
}
public static Intent getNearestIntent(final Activity context) {
return Intents.putListType(new Intent(context, CacheListActivity.class), CacheListType.NEAREST);
}
public static Intent getHistoryIntent(final Context context) {
return Intents.putListType(new Intent(context, CacheListActivity.class), CacheListType.HISTORY);
}
public static void startActivityAddress(final Context context, final Geopoint coords, final String address) {
final Intent addressIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(addressIntent, CacheListType.ADDRESS);
addressIntent.putExtra(Intents.EXTRA_COORDS, coords);
addressIntent.putExtra(Intents.EXTRA_ADDRESS, address);
context.startActivity(addressIntent);
}
/**
* start list activity, by searching around the given point.
*
* @param name
* name of coordinates, will lead to a title like "Around ..." instead of directly showing the
* coordinates as title
*/
public static void startActivityCoordinates(final AbstractActivity context, final Geopoint coords, @Nullable final String name) {
if (!isValidCoords(context, coords)) {
return;
}
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(cachesIntent, CacheListType.COORDINATE);
cachesIntent.putExtra(Intents.EXTRA_COORDS, coords);
if (StringUtils.isNotEmpty(name)) {
cachesIntent.putExtra(Intents.EXTRA_TITLE, context.getString(R.string.around, name));
}
context.startActivity(cachesIntent);
}
private static boolean isValidCoords(final AbstractActivity context, final Geopoint coords) {
if (coords == null) {
context.showToast(CgeoApplication.getInstance().getString(R.string.warn_no_coordinates));
return false;
}
return true;
}
public static void startActivityKeyword(final AbstractActivity context, final String keyword) {
if (keyword == null) {
context.showToast(CgeoApplication.getInstance().getString(R.string.warn_no_keyword));
return;
}
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(cachesIntent, CacheListType.KEYWORD);
cachesIntent.putExtra(Intents.EXTRA_KEYWORD, keyword);
context.startActivity(cachesIntent);
}
public static void startActivityMap(final Context context, final SearchResult search) {
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
cachesIntent.putExtra(Intents.EXTRA_SEARCH, search);
Intents.putListType(cachesIntent, CacheListType.MAP);
context.startActivity(cachesIntent);
}
public static void startActivityPocketDownload(@NonNull final Context context, @NonNull final PocketQuery pocketQuery) {
final String guid = pocketQuery.getGuid();
if (guid == null) {
ActivityMixin.showToast(context, CgeoApplication.getInstance().getString(R.string.warn_pocket_query_select));
return;
}
startActivityWithAttachment(context, pocketQuery);
}
public static void startActivityPocket(@NonNull final Context context, @NonNull final PocketQuery pocketQuery) {
final String guid = pocketQuery.getGuid();
if (guid == null) {
ActivityMixin.showToast(context, CgeoApplication.getInstance().getString(R.string.warn_pocket_query_select));
return;
}
startActivityPocket(context, pocketQuery, CacheListType.POCKET);
}
private static void startActivityWithAttachment(@NonNull final Context context, @NonNull final PocketQuery pocketQuery) {
final Uri uri = Uri.parse("https://www.geocaching.com/pocket/downloadpq.ashx?g=" + pocketQuery.getGuid() + "&src=web");
final Intent cachesIntent = new Intent(Intent.ACTION_VIEW, uri, context, CacheListActivity.class);
cachesIntent.setDataAndType(uri, "application/zip");
cachesIntent.putExtra(Intents.EXTRA_NAME, pocketQuery.getName());
context.startActivity(cachesIntent);
}
private static void startActivityPocket(@NonNull final Context context, @NonNull final PocketQuery pocketQuery, final CacheListType cacheListType) {
final Intent cachesIntent = new Intent(context, CacheListActivity.class);
Intents.putListType(cachesIntent, cacheListType);
cachesIntent.putExtra(Intents.EXTRA_NAME, pocketQuery.getName());
cachesIntent.putExtra(Intents.EXTRA_POCKET_GUID, pocketQuery.getGuid());
context.startActivity(cachesIntent);
}
// Loaders
@Override
public Loader<SearchResult> onCreateLoader(final int type, final Bundle extras) {
if (type >= CacheListLoaderType.values().length) {
throw new IllegalArgumentException("invalid loader type " + type);
}
final CacheListLoaderType enumType = CacheListLoaderType.values()[type];
AbstractSearchLoader loader = null;
switch (enumType) {
case OFFLINE:
// open either the requested or the last list
if (extras.containsKey(Intents.EXTRA_LIST_ID)) {
listId = extras.getInt(Intents.EXTRA_LIST_ID);
} else {
listId = Settings.getLastDisplayedList();
}
if (listId == PseudoList.ALL_LIST.id) {
title = res.getString(R.string.list_all_lists);
} else if (listId <= StoredList.TEMPORARY_LIST.id) {
listId = StoredList.STANDARD_LIST_ID;
title = res.getString(R.string.stored_caches_button);
} else {
final StoredList list = DataStore.getList(listId);
// list.id may be different if listId was not valid
if (list.id != listId) {
showToast(getString(R.string.list_not_available));
}
listId = list.id;
title = list.title;
}
loader = new OfflineGeocacheListLoader(this, coords, listId);
break;
case HISTORY:
title = res.getString(R.string.caches_history);
listId = PseudoList.HISTORY_LIST.id;
loader = new HistoryGeocacheListLoader(this, coords);
break;
case NEAREST:
title = res.getString(R.string.caches_nearby);
loader = new CoordsGeocacheListLoader(this, coords);
break;
case COORDINATE:
title = coords.toString();
loader = new CoordsGeocacheListLoader(this, coords);
break;
case KEYWORD:
final String keyword = extras.getString(Intents.EXTRA_KEYWORD);
title = listNameMemento.rememberTerm(keyword);
if (keyword != null) {
loader = new KeywordGeocacheListLoader(this, keyword);
}
break;
case ADDRESS:
final String address = extras.getString(Intents.EXTRA_ADDRESS);
if (StringUtils.isNotBlank(address)) {
title = listNameMemento.rememberTerm(address);
} else {
title = coords.toString();
}
loader = new CoordsGeocacheListLoader(this, coords);
break;
case FINDER:
final String username = extras.getString(Intents.EXTRA_USERNAME);
title = listNameMemento.rememberTerm(username);
if (username != null) {
loader = new FinderGeocacheListLoader(this, username);
}
break;
case OWNER:
final String ownerName = extras.getString(Intents.EXTRA_USERNAME);
title = listNameMemento.rememberTerm(ownerName);
if (ownerName != null) {
loader = new OwnerGeocacheListLoader(this, ownerName);
}
break;
case MAP:
//TODO Build Null loader
title = res.getString(R.string.map_map);
search = (SearchResult) extras.get(Intents.EXTRA_SEARCH);
replaceCacheListFromSearch();
loadCachesHandler.sendMessage(Message.obtain());
break;
case NEXT_PAGE:
loader = new NextPageGeocacheListLoader(this, search);
break;
case POCKET:
final String guid = extras.getString(Intents.EXTRA_POCKET_GUID);
title = listNameMemento.rememberTerm(extras.getString(Intents.EXTRA_NAME));
loader = new PocketGeocacheListLoader(this, guid);
break;
}
// if there is a title given in the activity start request, use this one instead of the default
if (extras != null && StringUtils.isNotBlank(extras.getString(Intents.EXTRA_TITLE))) {
title = extras.getString(Intents.EXTRA_TITLE);
}
if (loader != null && extras != null && extras.getSerializable(BUNDLE_ACTION_KEY) != null) {
final AfterLoadAction action = (AfterLoadAction) extras.getSerializable(BUNDLE_ACTION_KEY);
loader.setAfterLoadAction(action);
}
updateTitle();
showProgress(true);
showFooterLoadingCaches();
return loader;
}
@Override
public void onLoadFinished(final Loader<SearchResult> arg0, final SearchResult searchIn) {
// The database search was moved into the UI call intentionally. If this is done before the runOnUIThread,
// then we have 2 sets of caches in memory. This can lead to OOM for huge cache lists.
if (searchIn != null) {
cacheList.clear();
final Set<Geocache> cachesFromSearchResult = searchIn.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB);
cacheList.addAll(cachesFromSearchResult);
search = searchIn;
updateAdapter();
updateTitle();
showFooterMoreCaches();
}
showProgress(false);
hideLoading();
invalidateOptionsMenuCompatible();
if (arg0 instanceof AbstractSearchLoader) {
switch (((AbstractSearchLoader) arg0).getAfterLoadAction()) {
case CHECK_IF_EMPTY:
checkIfEmptyAndRemoveAfterConfirm();
break;
case NO_ACTION:
break;
}
}
}
@Override
public void onLoaderReset(final Loader<SearchResult> arg0) {
//Not interesting
}
/**
* Allow the title bar spinner to show the same subtitle like the activity itself would show.
*
*/
public CharSequence getCacheListSubtitle(@NonNull final AbstractList list) {
// if this is the current list, be aware of filtering
if (list.id == listId) {
return getCurrentSubtitle();
}
// otherwise return the overall number
final int numberOfCaches = list.getNumberOfCaches();
if (numberOfCaches < 0) {
return StringUtils.EMPTY;
}
return getCacheNumberString(getResources(), numberOfCaches);
}
/**
* Calculate the subtitle of the current list depending on (optional) filters.
*
*/
private CharSequence getCurrentSubtitle() {
if (search == null) {
return getCacheNumberString(getResources(), 0);
}
final StringBuilder result = new StringBuilder();
if (adapter.isFiltered()) {
result.append(adapter.getCount()).append('/');
}
result.append(getCacheNumberString(getResources(), search.getCount()));
return result.toString();
}
/**
* Used to indicate if an action should be taken after the AbstractSearchLoader has finished
*/
public enum AfterLoadAction {
/** Take no action */
NO_ACTION,
/** Check if the list is empty and prompt for deletion */
CHECK_IF_EMPTY
}
}