package cgeo.geocaching; import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.activity.AbstractViewPagerActivity; import cgeo.geocaching.activity.INavigationSource; import cgeo.geocaching.activity.Progress; import cgeo.geocaching.apps.cachelist.MapsMeCacheListApp; import cgeo.geocaching.apps.navi.NavigationAppFactory; import cgeo.geocaching.apps.navi.NavigationSelectionActionProvider; import cgeo.geocaching.calendar.CalendarAdder; import cgeo.geocaching.command.MoveToListAndRemoveFromOthersCommand; import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.capability.IgnoreCapability; import cgeo.geocaching.connector.capability.PersonalNoteCapability; import cgeo.geocaching.connector.capability.PgcChallengeCheckerCapability; import cgeo.geocaching.connector.capability.WatchListCapability; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.GCConstants; import cgeo.geocaching.connector.trackable.TrackableBrand; import cgeo.geocaching.connector.trackable.TrackableConnector; import cgeo.geocaching.enumerations.CacheAttribute; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.export.FieldNoteExport; import cgeo.geocaching.export.GpxExport; import cgeo.geocaching.export.PersonalNoteExport; import cgeo.geocaching.gcvote.GCVote; import cgeo.geocaching.gcvote.GCVoteDialog; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.location.GeopointFormatter; import cgeo.geocaching.location.Units; import cgeo.geocaching.log.CacheLogsViewCreator; import cgeo.geocaching.log.LoggingUI; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Trackable; import cgeo.geocaching.models.Waypoint; import cgeo.geocaching.network.AndroidBeam; import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.SmileyImage; import cgeo.geocaching.playservices.AppInvite; import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.staticmaps.StaticMapsProvider; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.ui.AbstractCachingPageViewCreator; import cgeo.geocaching.ui.AnchorAwareLinkMovementMethod; import cgeo.geocaching.ui.CacheDetailsCreator; import cgeo.geocaching.ui.CoordinatesFormatSwitcher; import cgeo.geocaching.ui.DecryptTextClickListener; import cgeo.geocaching.ui.EditNoteDialog; import cgeo.geocaching.ui.EditNoteDialog.EditNoteDialogListener; import cgeo.geocaching.ui.ImagesList; import cgeo.geocaching.ui.IndexOutOfBoundsAvoidingTextView; import cgeo.geocaching.ui.NavigationActionProvider; import cgeo.geocaching.ui.OwnerActionsClickListener; import cgeo.geocaching.ui.TrackableListAdapter; import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.ui.recyclerview.RecyclerViewProvider; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.CheckerUtils; import cgeo.geocaching.utils.ClipboardUtils; import cgeo.geocaching.utils.CryptUtils; import cgeo.geocaching.utils.DisposableHandler; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; import cgeo.geocaching.utils.SimpleDisposableHandler; import cgeo.geocaching.utils.SimpleHandler; import cgeo.geocaching.utils.TextUtils; import cgeo.geocaching.utils.UnknownTagsHandler; import cgeo.geocaching.utils.functions.Action1; import android.R.color; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.MenuItemCompat; import android.support.v7.view.ActionMode; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.style.StyleSpan; import android.text.util.Linkify; import android.util.TypedValue; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.GridView; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.TextView; import android.widget.TextView.BufferType; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.Callable; import java.util.regex.Pattern; import butterknife.BindView; import butterknife.ButterKnife; import io.reactivex.Maybe; import io.reactivex.MaybeEmitter; import io.reactivex.MaybeOnSubscribe; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; /** * Activity to handle all single-cache-stuff. * * e.g. details, description, logs, waypoints, inventory... */ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailActivity.Page> implements CacheMenuHandler.ActivityInterface, INavigationSource, AndroidBeam.ActivitySharingInterface, EditNoteDialogListener { private static final int MESSAGE_FAILED = -1; private static final int MESSAGE_SUCCEEDED = 1; private static final Pattern[] DARK_COLOR_PATTERNS = { Pattern.compile("((?<!bg)color)=\"#" + "(0[0-9]){3}" + "\"", Pattern.CASE_INSENSITIVE), Pattern.compile("((?<!bg)color)=\"" + "black" + "\"", Pattern.CASE_INSENSITIVE), Pattern.compile("((?<!bg)color)=\"#" + "000080" + "\"", Pattern.CASE_INSENSITIVE) }; private static final Pattern[] LIGHT_COLOR_PATTERNS = { Pattern.compile("((?<!bg)color)=\"#" + "([F][6-9A-F]){3}" + "\"", Pattern.CASE_INSENSITIVE), Pattern.compile("((?<!bg)color)=\"" + "white" + "\"", Pattern.CASE_INSENSITIVE) }; public static final String STATE_PAGE_INDEX = "cgeo.geocaching.pageIndex"; // Store Geocode here, as 'cache' is loaded Async. private String geocode; private Geocache cache; @NonNull private final List<Trackable> genericTrackables = new ArrayList<>(); private final Progress progress = new Progress(); private SearchResult search; private GeoDirHandler locationUpdater; private CharSequence clickedItemText = null; /** * If another activity is called and can modify the data of this activity, we refresh it on resume. */ private boolean refreshOnResume = false; // some views that must be available from everywhere // TODO: Reference can block GC? private TextView cacheDistanceView; protected ImagesList imagesList; private final CompositeDisposable createDisposables = new CompositeDisposable(); /** * waypoint selected in context menu. This variable will be gone when the waypoint context menu is a fragment. */ private Waypoint selectedWaypoint; private boolean requireGeodata; private final CompositeDisposable geoDataDisposable = new CompositeDisposable(); private final EnumSet<TrackableBrand> processedBrands = EnumSet.noneOf(TrackableBrand.class); @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.cachedetail_activity); // get parameters final Bundle extras = getIntent().getExtras(); final Uri uri = AndroidBeam.getUri(getIntent()); // try to get data from extras String name = null; String guid = null; if (extras != null) { geocode = extras.getString(Intents.EXTRA_GEOCODE); name = extras.getString(Intents.EXTRA_NAME); guid = extras.getString(Intents.EXTRA_GUID); } // When clicking a cache in MapsWithMe, we get back a PendingIntent if (StringUtils.isEmpty(geocode)) { geocode = MapsMeCacheListApp.getCacheFromMapsWithMe(this, getIntent()); } if (geocode == null && uri != null) { geocode = ConnectorFactory.getGeocodeFromURL(uri.toString()); } // try to get data from URI if (geocode == null && guid == null && uri != null) { final String uriHost = uri.getHost().toLowerCase(Locale.US); final String uriPath = uri.getPath().toLowerCase(Locale.US); final String uriQuery = uri.getQuery(); if (uriQuery != null) { Log.i("Opening URI: " + uriHost + uriPath + "?" + uriQuery); } else { Log.i("Opening URI: " + uriHost + uriPath); } if (uriHost.contains("geocaching.com")) { if (StringUtils.startsWith(uriPath, "/geocache/gc")) { geocode = StringUtils.substringBefore(uriPath.substring(10), "_").toUpperCase(Locale.US); } else { geocode = uri.getQueryParameter("wp"); guid = uri.getQueryParameter("guid"); if (StringUtils.isNotBlank(geocode)) { geocode = geocode.toUpperCase(Locale.US); guid = null; } else if (StringUtils.isNotBlank(guid)) { geocode = null; guid = guid.toLowerCase(Locale.US); } else { showToast(res.getString(R.string.err_detail_open)); finish(); return; } } } } // no given data if (geocode == null && guid == null) { showToast(res.getString(R.string.err_detail_cache)); finish(); return; } // If we open this cache from a search, let's properly initialize the title bar, even if we don't have cache details setCacheTitleBar(geocode, name, null); final LoadCacheHandler loadCacheHandler = new LoadCacheHandler(this, progress); try { String title = res.getString(R.string.cache); if (StringUtils.isNotBlank(name)) { title = name; } else if (geocode != null && StringUtils.isNotBlank(geocode)) { // can't be null, but the compiler doesn't understand StringUtils.isNotBlank() title = geocode; } progress.show(this, title, res.getString(R.string.cache_dialog_loading_details), true, loadCacheHandler.disposeMessage()); } catch (final RuntimeException ignored) { // nothing, we lost the window } final int pageToOpen = savedInstanceState != null ? savedInstanceState.getInt(STATE_PAGE_INDEX, 0) : Settings.isOpenLastDetailsPage() ? Settings.getLastDetailsPage() : 1; createViewPager(pageToOpen, new OnPageSelectedListener() { @Override public void onPageSelected(final int position) { if (Settings.isOpenLastDetailsPage()) { Settings.setLastDetailsPage(position); } // lazy loading of cache images if (getPage(position) == Page.IMAGES) { loadCacheImages(); } requireGeodata = getPage(position) == Page.DETAILS; startOrStopGeoDataListener(false); // dispose contextual actions on page change if (currentActionMode != null) { currentActionMode.finish(); } } }); requireGeodata = pageToOpen == 1; final String realGeocode = geocode; final String realGuid = guid; AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { search = Geocache.searchByGeocode(realGeocode, StringUtils.isBlank(realGeocode) ? realGuid : null, false, loadCacheHandler); loadCacheHandler.sendMessage(Message.obtain()); } }); // Load Generic Trackables if (StringUtils.isNotBlank(geocode)) { AndroidRxUtils.bindActivity(this, // Obtain the active connectors and load trackables in parallel. Observable.fromIterable(ConnectorFactory.getGenericTrackablesConnectors()).flatMap(new Function<TrackableConnector, Observable<Trackable>>() { @Override public Observable<Trackable> apply(final TrackableConnector trackableConnector) { processedBrands.add(trackableConnector.getBrand()); return Observable.defer(new Callable<Observable<Trackable>>() { @Override public Observable<Trackable> call() { return Observable.fromIterable(trackableConnector.searchTrackables(geocode)); } }).subscribeOn(AndroidRxUtils.networkScheduler); } }).toList()).subscribe(new Consumer<List<Trackable>>() { @Override public void accept(final List<Trackable> trackables) { // Todo: this is not really a good method, it may lead to duplicates ; ie: in OC connectors. // Store trackables. genericTrackables.addAll(trackables); if (!trackables.isEmpty()) { // Update the UI if any trackables were found. notifyDataSetChanged(); } } }); } locationUpdater = new CacheDetailsGeoDirHandler(this); // If we have a newer Android device setup Android Beam for easy cache sharing AndroidBeam.enable(this, this); } @Override @Nullable public String getAndroidBeamUri() { return cache != null ? cache.getCgeoUrl() : null; } @Override public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(STATE_PAGE_INDEX, getCurrentItem()); } private void startOrStopGeoDataListener(final boolean initial) { final boolean start; if (Settings.useLowPowerMode()) { geoDataDisposable.clear(); start = requireGeodata; } else { start = initial; } if (start) { geoDataDisposable.add(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA)); } } @Override public void onStart() { super.onStart(); LocalBroadcastManager.getInstance(this).registerReceiver(updateReceiver, new IntentFilter(Intents.INTENT_CACHE_CHANGED)); } @Override public void onResume() { super.onResume(); startOrStopGeoDataListener(true); if (refreshOnResume) { notifyDataSetChanged(); refreshOnResume = false; } } @Override public void onPause() { geoDataDisposable.clear(); super.onPause(); } @Override public void onDestroy() { createDisposables.clear(); super.onDestroy(); } @Override public void onStop() { if (cache != null) { cache.setChangeNotificationHandler(null); } LocalBroadcastManager.getInstance(this).unregisterReceiver(updateReceiver); super.onStop(); } @Override public void onCreateContextMenu(final ContextMenu menu, final View view, final ContextMenu.ContextMenuInfo info) { super.onCreateContextMenu(menu, view, info); final int viewId = view.getId(); switch (viewId) { case R.id.waypoint: menu.setHeaderTitle(selectedWaypoint.getName() + " (" + res.getString(R.string.waypoint) + ")"); getMenuInflater().inflate(R.menu.waypoint_options, menu); final boolean isOriginalWaypoint = selectedWaypoint.getWaypointType() == WaypointType.ORIGINAL; menu.findItem(R.id.menu_waypoint_reset_cache_coords).setVisible(isOriginalWaypoint); menu.findItem(R.id.menu_waypoint_edit).setVisible(!isOriginalWaypoint); menu.findItem(R.id.menu_waypoint_duplicate).setVisible(!isOriginalWaypoint); menu.findItem(R.id.menu_waypoint_delete).setVisible(!isOriginalWaypoint); final boolean hasCoords = selectedWaypoint.getCoords() != null; final MenuItem defaultNavigationMenu = menu.findItem(R.id.menu_waypoint_navigate_default); defaultNavigationMenu.setVisible(hasCoords); defaultNavigationMenu.setTitle(NavigationAppFactory.getDefaultNavigationApplication().getName()); menu.findItem(R.id.menu_waypoint_navigate).setVisible(hasCoords); menu.findItem(R.id.menu_waypoint_caches_around).setVisible(hasCoords); menu.findItem(R.id.menu_waypoint_copy_coordinates).setVisible(hasCoords); break; default: if (imagesList != null) { imagesList.onCreateContextMenu(menu, view); } break; } } @Override public boolean onContextItemSelected(final MenuItem item) { switch (item.getItemId()) { // waypoints case R.id.menu_waypoint_edit: if (selectedWaypoint != null) { ensureSaved(); EditWaypointActivity.startActivityEditWaypoint(this, cache, selectedWaypoint.getId()); refreshOnResume = true; } return true; case R.id.menu_waypoint_visited: if (selectedWaypoint != null) { ensureSaved(); new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(final Void... params) { selectedWaypoint.setVisited(true); DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); return true; } @Override protected void onPostExecute(final Boolean result) { if (result) { notifyDataSetChanged(); } } }.execute(); } return true; case R.id.menu_waypoint_copy_coordinates: if (selectedWaypoint != null) { final Geopoint coordinates = selectedWaypoint.getCoords(); if (coordinates != null) { ClipboardUtils.copyToClipboard( GeopointFormatter.reformatForClipboard(coordinates.toString())); showToast(getString(R.string.clipboard_copy_ok)); } } return true; case R.id.menu_waypoint_duplicate: ensureSaved(); new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(final Void... params) { if (cache.duplicateWaypoint(selectedWaypoint) != null) { DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); return true; } return false; } @Override protected void onPostExecute(final Boolean result) { if (result) { notifyDataSetChanged(); } } }.execute(); return true; case R.id.menu_waypoint_delete: ensureSaved(); new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(final Void... params) { if (cache.deleteWaypoint(selectedWaypoint)) { DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); return true; } return false; } @Override protected void onPostExecute(final Boolean result) { if (result) { notifyDataSetChanged(); } } }.execute(); return true; case R.id.menu_waypoint_navigate_default: if (selectedWaypoint != null) { NavigationAppFactory.startDefaultNavigationApplication(1, this, selectedWaypoint); } return true; case R.id.menu_waypoint_navigate: if (selectedWaypoint != null) { NavigationAppFactory.showNavigationMenu(this, null, selectedWaypoint, null); } return true; case R.id.menu_waypoint_caches_around: if (selectedWaypoint != null) { final Geopoint coordinates = selectedWaypoint.getCoords(); if (coordinates != null) { CacheListActivity.startActivityCoordinates(this, coordinates, selectedWaypoint.getName()); } } return true; case R.id.menu_waypoint_reset_cache_coords: ensureSaved(); if (ConnectorFactory.getConnector(cache).supportsOwnCoordinates()) { createResetCacheCoordinatesDialog(selectedWaypoint).show(); } else { final ProgressDialog progressDialog = ProgressDialog.show(this, getString(R.string.cache), getString(R.string.waypoint_reset), true); final HandlerResetCoordinates handler = new HandlerResetCoordinates(this, progressDialog, false); resetCoords(cache, handler, selectedWaypoint, true, false, progressDialog); } return true; case R.id.menu_calendar: CalendarAdder.addToCalendar(this, cache); return true; default: break; } if (imagesList != null && imagesList.onContextItemSelected(item)) { return true; } return onOptionsItemSelected(item); } @Override public boolean onCreateOptionsMenu(final Menu menu) { CacheMenuHandler.addMenuItems(this, menu, cache); final MenuItem menuItem = menu.findItem(R.id.menu_default_navigation); final NavigationActionProvider navAction = (NavigationActionProvider) MenuItemCompat.getActionProvider(menuItem); if (navAction != null) { navAction.setNavigationSource(this); } NavigationSelectionActionProvider.initialize(menu.findItem(R.id.menu_navigate), cache); return true; } @Override public boolean onPrepareOptionsMenu(final Menu menu) { CacheMenuHandler.onPrepareOptionsMenu(menu, cache); LoggingUI.onPrepareOptionsMenu(menu, cache); menu.findItem(R.id.menu_edit_fieldnote).setVisible(true); menu.findItem(R.id.menu_store_in_list).setVisible(cache != null); menu.findItem(R.id.menu_delete).setVisible(cache != null && cache.isOffline()); menu.findItem(R.id.menu_refresh).setVisible(cache != null && cache.supportsRefresh()); menu.findItem(R.id.menu_gcvote).setVisible(cache != null && GCVote.isVotingPossible(cache)); menu.findItem(R.id.menu_checker).setVisible(cache != null && StringUtils.isNotEmpty(CheckerUtils.getCheckerUrl(cache))); menu.findItem(R.id.menu_app_invite).setVisible(cache != null && AppInvite.isAvailable()); menu.findItem(R.id.menu_extract_waypoints).setVisible(cache != null); menu.findItem(R.id.menu_export).setVisible(cache != null); if (cache != null) { final IConnector connector = ConnectorFactory.getConnector(cache); if (connector instanceof IgnoreCapability) { menu.findItem(R.id.menu_ignore).setVisible(((IgnoreCapability) connector).canIgnoreCache(cache)); } if (connector instanceof PgcChallengeCheckerCapability) { menu.findItem(R.id.menu_challenge_checker).setVisible(((PgcChallengeCheckerCapability) connector).isChallengeCache(cache)); } } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (CacheMenuHandler.onMenuItemSelected(item, this, cache)) { return true; } final int menuItem = item.getItemId(); switch (menuItem) { case R.id.menu_delete: dropCache(); return true; case R.id.menu_store_in_list: storeCache(false); return true; case R.id.menu_refresh: refreshCache(); return true; case R.id.menu_gcvote: showVoteDialog(); return true; case R.id.menu_checker: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(CheckerUtils.getCheckerUrl(cache)))); return true; case R.id.menu_challenge_checker: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://project-gc.com/Challenges/" + cache.getGeocode()))); return true; case R.id.menu_ignore: ignoreCache(); return true; case R.id.menu_extract_waypoints: extractWaypoints(cache.getDescription(), cache); return true; case R.id.menu_export_gpx: new GpxExport().export(Collections.singletonList(cache), this); return true; case R.id.menu_export_fieldnotes: new FieldNoteExport().export(Collections.singletonList(cache), this); return true; case R.id.menu_export_persnotes: new PersonalNoteExport().export(Collections.singletonList(cache), this); return true; case R.id.menu_edit_fieldnote: ensureSaved(); editPersonalNote(cache, this); return true; case R.id.menu_navigate: if (NavigationAppFactory.onMenuItemSelected(item, this, cache)) { return true; } break; case R.id.menu_app_invite: if (AppInvite.isAvailable()) { AppInvite.send(this, cache); } return true; default: if (LoggingUI.onMenuItemSelected(item, this, cache, null)) { refreshOnResume = true; return true; } } return super.onOptionsItemSelected(item); } private void ignoreCache() { Dialogs.confirm(this, R.string.ignore_confirm_title, R.string.ignore_confirm_message, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { ((IgnoreCapability) ConnectorFactory.getConnector(cache)).ignoreCache(cache); } }); // For consistency, remove also the local cache immediately from memory cache and database if (cache.isOffline()) { dropCache(); DataStore.removeCache(cache.getGeocode(), EnumSet.of(RemoveFlag.DB)); } } }); } private void showVoteDialog() { GCVoteDialog.show(this, cache, new Runnable() { @Override public void run() { notifyDataSetChanged(); } }); } private static final class CacheDetailsGeoDirHandler extends GeoDirHandler { private final WeakReference<CacheDetailActivity> activityRef; CacheDetailsGeoDirHandler(final CacheDetailActivity activity) { this.activityRef = new WeakReference<>(activity); } @Override public void updateGeoData(final GeoData geo) { final CacheDetailActivity activity = activityRef.get(); if (activity == null) { return; } if (activity.cacheDistanceView == null) { return; } if (activity.cache != null && activity.cache.getCoords() != null) { activity.cacheDistanceView.setText(Units.getDistanceFromKilometers(geo.getCoords().distanceTo(activity.cache.getCoords()))); activity.cacheDistanceView.bringToFront(); } } } private static final class LoadCacheHandler extends SimpleDisposableHandler { LoadCacheHandler(final CacheDetailActivity activity, final Progress progress) { super(activity, progress); } @Override public void handleRegularMessage(final Message msg) { if (msg.what == UPDATE_LOAD_PROGRESS_DETAIL && msg.obj instanceof String) { updateStatusMsg((String) msg.obj); } else { final CacheDetailActivity activity = (CacheDetailActivity) activityRef.get(); if (activity == null) { return; } if (activity.search == null) { showToast(R.string.err_dwld_details_failed); dismissProgress(); finishActivity(); return; } if (activity.search.getError() != StatusCode.NO_ERROR) { // Cache not found is not a download error final StatusCode error = activity.search.getError(); final Resources res = activity.getResources(); final String toastPrefix = error != StatusCode.CACHE_NOT_FOUND ? res.getString(R.string.err_dwld_details_failed) + " " : ""; activity.showToast(toastPrefix + error.getErrorString(res)); dismissProgress(); finishActivity(); return; } updateStatusMsg(activity.getString(R.string.cache_dialog_loading_details_status_render)); // Data loaded, we're ready to show it! activity.notifyDataSetChanged(); } } private void updateStatusMsg(final String msg) { final CacheDetailActivity activity = (CacheDetailActivity) activityRef.get(); if (activity == null) { return; } setProgressMessage(activity.getString(R.string.cache_dialog_loading_details) + "\n\n" + msg); } @Override public void handleDispose() { finishActivity(); } } private void notifyDataSetChanged() { // This might get called asynchronous when the activity is shut down if (isFinishing()) { return; } if (search == null) { return; } cache = search.getFirstCacheFromResult(LoadFlags.LOAD_ALL_DB_ONLY); if (cache == null) { progress.dismiss(); showToast(res.getString(R.string.err_detail_cache_find_some)); finish(); return; } // allow cache to notify CacheDetailActivity when it changes so it can be reloaded cache.setChangeNotificationHandler(new ChangeNotificationHandler(this, progress)); setCacheTitleBar(cache); // reset imagesList so Images view page will be redrawn imagesList = null; reinitializeViewPager(); // rendering done! remove progress popup if any there invalidateOptionsMenuCompatible(); progress.dismiss(); Settings.addCacheToHistory(cache.getGeocode()); } /** * Receives update notifications from asynchronous processes */ private final BroadcastReceiver updateReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { notifyDataSetChanged(); } }; /** * Tries to navigate to the {@link Geocache} of this activity using the default navigation tool. */ @Override public void startDefaultNavigation() { NavigationAppFactory.startDefaultNavigationApplication(1, this, cache); } /** * Tries to navigate to the {@link Geocache} of this activity using the second default navigation tool. */ @Override public void startDefaultNavigation2() { NavigationAppFactory.startDefaultNavigationApplication(2, this, cache); } /** * Wrapper for the referenced method in the xml-layout. */ public void goDefaultNavigation(@SuppressWarnings("unused") final View view) { startDefaultNavigation(); } /** * referenced from XML view */ public void showNavigationMenu(@SuppressWarnings("unused") final View view) { NavigationAppFactory.showNavigationMenu(this, cache, null, null, true, true); } private void loadCacheImages() { if (imagesList != null) { return; } final PageViewCreator creator = getViewCreator(Page.IMAGES); if (creator == null) { return; } final View imageView = creator.getView(null); if (imageView == null) { return; } imagesList = new ImagesList(this, cache.getGeocode(), cache); createDisposables.add(imagesList.loadImages(imageView, cache.getNonStaticImages())); } public static void startActivity(final Context context, final String geocode) { final Intent detailIntent = new Intent(context, CacheDetailActivity.class); detailIntent.putExtra(Intents.EXTRA_GEOCODE, geocode); context.startActivity(detailIntent); } /** * Enum of all possible pages with methods to get the view and a title. */ public enum Page { DETAILS(R.string.detail), DESCRIPTION(R.string.cache_description), LOGS(R.string.cache_logs), LOGSFRIENDS(R.string.cache_logs_friends_and_own), WAYPOINTS(R.string.cache_waypoints), INVENTORY(R.string.cache_inventory), IMAGES(R.string.cache_images); private final int titleStringId; Page(final int titleStringId) { this.titleStringId = titleStringId; } } private void refreshCache() { if (progress.isShowing()) { showToast(res.getString(R.string.err_detail_still_working)); return; } if (!Network.isConnected()) { showToast(getString(R.string.err_server)); return; } final RefreshCacheHandler refreshCacheHandler = new RefreshCacheHandler(this, progress); progress.show(this, res.getString(R.string.cache_dialog_refresh_title), res.getString(R.string.cache_dialog_refresh_message), true, refreshCacheHandler.disposeMessage()); cache.refresh(refreshCacheHandler, AndroidRxUtils.refreshScheduler); } private void dropCache() { if (progress.isShowing()) { showToast(res.getString(R.string.err_detail_still_working)); return; } progress.show(this, res.getString(R.string.cache_dialog_offline_drop_title), res.getString(R.string.cache_dialog_offline_drop_message), true, null); cache.drop(new ChangeNotificationHandler(this, progress)); } private void storeCache(final boolean fastStoreOnLastSelection) { if (progress.isShowing()) { showToast(res.getString(R.string.err_detail_still_working)); return; } if (Settings.getChooseList() || cache.isOffline()) { // let user select list to store cache in new StoredList.UserInterface(this).promptForMultiListSelection(R.string.lists_title, new Action1<Set<Integer>>() { @Override public void call(final Set<Integer> selectedListIds) { storeCacheInLists(selectedListIds); } }, true, cache.getLists(), fastStoreOnLastSelection); } else { storeCacheInLists(Collections.singleton(StoredList.STANDARD_LIST_ID)); } } private void moveCache() { if (progress.isShowing()) { showToast(res.getString(R.string.err_detail_still_working)); return; } new MoveToListAndRemoveFromOthersCommand(CacheDetailActivity.this, cache) { @Override protected void onFinished() { updateCacheLists(ButterKnife.findById(CacheDetailActivity.this, R.id.offline_lists), cache, res); } }.execute(); } private void storeCacheInLists(final Set<Integer> selectedListIds) { if (cache.isOffline()) { // cache already offline, just add to another list DataStore.saveLists(Collections.singletonList(cache), selectedListIds); new StoreCacheHandler(CacheDetailActivity.this, progress).sendEmptyMessage(DisposableHandler.DONE); } else { storeCache(selectedListIds); } } private static final class CheckboxHandler extends SimpleDisposableHandler { private final WeakReference<DetailsViewCreator> creatorRef; CheckboxHandler(final DetailsViewCreator creator, final CacheDetailActivity activity, final Progress progress) { super(activity, progress); creatorRef = new WeakReference<>(creator); } @Override public void handleRegularMessage(final Message message) { final DetailsViewCreator creator = creatorRef.get(); if (creator != null) { super.handleRegularMessage(message); creator.updateWatchlistBox(); creator.updateFavPointBox(); } } } /** * Creator for details-view. */ public class DetailsViewCreator extends AbstractCachingPageViewCreator<ScrollView> { // Reference to the details list and favorite line, so that the helper-method can access them without an additional argument private LinearLayout detailsList; private ImmutablePair<RelativeLayout, TextView> favoriteLine; @Override public ScrollView getDispatchedView(final ViewGroup parentView) { if (cache == null) { // something is really wrong return null; } view = (ScrollView) getLayoutInflater().inflate(R.layout.cachedetail_details_page, parentView, false); // Start loading preview map AndroidRxUtils.bindActivity(CacheDetailActivity.this, previewMap).subscribeOn(AndroidRxUtils.networkScheduler) .subscribe(new Consumer<BitmapDrawable>() { @Override public void accept(final BitmapDrawable image) { final Bitmap bitmap = image.getBitmap(); if (bitmap != null && bitmap.getWidth() > 10) { final ImageView imageView = ButterKnife.findById(view, R.id.map_preview); imageView.setImageDrawable(image); view.findViewById(R.id.map_preview_box).setVisibility(View.VISIBLE); } } }); detailsList = ButterKnife.findById(view, R.id.details_list); final CacheDetailsCreator details = new CacheDetailsCreator(CacheDetailActivity.this, detailsList); // cache name (full name) final SpannableString span = TextUtils.coloredCacheText(cache, cache.getName()); addContextMenu(details.add(R.string.cache_name, span).right); details.add(R.string.cache_type, cache.getType().getL10n()); details.addSize(cache); addContextMenu(details.add(R.string.cache_geocode, cache.getGeocode()).right); details.addCacheState(cache); cacheDistanceView = details.addDistance(cache, cacheDistanceView); details.addDifficulty(cache); details.addTerrain(cache); details.addRating(cache); // favorite count favoriteLine = details.add(R.string.cache_favorite, ""); // own rating if (cache.getMyVote() > 0) { details.addStars(R.string.cache_own_rating, cache.getMyVote()); } // cache author if (StringUtils.isNotBlank(cache.getOwnerDisplayName()) || StringUtils.isNotBlank(cache.getOwnerUserId())) { final TextView ownerView = details.add(R.string.cache_owner, "").right; if (StringUtils.isNotBlank(cache.getOwnerDisplayName())) { ownerView.setText(cache.getOwnerDisplayName(), TextView.BufferType.SPANNABLE); } else { // OwnerReal guaranteed to be not blank based on above ownerView.setText(cache.getOwnerUserId(), TextView.BufferType.SPANNABLE); } ownerView.setOnClickListener(new OwnerActionsClickListener(cache)); } // hidden or event date final TextView hiddenView = details.addHiddenDate(cache); if (hiddenView != null) { addContextMenu(hiddenView); } // cache location if (StringUtils.isNotBlank(cache.getLocation())) { details.add(R.string.cache_location, cache.getLocation()); } // cache coordinates if (cache.getCoords() != null) { final TextView valueView = details.add(R.string.cache_coordinates, cache.getCoords().toString()).right; valueView.setOnClickListener(new CoordinatesFormatSwitcher(cache.getCoords())); addContextMenu(valueView); } // cache attributes updateAttributesIcons(); updateAttributesText(); ButterKnife.findById(view, R.id.attributes_box).setVisibility(cache.getAttributes().isEmpty() ? View.GONE : View.VISIBLE); updateOfflineBox(view, cache, res, new RefreshCacheClickListener(), new DropCacheClickListener(), new StoreCacheClickListener(), new MoveCacheClickListener(), new StoreCacheClickListener()); // list updateCacheLists(view, cache, res); // watchlist final ImageButton buttonWatchlistAdd = ButterKnife.findById(view, R.id.add_to_watchlist); final ImageButton buttonWatchlistRemove = ButterKnife.findById(view, R.id.remove_from_watchlist); buttonWatchlistAdd.setOnClickListener(new AddToWatchlistClickListener()); buttonWatchlistRemove.setOnClickListener(new RemoveFromWatchlistClickListener()); updateWatchlistBox(); // favorite points final ImageButton buttonFavPointAdd = ButterKnife.findById(view, R.id.add_to_favpoint); final ImageButton buttonFavPointRemove = ButterKnife.findById(view, R.id.remove_from_favpoint); buttonFavPointAdd.setOnClickListener(new FavoriteAddClickListener()); buttonFavPointRemove.setOnClickListener(new FavoriteRemoveClickListener()); updateFavPointBox(); // data license final IConnector connector = ConnectorFactory.getConnector(cache); final String license = connector.getLicenseText(cache); if (StringUtils.isNotBlank(license)) { view.findViewById(R.id.license_box).setVisibility(View.VISIBLE); final TextView licenseView = ButterKnife.findById(view, R.id.license); licenseView.setText(Html.fromHtml(license), BufferType.SPANNABLE); licenseView.setClickable(true); licenseView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); } else { view.findViewById(R.id.license_box).setVisibility(View.GONE); } return view; } private void updateAttributesIcons() { final GridView gridView = ButterKnife.findById(view, R.id.attributes_grid); final List<String> attributes = cache.getAttributes(); if (!CacheAttribute.hasRecognizedAttributeIcon(attributes)) { gridView.setVisibility(View.GONE); return; } gridView.setAdapter(new AttributesGridAdapter(CacheDetailActivity.this, cache)); gridView.setVisibility(View.VISIBLE); gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) { toggleAttributesView(); } }); } protected void toggleAttributesView() { final View textView = ButterKnife.findById(view, R.id.attributes_text); textView.setVisibility(textView.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); final View gridView = ButterKnife.findById(view, R.id.attributes_grid); gridView.setVisibility(gridView.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); } private void updateAttributesText() { final TextView attribView = ButterKnife.findById(view, R.id.attributes_text); final List<String> attributes = cache.getAttributes(); if (attributes.isEmpty()) { attribView.setVisibility(View.GONE); return; } final StringBuilder text = new StringBuilder(); for (final String attributeName : attributes) { final boolean enabled = CacheAttribute.isEnabled(attributeName); // search for a translation of the attribute final CacheAttribute attrib = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attributeName)); String attributeNameL10n = attributeName; if (attrib != null) { attributeNameL10n = attrib.getL10n(enabled); } if (text.length() > 0) { text.append('\n'); } text.append(attributeNameL10n); } attribView.setText(text); if (ButterKnife.findById(view, R.id.attributes_grid).getVisibility() == View.VISIBLE) { attribView.setVisibility(View.GONE); attribView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { toggleAttributesView(); } }); } else { attribView.setVisibility(View.VISIBLE); } } private class StoreCacheClickListener implements View.OnClickListener, View.OnLongClickListener { @Override public void onClick(final View arg0) { storeCache(false); } @Override public boolean onLongClick(final View v) { storeCache(true); return true; } } private class MoveCacheClickListener implements OnLongClickListener { @Override public boolean onLongClick(final View v) { moveCache(); return true; } } private class DropCacheClickListener implements View.OnClickListener { @Override public void onClick(final View arg0) { dropCache(); } } private class RefreshCacheClickListener implements View.OnClickListener { @Override public void onClick(final View arg0) { refreshCache(); } } /** * Abstract Listener for add / remove buttons for watchlist */ private abstract class AbstractPropertyListener implements View.OnClickListener { private final SimpleDisposableHandler handler = new CheckboxHandler(DetailsViewCreator.this, CacheDetailActivity.this, progress); public void doExecute(final int titleId, final int messageId, final Action1<SimpleDisposableHandler> action) { if (progress.isShowing()) { showToast(res.getString(R.string.err_watchlist_still_managing)); return; } progress.show(CacheDetailActivity.this, res.getString(titleId), res.getString(messageId), true, null); AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { action.call(handler); } }); } } /** * Listener for "add to watchlist" button */ private class AddToWatchlistClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_watchlist_add_title, R.string.cache_dialog_watchlist_add_message, new Action1<SimpleDisposableHandler>() { @Override public void call(final SimpleDisposableHandler simpleCancellableHandler) { watchListAdd(simpleCancellableHandler); } }); } } /** * Listener for "remove from watchlist" button */ private class RemoveFromWatchlistClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_watchlist_remove_title, R.string.cache_dialog_watchlist_remove_message, new Action1<SimpleDisposableHandler>() { @Override public void call(final SimpleDisposableHandler simpleCancellableHandler) { watchListRemove(simpleCancellableHandler); } }); } } /** Add this cache to the watchlist of the user */ private void watchListAdd(final SimpleDisposableHandler handler) { final WatchListCapability connector = (WatchListCapability) ConnectorFactory.getConnector(cache); if (connector.addToWatchlist(cache)) { handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); } else { handler.sendTextMessage(MESSAGE_FAILED, R.string.err_watchlist_failed); } } /** Remove this cache from the watchlist of the user */ private void watchListRemove(final SimpleDisposableHandler handler) { final WatchListCapability connector = (WatchListCapability) ConnectorFactory.getConnector(cache); if (connector.removeFromWatchlist(cache)) { handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); } else { handler.sendTextMessage(MESSAGE_FAILED, R.string.err_watchlist_failed); } } /** Add this cache to the favorite list of the user */ private void favoriteAdd(final SimpleDisposableHandler handler) { if (GCConnector.addToFavorites(cache)) { handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); } else { handler.sendTextMessage(MESSAGE_FAILED, R.string.err_favorite_failed); } } /** Remove this cache to the favorite list of the user */ private void favoriteRemove(final SimpleDisposableHandler handler) { if (GCConnector.removeFromFavorites(cache)) { handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); } else { handler.sendTextMessage(MESSAGE_FAILED, R.string.err_favorite_failed); } } /** * Listener for "add to favorites" button */ private class FavoriteAddClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_favorite_add_title, R.string.cache_dialog_favorite_add_message, new Action1<SimpleDisposableHandler>() { @Override public void call(final SimpleDisposableHandler simpleCancellableHandler) { favoriteAdd(simpleCancellableHandler); } }); } } /** * Listener for "remove from favorites" button */ private class FavoriteRemoveClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_favorite_remove_title, R.string.cache_dialog_favorite_remove_message, new Action1<SimpleDisposableHandler>() { @Override public void call(final SimpleDisposableHandler simpleCancellableHandler) { favoriteRemove(simpleCancellableHandler); } }); } } /** * Show/hide buttons, set text in watchlist box */ private void updateWatchlistBox() { final LinearLayout layout = ButterKnife.findById(view, R.id.watchlist_box); final boolean supportsWatchList = cache.supportsWatchList(); layout.setVisibility(supportsWatchList ? View.VISIBLE : View.GONE); if (!supportsWatchList) { return; } final ImageButton buttonAdd = ButterKnife.findById(view, R.id.add_to_watchlist); final ImageButton buttonRemove = ButterKnife.findById(view, R.id.remove_from_watchlist); final TextView text = ButterKnife.findById(view, R.id.watchlist_text); final int watchListCount = cache.getWatchlistCount(); if (cache.isOnWatchlist() || cache.isOwner()) { buttonAdd.setVisibility(View.GONE); buttonRemove.setVisibility(View.VISIBLE); if (watchListCount != -1) { text.setText(res.getString(R.string.cache_watchlist_on_extra, watchListCount)); } else { text.setText(R.string.cache_watchlist_on); } } else { buttonAdd.setVisibility(View.VISIBLE); buttonRemove.setVisibility(View.GONE); if (watchListCount != -1) { text.setText(res.getString(R.string.cache_watchlist_not_on_extra, watchListCount)); } else { text.setText(R.string.cache_watchlist_not_on); } } // the owner of a cache has it always on his watchlist. Adding causes an error if (cache.isOwner()) { buttonAdd.setEnabled(false); buttonAdd.setVisibility(View.GONE); buttonRemove.setEnabled(false); buttonRemove.setVisibility(View.GONE); } } /** * Show/hide buttons, set text in favorite line and box */ private void updateFavPointBox() { // Favorite counts if (cache.getFavoritePoints() > 0) { favoriteLine.left.setVisibility(View.VISIBLE); favoriteLine.right.setText(cache.getFavoritePoints() + "×"); } else { favoriteLine.left.setVisibility(View.GONE); } final LinearLayout layout = ButterKnife.findById(view, R.id.favpoint_box); final boolean supportsFavoritePoints = cache.supportsFavoritePoints(); layout.setVisibility(supportsFavoritePoints ? View.VISIBLE : View.GONE); if (!supportsFavoritePoints || cache.isOwner() || !Settings.isGCPremiumMember()) { return; } final ImageButton buttonAdd = ButterKnife.findById(view, R.id.add_to_favpoint); final ImageButton buttonRemove = ButterKnife.findById(view, R.id.remove_from_favpoint); final TextView text = ButterKnife.findById(view, R.id.favpoint_text); if (cache.isFavorite()) { buttonAdd.setVisibility(View.GONE); buttonRemove.setVisibility(View.VISIBLE); text.setText(R.string.cache_favpoint_on); } else { buttonAdd.setVisibility(View.VISIBLE); buttonRemove.setVisibility(View.GONE); text.setText(R.string.cache_favpoint_not_on); } // Add/remove to Favorites is only possible if the cache has been found if (!cache.isFound()) { buttonAdd.setEnabled(false); buttonAdd.setVisibility(View.GONE); buttonRemove.setEnabled(false); buttonRemove.setVisibility(View.GONE); } } } private final Maybe<BitmapDrawable> previewMap = Maybe.create(new MaybeOnSubscribe<BitmapDrawable>() { @Override public void subscribe(final MaybeEmitter<BitmapDrawable> emitter) throws Exception { try { // persistent preview from storage Bitmap image = StaticMapsProvider.getPreviewMap(cache); if (image == null && Settings.isStoreOfflineMaps() && cache.getCoords() != null) { StaticMapsProvider.storeCachePreviewMap(cache).blockingAwait(); image = StaticMapsProvider.getPreviewMap(cache); } if (image != null) { emitter.onSuccess(ImageUtils.scaleBitmapToFitDisplay(image)); } else { emitter.onComplete(); } } catch (final Exception e) { Log.w("CacheDetailActivity.previewMap", e); emitter.onError(e); } } }); /** * Reflect the (contextual) action mode of the action bar. */ protected ActionMode currentActionMode; protected class DescriptionViewCreator extends AbstractCachingPageViewCreator<ScrollView> { @BindView(R.id.personalnote) protected TextView personalNoteView; @BindView(R.id.description) protected IndexOutOfBoundsAvoidingTextView descView; @BindView(R.id.loading) protected View loadingView; @Override public ScrollView getDispatchedView(final ViewGroup parentView) { if (cache == null) { // something is really wrong return null; } view = (ScrollView) getLayoutInflater().inflate(R.layout.cachedetail_description_page, parentView, false); ButterKnife.bind(this, view); // cache short description if (StringUtils.isNotBlank(cache.getShortDescription())) { loadDescription(cache.getShortDescription(), descView, null); } // long description if (StringUtils.isNotBlank(cache.getDescription())) { loadLongDescription(); } // cache personal note setPersonalNote(personalNoteView, cache.getPersonalNote()); personalNoteView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); addContextMenu(personalNoteView); final Button personalNoteEdit = ButterKnife.findById(view, R.id.edit_personalnote); personalNoteEdit.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { ensureSaved(); editPersonalNote(cache, CacheDetailActivity.this); } }); final Button personalNoteUpload = ButterKnife.findById(view, R.id.upload_personalnote); final PersonalNoteCapability connector = ConnectorFactory.getConnectorAs(cache, PersonalNoteCapability.class); if (connector != null && connector.canAddPersonalNote(cache)) { personalNoteUpload.setVisibility(View.VISIBLE); personalNoteUpload.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { if (StringUtils.length(cache.getPersonalNote()) > GCConstants.PERSONAL_NOTE_MAX_CHARS) { warnPersonalNoteExceedsLimit(); } else { uploadPersonalNote(); } } }); } else { personalNoteUpload.setVisibility(View.GONE); } // cache hint and spoiler images final View hintBoxView = view.findViewById(R.id.hint_box); if (StringUtils.isNotBlank(cache.getHint()) || CollectionUtils.isNotEmpty(cache.getSpoilers())) { hintBoxView.setVisibility(View.VISIBLE); } else { hintBoxView.setVisibility(View.GONE); } final TextView hintView = ButterKnife.findById(view, R.id.hint); if (StringUtils.isNotBlank(cache.getHint())) { if (TextUtils.containsHtml(cache.getHint())) { hintView.setText(Html.fromHtml(cache.getHint(), new HtmlImage(cache.getGeocode(), false, false, false), null), TextView.BufferType.SPANNABLE); hintView.setText(CryptUtils.rot13((Spannable) hintView.getText())); } else { hintView.setText(CryptUtils.rot13(cache.getHint())); } hintView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); hintView.setVisibility(View.VISIBLE); hintView.setClickable(true); hintView.setOnClickListener(new DecryptTextClickListener(hintView)); hintBoxView.setOnClickListener(new DecryptTextClickListener(hintView)); hintBoxView.setClickable(true); addContextMenu(hintView); } else { hintView.setVisibility(View.GONE); hintView.setClickable(false); hintView.setOnClickListener(null); hintBoxView.setClickable(false); hintBoxView.setOnClickListener(null); } final TextView spoilerlinkView = ButterKnife.findById(view, R.id.hint_spoilerlink); if (CollectionUtils.isNotEmpty(cache.getSpoilers())) { spoilerlinkView.setVisibility(View.VISIBLE); spoilerlinkView.setClickable(true); spoilerlinkView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View arg0) { if (cache == null || CollectionUtils.isEmpty(cache.getSpoilers())) { showToast(res.getString(R.string.err_detail_no_spoiler)); return; } ImagesActivity.startActivity(CacheDetailActivity.this, cache.getGeocode(), cache.getSpoilers()); } }); } else { spoilerlinkView.setVisibility(View.GONE); spoilerlinkView.setClickable(true); spoilerlinkView.setOnClickListener(null); } return view; } private void uploadPersonalNote() { final SimpleDisposableHandler myHandler = new SimpleDisposableHandler(CacheDetailActivity.this, progress); final Message cancelMessage = myHandler.cancelMessage(res.getString(R.string.cache_personal_note_upload_cancelled)); progress.show(CacheDetailActivity.this, res.getString(R.string.cache_personal_note_uploading), res.getString(R.string.cache_personal_note_uploading), true, cancelMessage); myHandler.add(AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { final PersonalNoteCapability connector = (PersonalNoteCapability) ConnectorFactory.getConnector(cache); final boolean success = connector.uploadPersonalNote(cache); final Message msg = Message.obtain(); final Bundle bundle = new Bundle(); bundle.putString(SimpleDisposableHandler.MESSAGE_TEXT, CgeoApplication.getInstance().getString(success ? R.string.cache_personal_note_upload_done : R.string.cache_personal_note_upload_error)); msg.setData(bundle); myHandler.sendMessage(msg); } })); } private void loadLongDescription() { loadingView.setVisibility(View.VISIBLE); final String longDescription = cache.getDescription(); loadDescription(longDescription, descView, loadingView); } private void warnPersonalNoteExceedsLimit() { Dialogs.confirm(CacheDetailActivity.this, R.string.cache_personal_note_limit, getString(R.string.cache_personal_note_truncation, GCConstants.PERSONAL_NOTE_MAX_CHARS), new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { dialog.dismiss(); uploadPersonalNote(); } }); } /** * Load the description in the background. * * @param descriptionString * the HTML description as retrieved from the connector * @param descriptionView * the view to fill * @param loadingIndicatorView * the loading indicator view, will be hidden when completed */ private void loadDescription(final String descriptionString, final IndexOutOfBoundsAvoidingTextView descriptionView, final View loadingIndicatorView) { try { final UnknownTagsHandler unknownTagsHandler = new UnknownTagsHandler(); final Spanned description = Html.fromHtml(descriptionString, new HtmlImage(cache.getGeocode(), true, false, descriptionView, false), unknownTagsHandler); addWarning(unknownTagsHandler, description); if (StringUtils.isNotBlank(descriptionString)) { try { if (descriptionView.getText().length() == 0) { descriptionView.setText(description, TextView.BufferType.SPANNABLE); } else { descriptionView.append("\n"); descriptionView.append(description); } } catch (final Exception e) { // On 4.1, there is sometimes a crash on measuring the layout: https://code.google.com/p/android/issues/detail?id=35412 Log.e("Android bug setting text: ", e); // remove the formatting by converting to a simple string descriptionView.append(description.toString()); } descriptionView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); fixTextColor(descriptionString, descriptionView); descriptionView.setVisibility(View.VISIBLE); addContextMenu(descriptionView); potentiallyHideShortDescription(); } if (loadingIndicatorView != null) { loadingIndicatorView.setVisibility(View.GONE); } } catch (final RuntimeException ignored) { showToast(res.getString(R.string.err_load_descr_failed)); } } } // If description has an HTML construct which may be problematic to render, add a note at the end of the long description. // Technically, it may not be a table, but a pre, which has the same problems as a table, so the message is ok even though // sometimes technically incorrect. private void addWarning(final UnknownTagsHandler unknownTagsHandler, final Spanned description) { if (unknownTagsHandler.isProblematicDetected()) { final int startPos = description.length(); final IConnector connector = ConnectorFactory.getConnector(cache); if (StringUtils.isNotEmpty(cache.getUrl())) { final Spanned tableNote = Html.fromHtml(res.getString(R.string.cache_description_table_note, "<a href=\"" + cache.getUrl() + "\">" + connector.getName() + "</a>")); ((Editable) description).append("\n\n").append(tableNote); ((Editable) description).setSpan(new StyleSpan(Typeface.ITALIC), startPos, description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static void fixTextColor(final String descriptionString, final IndexOutOfBoundsAvoidingTextView descriptionView) { final int backcolor; if (Settings.isLightSkin()) { backcolor = color.white; for (final Pattern pattern : LIGHT_COLOR_PATTERNS) { final MatcherWrapper matcher = new MatcherWrapper(pattern, descriptionString); if (matcher.find()) { descriptionView.setBackgroundResource(color.darker_gray); return; } } } else { backcolor = color.black; for (final Pattern pattern : DARK_COLOR_PATTERNS) { final MatcherWrapper matcher = new MatcherWrapper(pattern, descriptionString); if (matcher.find()) { descriptionView.setBackgroundResource(color.darker_gray); return; } } } descriptionView.setBackgroundResource(backcolor); } /** * Hide the short description, if it is contained somewhere at the start of the long description. */ public void potentiallyHideShortDescription() { final View shortView = ButterKnife.findById(this, R.id.description); if (shortView == null) { return; } if (shortView.getVisibility() == View.GONE) { return; } final String shortDescription = cache.getShortDescription(); if (StringUtils.isNotBlank(shortDescription)) { final int index = StringUtils.indexOf(cache.getDescription(), shortDescription); // allow up to 200 characters of HTML formatting if (index >= 0 && index < 200) { shortView.setVisibility(View.GONE); } } } private void ensureSaved() { if (!cache.isOffline()) { showToast(getString(R.string.info_cache_saved)); cache.getLists().add(StoredList.STANDARD_LIST_ID); new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... params) { DataStore.saveCache(cache, LoadFlags.SAVE_ALL); return null; } }.execute(); notifyDataSetChanged(); } } private class WaypointsViewCreator extends AbstractCachingPageViewCreator<ListView> { private final int visitedInset = (int) (6.6f * CgeoApplication.getInstance().getResources().getDisplayMetrics().density + 0.5f); @Override public ListView getDispatchedView(final ViewGroup parentView) { if (cache == null) { // something is really wrong return null; } // sort waypoints: PP, Sx, FI, OWN final List<Waypoint> sortedWaypoints = new ArrayList<>(cache.getWaypoints()); Collections.sort(sortedWaypoints, Waypoint.WAYPOINT_COMPARATOR); view = (ListView) getLayoutInflater().inflate(R.layout.cachedetail_waypoints_page, parentView, false); view.setClickable(true); final View addWaypointButton = getLayoutInflater().inflate(R.layout.cachedetail_waypoints_header, view, false); view.addHeaderView(addWaypointButton); addWaypointButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { ensureSaved(); EditWaypointActivity.startActivityAddWaypoint(CacheDetailActivity.this, cache); refreshOnResume = true; } }); view.setAdapter(new ArrayAdapter<Waypoint>(CacheDetailActivity.this, R.layout.waypoint_item, sortedWaypoints) { @Override public View getView(final int position, final View convertView, final ViewGroup parent) { View rowView = convertView; if (rowView == null) { rowView = getLayoutInflater().inflate(R.layout.waypoint_item, parent, false); rowView.setClickable(true); rowView.setLongClickable(true); registerForContextMenu(rowView); } WaypointViewHolder holder = (WaypointViewHolder) rowView.getTag(); if (holder == null) { holder = new WaypointViewHolder(rowView); } final Waypoint waypoint = getItem(position); fillViewHolder(rowView, holder, waypoint); return rowView; } }); return view; } protected void fillViewHolder(final View rowView, final WaypointViewHolder holder, final Waypoint wpt) { // coordinates final TextView coordinatesView = holder.coordinatesView; final Geopoint coordinates = wpt.getCoords(); if (coordinates != null) { coordinatesView.setOnClickListener(new CoordinatesFormatSwitcher(coordinates)); coordinatesView.setText(coordinates.toString()); coordinatesView.setVisibility(View.VISIBLE); } else { coordinatesView.setVisibility(View.GONE); } // info final String waypointInfo = Formatter.formatWaypointInfo(wpt); final TextView infoView = holder.infoView; if (StringUtils.isNotBlank(waypointInfo)) { infoView.setText(waypointInfo); infoView.setVisibility(View.VISIBLE); } else { infoView.setVisibility(View.GONE); } // title final TextView nameView = holder.nameView; if (StringUtils.isNotBlank(wpt.getName())) { nameView.setText(StringEscapeUtils.unescapeHtml4(wpt.getName())); } else if (coordinates != null) { nameView.setText(coordinates.toString()); } else { nameView.setText(res.getString(R.string.waypoint)); } setWaypointIcon(nameView, wpt); // visited if (wpt.isVisited()) { final TypedValue typedValue = new TypedValue(); getTheme().resolveAttribute(R.attr.text_color_grey, typedValue, true); if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { // really should be just a color! nameView.setTextColor(typedValue.data); } } // note final TextView noteView = holder.noteView; if (StringUtils.isNotBlank(wpt.getNote())) { noteView.setOnClickListener(new DecryptTextClickListener(noteView)); noteView.setVisibility(View.VISIBLE); if (TextUtils.containsHtml(wpt.getNote())) { noteView.setText(Html.fromHtml(wpt.getNote(), new SmileyImage(cache.getGeocode(), noteView), new UnknownTagsHandler()), TextView.BufferType.SPANNABLE); } else { noteView.setText(wpt.getNote()); } } else { noteView.setVisibility(View.GONE); } // user note final TextView userNoteView = holder.userNoteView; if (StringUtils.isNotBlank(wpt.getUserNote()) && !StringUtils.equals(wpt.getNote(), wpt.getUserNote())) { userNoteView.setOnClickListener(new DecryptTextClickListener(userNoteView)); userNoteView.setVisibility(View.VISIBLE); if (TextUtils.containsHtml(wpt.getUserNote())) { userNoteView.setText(Html.fromHtml(wpt.getUserNote(), new SmileyImage(cache.getGeocode(), userNoteView), new UnknownTagsHandler()), TextView.BufferType.SPANNABLE); } else { userNoteView.setText(wpt.getUserNote()); } } else { userNoteView.setVisibility(View.GONE); } final View wpNavView = holder.wpNavView; wpNavView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { NavigationAppFactory.startDefaultNavigationApplication(1, CacheDetailActivity.this, wpt); } }); wpNavView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { NavigationAppFactory.startDefaultNavigationApplication(2, CacheDetailActivity.this, wpt); return true; } }); addContextMenu(rowView); rowView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { selectedWaypoint = wpt; ensureSaved(); EditWaypointActivity.startActivityEditWaypoint(CacheDetailActivity.this, cache, wpt.getId()); refreshOnResume = true; } }); rowView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { selectedWaypoint = wpt; openContextMenu(v); return true; } }); } private void setWaypointIcon(final TextView nameView, final Waypoint wpt) { final WaypointType waypointType = wpt.getWaypointType(); final Drawable icon; if (wpt.isVisited()) { final LayerDrawable ld = new LayerDrawable(new Drawable[] { Compatibility.getDrawable(res, waypointType.markerId), Compatibility.getDrawable(res, R.drawable.tick) }); ld.setLayerInset(0, 0, 0, visitedInset, visitedInset); ld.setLayerInset(1, visitedInset, visitedInset, 0, 0); icon = ld; } else { icon = Compatibility.getDrawable(res, waypointType.markerId); } nameView.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); } } private class InventoryViewCreator extends AbstractCachingPageViewCreator<RecyclerView> { @Override public RecyclerView getDispatchedView(final ViewGroup parentView) { if (cache == null) { // something is really wrong return null; } view = (RecyclerView) getLayoutInflater().inflate(R.layout.cachedetail_inventory_page, parentView, false); final RecyclerView recyclerView = ButterKnife.findById(view, R.id.list); // TODO: fix layout, then switch back to Android-resource and delete copied one // this copy is modified to respect the text color RecyclerViewProvider.provideRecyclerView(CacheDetailActivity.this, recyclerView, true, true); cache.mergeInventory(genericTrackables, processedBrands); final TrackableListAdapter adapterTrackables = new TrackableListAdapter(cache.getInventory(), new TrackableListAdapter.TrackableClickListener() { @Override public void onTrackableClicked(final Trackable trackable) { TrackableActivity.startActivity(CacheDetailActivity.this, trackable.getGuid(), trackable.getGeocode(), trackable.getName(), cache.getGeocode(), trackable.getBrand().getId()); } }); recyclerView.setAdapter(adapterTrackables); cache.mergeInventory(genericTrackables, processedBrands); return view; } } private class ImagesViewCreator extends AbstractCachingPageViewCreator<View> { @Override public View getDispatchedView(final ViewGroup parentView) { if (cache == null) { return null; // something is really wrong } view = getLayoutInflater().inflate(R.layout.cachedetail_images_page, parentView, false); if (imagesList == null && isCurrentPage(Page.IMAGES)) { loadCacheImages(); } return view; } } public static void startActivity(final Context context, final String geocode, final String cacheName) { final Intent cachesIntent = new Intent(context, CacheDetailActivity.class); cachesIntent.putExtra(Intents.EXTRA_GEOCODE, geocode); cachesIntent.putExtra(Intents.EXTRA_NAME, cacheName); context.startActivity(cachesIntent); } private ActionMode mActionMode = null; private boolean mSelectionModeActive = false; private IndexOutOfBoundsAvoidingTextView selectedTextView; private class TextMenuItemClickListener implements MenuItem.OnMenuItemClickListener { @Override public boolean onMenuItemClick(final MenuItem menuItem) { final int startSelection = selectedTextView.getSelectionStart(); final int endSelection = selectedTextView.getSelectionEnd(); clickedItemText = selectedTextView.getText().subSequence(startSelection, endSelection); return onClipboardItemSelected(mActionMode, menuItem, clickedItemText, cache); } } @Override public void onSupportActionModeStarted(final ActionMode mode) { if (mSelectionModeActive && selectedTextView != null) { mSelectionModeActive = false; mActionMode = mode; final Menu menu = mode.getMenu(); mode.getMenuInflater().inflate(R.menu.details_context, menu); menu.findItem(R.id.menu_copy).setVisible(false); menu.findItem(R.id.menu_cache_share_field).setOnMenuItemClickListener(new TextMenuItemClickListener()); menu.findItem(R.id.menu_translate_to_sys_lang).setOnMenuItemClickListener(new TextMenuItemClickListener()); menu.findItem(R.id.menu_translate_to_english).setOnMenuItemClickListener(new TextMenuItemClickListener()); final MenuItem extWpts = menu.findItem(R.id.menu_extract_waypoints); extWpts.setVisible(true); extWpts.setOnMenuItemClickListener(new TextMenuItemClickListener()); buildDetailsContextMenu(mode, menu, res.getString(R.string.cache_description), false); selectedTextView.setWindowFocusWait(true); } super.onSupportActionModeStarted(mode); } @Override public void onSupportActionModeFinished(final ActionMode mode) { mActionMode = null; if (selectedTextView != null) { selectedTextView.setWindowFocusWait(false); } if (!mSelectionModeActive) { selectedTextView = null; } super.onSupportActionModeFinished(mode); } @Override public void addContextMenu(final View view) { view.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(final View v) { if (view.getId() == R.id.description || view.getId() == R.id.hint) { selectedTextView = (IndexOutOfBoundsAvoidingTextView) view; mSelectionModeActive = true; return false; } currentActionMode = startSupportActionMode(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) { return prepareClipboardActionMode(view, actionMode, menu); } private boolean prepareClipboardActionMode(final View view, final ActionMode actionMode, final Menu menu) { switch (view.getId()) { case R.id.value: // coordinates, gc-code, name clickedItemText = ((TextView) view).getText(); final CharSequence itemTitle = ((TextView) ((View) view.getParent()).findViewById(R.id.name)).getText(); if (itemTitle.equals(res.getText(R.string.cache_coordinates))) { clickedItemText = GeopointFormatter.reformatForClipboard(clickedItemText); } buildDetailsContextMenu(actionMode, menu, itemTitle, true); return true; case R.id.description: // combine short and long description final String shortDesc = cache.getShortDescription(); if (StringUtils.isBlank(shortDesc)) { clickedItemText = cache.getDescription(); } else { clickedItemText = shortDesc + "\n\n" + cache.getDescription(); } buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_description), false); return true; case R.id.personalnote: clickedItemText = cache.getPersonalNote(); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_personal_note), true); return true; case R.id.hint: clickedItemText = cache.getHint(); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_hint), false); return true; case R.id.log: clickedItemText = ((TextView) view).getText(); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_logs), false); return true; case R.id.date: // event date clickedItemText = Formatter.formatHiddenDate(cache); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_event), true); menu.findItem(R.id.menu_calendar).setVisible(cache.canBeAddedToCalendar()); return true; } return false; } @Override public void onDestroyActionMode(final ActionMode actionMode) { currentActionMode = null; } @Override public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) { actionMode.getMenuInflater().inflate(R.menu.details_context, menu); prepareClipboardActionMode(view, actionMode, menu); // Return true so that the action mode is shown return true; } @Override public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) { switch (menuItem.getItemId()) { // detail fields case R.id.menu_calendar: CalendarAdder.addToCalendar(CacheDetailActivity.this, cache); actionMode.finish(); return true; // handle clipboard actions in base default: return onClipboardItemSelected(actionMode, menuItem, clickedItemText, cache); } } }); return true; } }); } public static void startActivityGuid(final Context context, final String guid, final String cacheName) { final Intent cacheIntent = new Intent(context, CacheDetailActivity.class); cacheIntent.putExtra(Intents.EXTRA_GUID, guid); cacheIntent.putExtra(Intents.EXTRA_NAME, cacheName); context.startActivity(cacheIntent); } /** * A dialog to allow the user to select reseting coordinates local/remote/both. */ private AlertDialog createResetCacheCoordinatesDialog(final Waypoint wpt) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.waypoint_reset_cache_coords); final String[] items = { res.getString(R.string.waypoint_localy_reset_cache_coords), res.getString(R.string.waypoint_reset_local_and_remote_cache_coords) }; builder.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { dialog.dismiss(); final ProgressDialog progressDialog = ProgressDialog.show(CacheDetailActivity.this, getString(R.string.cache), getString(R.string.waypoint_reset), true); final HandlerResetCoordinates handler = new HandlerResetCoordinates(CacheDetailActivity.this, progressDialog, which == 1); resetCoords(cache, handler, wpt, which == 0 || which == 1, which == 1, progressDialog); } }); return builder.create(); } private static class HandlerResetCoordinates extends WeakReferenceHandler<CacheDetailActivity> { public static final int LOCAL = 0; public static final int ON_WEBSITE = 1; private boolean remoteFinished = false; private boolean localFinished = false; private final ProgressDialog progressDialog; private final boolean resetRemote; protected HandlerResetCoordinates(final CacheDetailActivity activity, final ProgressDialog progressDialog, final boolean resetRemote) { super(activity); this.progressDialog = progressDialog; this.resetRemote = resetRemote; } @Override public void handleMessage(final Message msg) { if (msg.what == LOCAL) { localFinished = true; } else { remoteFinished = true; } if (localFinished && (remoteFinished || !resetRemote)) { progressDialog.dismiss(); final CacheDetailActivity activity = getReference(); if (activity != null) { activity.notifyDataSetChanged(); } } } } private void resetCoords(final Geocache cache, final Handler handler, final Waypoint wpt, final boolean local, final boolean remote, final ProgressDialog progress) { AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { if (local) { runOnUiThread(new Runnable() { @Override public void run() { progress.setMessage(res.getString(R.string.waypoint_reset_cache_coords)); } }); cache.setCoords(wpt.getCoords()); cache.setUserModifiedCoords(false); cache.deleteWaypointForce(wpt); DataStore.saveChangedCache(cache); handler.sendEmptyMessage(HandlerResetCoordinates.LOCAL); } final IConnector con = ConnectorFactory.getConnector(cache); if (remote && con.supportsOwnCoordinates()) { runOnUiThread(new Runnable() { @Override public void run() { progress.setMessage(res.getString(R.string.waypoint_coordinates_being_reset_on_website)); } }); final boolean result = con.deleteModifiedCoordinates(cache); runOnUiThread(new Runnable() { @Override public void run() { if (result) { showToast(getString(R.string.waypoint_coordinates_has_been_reset_on_website)); } else { showToast(getString(R.string.waypoint_coordinates_upload_error)); } handler.sendEmptyMessage(HandlerResetCoordinates.ON_WEBSITE); notifyDataSetChanged(); } }); } } }); } @Override protected String getTitle(final Page page) { // show number of waypoints directly in waypoint title if (page == Page.WAYPOINTS) { final int waypointCount = cache.getWaypoints().size(); return res.getQuantityString(R.plurals.waypoints, waypointCount, waypointCount); } return res.getString(page.titleStringId); } @Override protected Pair<List<? extends Page>, Integer> getOrderedPages() { final ArrayList<Page> pages = new ArrayList<>(); pages.add(Page.WAYPOINTS); pages.add(Page.DETAILS); final int detailsIndex = pages.size() - 1; pages.add(Page.DESCRIPTION); // enforce showing the empty log book if new entries can be added if (cache.supportsLogging() || !cache.getLogs().isEmpty()) { pages.add(Page.LOGS); } if (CollectionUtils.isNotEmpty(cache.getFriendsLogs())) { pages.add(Page.LOGSFRIENDS); } if (CollectionUtils.isNotEmpty(cache.getInventory()) || CollectionUtils.isNotEmpty(genericTrackables)) { pages.add(Page.INVENTORY); } if (CollectionUtils.isNotEmpty(cache.getNonStaticImages())) { pages.add(Page.IMAGES); } return new ImmutablePair<List<? extends Page>, Integer>(pages, detailsIndex); } @Override protected AbstractViewPagerActivity.PageViewCreator createViewCreator(final Page page) { switch (page) { case DETAILS: return new DetailsViewCreator(); case DESCRIPTION: return new DescriptionViewCreator(); case LOGS: return new CacheLogsViewCreator(this, true); case LOGSFRIENDS: return new CacheLogsViewCreator(this, false); case WAYPOINTS: return new WaypointsViewCreator(); case INVENTORY: return new InventoryViewCreator(); case IMAGES: return new ImagesViewCreator(); } throw new IllegalStateException(); // cannot happen as long as switch case is enum complete } static void updateOfflineBox(final View view, final Geocache cache, final Resources res, final OnClickListener refreshCacheClickListener, final OnClickListener dropCacheClickListener, final OnClickListener storeCacheClickListener, final OnLongClickListener moveCacheListener, final OnLongClickListener storeCachePreselectedListener) { // offline use final TextView offlineText = ButterKnife.findById(view, R.id.offline_text); final ImageButton offlineRefresh = ButterKnife.findById(view, R.id.offline_refresh); final ImageButton offlineStoreDrop = ButterKnife.findById(view, R.id.offline_store_drop); final ImageButton offlineEdit = ButterKnife.findById(view, R.id.offline_edit); offlineStoreDrop.setClickable(true); offlineStoreDrop.setOnClickListener(storeCacheClickListener); offlineStoreDrop.setOnLongClickListener(storeCachePreselectedListener); if (moveCacheListener != null) { offlineEdit.setOnLongClickListener(moveCacheListener); } offlineRefresh.setVisibility(cache.supportsRefresh() ? View.VISIBLE : View.GONE); offlineRefresh.setClickable(true); offlineRefresh.setOnClickListener(refreshCacheClickListener); if (cache.isOffline()) { final long diff = (System.currentTimeMillis() / (60 * 1000)) - (cache.getUpdated() / (60 * 1000)); // minutes final String ago; if (diff < 15) { ago = res.getString(R.string.cache_offline_time_mins_few); } else if (diff < 50) { ago = res.getQuantityString(R.plurals.cache_offline_about_time_mins, (int) diff, (int) diff); } else if (diff < (48 * 60)) { ago = res.getQuantityString(R.plurals.cache_offline_about_time_hours, (int) (diff / 60), (int) (diff / 60)); } else { ago = res.getQuantityString(R.plurals.cache_offline_about_time_days, (int) (diff / (24 * 60)), (int) (diff / (24 * 60))); } offlineText.setText(res.getString(R.string.cache_offline_stored) + "\n" + ago); offlineStoreDrop.setOnClickListener(dropCacheClickListener); offlineStoreDrop.setOnLongClickListener(null); offlineStoreDrop.setClickable(true); offlineStoreDrop.setImageResource(R.drawable.ic_menu_delete); offlineEdit.setVisibility(View.VISIBLE); offlineEdit.setOnClickListener(storeCacheClickListener); } else { offlineText.setText(res.getString(R.string.cache_offline_not_ready)); offlineStoreDrop.setImageResource(R.drawable.ic_menu_save); offlineEdit.setVisibility(View.GONE); } } static void updateCacheLists(final View view, final Geocache cache, final Resources res) { final Set<String> listNames = new HashSet<>(); for (final Integer listId : cache.getLists()) { final StoredList list = DataStore.getList(listId); listNames.add(list.getTitle()); } final TextView offlineLists = ButterKnife.findById(view, R.id.offline_lists); offlineLists.setText(res.getString(R.string.list_list_headline) + " " + StringUtils.join(listNames.toArray(), ", ")); } public Geocache getCache() { return cache; } private static class StoreCacheHandler extends SimpleDisposableHandler { StoreCacheHandler(final CacheDetailActivity activity, final Progress progress) { super(activity, progress); } @Override public void handleRegularMessage(final Message msg) { if (msg.what == UPDATE_LOAD_PROGRESS_DETAIL && msg.obj instanceof String) { updateStatusMsg(R.string.cache_dialog_offline_save_message, (String) msg.obj); } else { notifyDataSetChanged(activityRef); } } } private static final class RefreshCacheHandler extends SimpleDisposableHandler { RefreshCacheHandler(final CacheDetailActivity activity, final Progress progress) { super(activity, progress); } @Override public void handleRegularMessage(final Message msg) { if (msg.what == UPDATE_LOAD_PROGRESS_DETAIL && msg.obj instanceof String) { updateStatusMsg(R.string.cache_dialog_refresh_message, (String) msg.obj); } else { notifyDataSetChanged(activityRef); } } } private static final class ChangeNotificationHandler extends SimpleHandler { ChangeNotificationHandler(final CacheDetailActivity activity, final Progress progress) { super(activity, progress); } @Override public void handleMessage(final Message msg) { notifyDataSetChanged(activityRef); } } private static void notifyDataSetChanged(final WeakReference<AbstractActivity> activityRef) { final CacheDetailActivity activity = (CacheDetailActivity) activityRef.get(); if (activity != null) { activity.notifyDataSetChanged(); } } protected void storeCache(final Set<Integer> listIds) { final StoreCacheHandler storeCacheHandler = new StoreCacheHandler(CacheDetailActivity.this, progress); progress.show(this, res.getString(R.string.cache_dialog_offline_save_title), res.getString(R.string.cache_dialog_offline_save_message), true, storeCacheHandler.disposeMessage()); AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { cache.store(listIds, storeCacheHandler); } }); } public static void editPersonalNote(final Geocache cache, final CacheDetailActivity activity) { final FragmentManager fm = activity.getSupportFragmentManager(); final EditNoteDialog dialog = EditNoteDialog.newInstance(cache.getPersonalNote()); dialog.show(fm, "fragment_edit_note"); } @Override public void onFinishEditNoteDialog(final String note) { cache.setPersonalNote(note); if (cache.addWaypointsFromNote()) { final PageViewCreator wpViewCreator = getViewCreator(Page.WAYPOINTS); if (wpViewCreator != null) { wpViewCreator.notifyDataSetChanged(); } } final TextView personalNoteView = ButterKnife.findById(this, R.id.personalnote); if (personalNoteView != null) { setPersonalNote(personalNoteView, note); } else { reinitializeViewPager(); } Schedulers.io().scheduleDirect(new Runnable() { @Override public void run() { DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); } }); } private static void setPersonalNote(final TextView personalNoteView, final String personalNote) { personalNoteView.setText(personalNote, TextView.BufferType.SPANNABLE); if (StringUtils.isNotBlank(personalNote)) { personalNoteView.setVisibility(View.VISIBLE); Linkify.addLinks(personalNoteView, Linkify.ALL); } else { personalNoteView.setVisibility(View.GONE); } } @Override public void navigateTo() { startDefaultNavigation(); } @Override public void showNavigationMenu() { NavigationAppFactory.showNavigationMenu(this, cache, null, null); } @Override public void cachesAround() { CacheListActivity.startActivityCoordinates(this, cache.getCoords(), cache.getName()); } public void setNeedsRefresh() { refreshOnResume = true; } }