package cgeo.geocaching.models; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.activity.SimpleWebviewActivity; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.ILoggingManager; import cgeo.geocaching.connector.capability.ISearchByCenter; import cgeo.geocaching.connector.capability.ISearchByGeocode; import cgeo.geocaching.connector.capability.WatchListCapability; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.GCConstants; import cgeo.geocaching.connector.gc.Tile; import cgeo.geocaching.connector.gc.UncertainProperty; import cgeo.geocaching.connector.trackable.TrackableBrand; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.CoordinatesType; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.log.LogCacheActivity; import cgeo.geocaching.log.LogEntry; import cgeo.geocaching.log.LogTemplateProvider; import cgeo.geocaching.log.LogTemplateProvider.LogContext; import cgeo.geocaching.log.LogType; import cgeo.geocaching.maps.mapsforge.v6.caches.GeoitemRef; import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.staticmaps.StaticMapsProvider; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.storage.DataStore.StorageLocation; import cgeo.geocaching.storage.LocalStorage; import cgeo.geocaching.utils.CalendarUtils; import cgeo.geocaching.utils.DisposableHandler; import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.LazyInitializedList; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Html; import java.io.File; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.reactivex.Scheduler; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; /** * Internal representation of a "cache" */ public class Geocache implements IWaypoint { private static final int OWN_WP_PREFIX_OFFSET = 17; private long updated = 0; private long detailedUpdate = 0; private long visitedDate = 0; private Set<Integer> lists = new HashSet<>(); private boolean detailed = false; @NonNull private String geocode = ""; private String cacheId = ""; private String guid = ""; private UncertainProperty<CacheType> cacheType = new UncertainProperty<>(CacheType.UNKNOWN, Tile.ZOOMLEVEL_MIN - 1); private String name = ""; private String ownerDisplayName = ""; private String ownerUserId = ""; @Nullable private Date hidden = null; /** * lazy initialized */ private String hint = null; @NonNull private CacheSize size = CacheSize.UNKNOWN; private float difficulty = 0; private float terrain = 0; private Float direction = null; private Float distance = null; /** * lazy initialized */ private String location = null; private UncertainProperty<Geopoint> coords = new UncertainProperty<>(null); private boolean reliableLatLon = false; private String personalNote = null; /** * lazy initialized */ private String shortdesc = null; /** * lazy initialized */ private String description = null; private Boolean disabled = null; private Boolean archived = null; private Boolean premiumMembersOnly = null; private Boolean found = null; private Boolean favorite = null; private Boolean onWatchlist = null; private Boolean logOffline = null; private int watchlistCount = -1; // valid numbers are larger than -1 private int favoritePoints = 0; private float rating = 0; // valid ratings are larger than zero private int votes = 0; private float myVote = 0; // valid ratings are larger than zero private int inventoryItems = 0; private final LazyInitializedList<String> attributes = new LazyInitializedList<String>() { @Override public List<String> call() { return inDatabase() ? DataStore.loadAttributes(geocode) : new LinkedList<String>(); } }; private final LazyInitializedList<Waypoint> waypoints = new LazyInitializedList<Waypoint>() { @Override public List<Waypoint> call() { return inDatabase() ? DataStore.loadWaypoints(geocode) : new LinkedList<Waypoint>(); } }; private List<Image> spoilers = null; private List<Trackable> inventory = null; private Map<LogType, Integer> logCounts = new EnumMap<>(LogType.class); private boolean userModifiedCoords = false; // temporary values private boolean statusChecked = false; private String directionImg = ""; private String nameForSorting; private final EnumSet<StorageLocation> storageLocation = EnumSet.of(StorageLocation.HEAP); private boolean finalDefined = false; private boolean logPasswordRequired = false; private LogEntry offlineLog = null; private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); private Handler changeNotificationHandler = null; public void setChangeNotificationHandler(final Handler newNotificationHandler) { changeNotificationHandler = newNotificationHandler; } /** * Sends a change notification to interested parties */ private void notifyChange() { if (changeNotificationHandler != null) { changeNotificationHandler.sendEmptyMessage(0); } } /** * Gather missing information for new Geocache object from the stored Geocache object. * This is called in the new Geocache parsed from website to set information not yet * parsed. * * @param other * the other version, or null if non-existent * @return true if this cache is "equal" to the other version */ public boolean gatherMissingFrom(final Geocache other) { if (other == null) { return false; } if (other == this) { return true; } updated = System.currentTimeMillis(); // if parsed cache is not yet detailed and stored is, the information of // the parsed cache will be overwritten if (!detailed && other.detailed) { detailed = true; detailedUpdate = other.detailedUpdate; // boolean values must be enumerated here. Other types are assigned outside this if-statement reliableLatLon = other.reliableLatLon; finalDefined = other.finalDefined; } if (premiumMembersOnly == null) { premiumMembersOnly = other.premiumMembersOnly; } if (found == null) { found = other.found; } if (disabled == null) { disabled = other.disabled; } if (favorite == null) { favorite = other.favorite; } if (archived == null) { archived = other.archived; } if (onWatchlist == null) { onWatchlist = other.onWatchlist; } if (logOffline == null) { logOffline = other.logOffline; } if (visitedDate == 0) { visitedDate = other.visitedDate; } if (lists.isEmpty()) { lists.addAll(other.lists); } if (StringUtils.isBlank(geocode)) { geocode = other.geocode; } if (StringUtils.isBlank(cacheId)) { cacheId = other.cacheId; } if (StringUtils.isBlank(guid)) { guid = other.guid; } cacheType = UncertainProperty.getMergedProperty(cacheType, other.cacheType); if (StringUtils.isBlank(name)) { name = other.name; } if (StringUtils.isBlank(ownerDisplayName)) { ownerDisplayName = other.ownerDisplayName; } if (StringUtils.isBlank(ownerUserId)) { ownerUserId = other.ownerUserId; } if (hidden == null) { hidden = other.hidden; } if (!detailed && StringUtils.isBlank(getHint())) { hint = other.getHint(); } if (size == CacheSize.UNKNOWN) { size = other.size; } if (difficulty == 0) { difficulty = other.difficulty; } if (terrain == 0) { terrain = other.terrain; } if (direction == null) { direction = other.direction; } if (distance == null) { distance = other.distance; } if (StringUtils.isBlank(getLocation())) { location = other.getLocation(); } // don't use StringUtils.isBlank here. Otherwise we cannot recognize a note which was deleted on GC if (personalNote == null) { personalNote = other.personalNote; } else if (other.personalNote != null && !personalNote.equals(other.personalNote)) { final PersonalNote myNote = new PersonalNote(this); final PersonalNote otherNote = new PersonalNote(other); final PersonalNote mergedNote = myNote.mergeWith(otherNote); personalNote = mergedNote.toString(); } if (!detailed && StringUtils.isBlank(getShortDescription())) { shortdesc = other.getShortDescription(); } if (StringUtils.isBlank(getDescription())) { description = other.getDescription(); } // FIXME: this makes no sense to favor this over the other. 0 should not be a special case here as it is // in the range of acceptable values. This is probably the case at other places (rating, votes, etc.) too. if (favoritePoints == 0) { favoritePoints = other.favoritePoints; } if (rating == 0) { rating = other.rating; } if (votes == 0) { votes = other.votes; } if (myVote == 0) { myVote = other.myVote; } if (!detailed && attributes.isEmpty() && other.attributes != null) { attributes.addAll(other.attributes); } if (waypoints.isEmpty()) { this.setWaypoints(other.waypoints, false); } else { final List<Waypoint> newPoints = new ArrayList<>(waypoints); Waypoint.mergeWayPoints(newPoints, other.waypoints, false); this.setWaypoints(newPoints, false); } if (spoilers == null) { spoilers = other.spoilers; } if (inventory == null) { // If inventoryItems is 0, it can mean both // "don't know" or "0 items". Since we cannot distinguish // them here, only populate inventoryItems from // old data when we have to do it for inventory. setInventory(other.inventory); } if (logCounts.isEmpty()) { logCounts = other.logCounts; } if (!userModifiedCoords && other.hasUserModifiedCoords()) { final Waypoint original = other.getOriginalWaypoint(); if (original != null) { original.setCoords(getCoords()); } setCoords(other.getCoords()); } else { coords = UncertainProperty.getMergedProperty(coords, other.coords); } // if cache has ORIGINAL type waypoint ... it is considered that it has modified coordinates, otherwise not userModifiedCoords = getOriginalWaypoint() != null; if (!reliableLatLon) { reliableLatLon = other.reliableLatLon; } return isEqualTo(other); } /** * Returns the Original Waypoint if exists */ public Waypoint getOriginalWaypoint() { for (final Waypoint wpt : waypoints) { if (wpt.getWaypointType() == WaypointType.ORIGINAL) { return wpt; } } return null; } /** * Compare two caches quickly. For map and list fields only the references are compared ! * * @param other * the other cache to compare this one to * @return true if both caches have the same content */ @SuppressWarnings("deprecation") @SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY") private boolean isEqualTo(final Geocache other) { return detailed == other.detailed && StringUtils.equalsIgnoreCase(geocode, other.geocode) && StringUtils.equalsIgnoreCase(name, other.name) && UncertainProperty.equalValues(cacheType, other.cacheType) && size == other.size && ObjectUtils.equals(found, other.found) && ObjectUtils.equals(premiumMembersOnly, other.premiumMembersOnly) && difficulty == other.difficulty && terrain == other.terrain && UncertainProperty.equalValues(coords, other.coords) && reliableLatLon == other.reliableLatLon && ObjectUtils.equals(disabled, other.disabled) && ObjectUtils.equals(archived, other.archived) && ObjectUtils.equals(lists, other.lists) && StringUtils.equalsIgnoreCase(ownerDisplayName, other.ownerDisplayName) && StringUtils.equalsIgnoreCase(ownerUserId, other.ownerUserId) && StringUtils.equalsIgnoreCase(getDescription(), other.getDescription()) && StringUtils.equalsIgnoreCase(personalNote, other.personalNote) && StringUtils.equalsIgnoreCase(getShortDescription(), other.getShortDescription()) && StringUtils.equalsIgnoreCase(getLocation(), other.getLocation()) && ObjectUtils.equals(favorite, other.favorite) && favoritePoints == other.favoritePoints && ObjectUtils.equals(onWatchlist, other.onWatchlist) && (hidden != null ? hidden.equals(other.hidden) : other.hidden == null) && StringUtils.equalsIgnoreCase(guid, other.guid) && StringUtils.equalsIgnoreCase(getHint(), other.getHint()) && StringUtils.equalsIgnoreCase(cacheId, other.cacheId) && (direction != null ? direction.equals(other.direction) : other.direction == null) && (distance != null ? distance.equals(other.distance) : other.distance == null) && rating == other.rating && votes == other.votes && myVote == other.myVote && inventoryItems == other.inventoryItems && attributes == other.attributes && waypoints == other.waypoints && spoilers == other.spoilers && inventory == other.inventory && logCounts == other.logCounts && ObjectUtils.equals(logOffline, other.logOffline) && finalDefined == other.finalDefined; } public boolean hasTrackables() { return inventoryItems > 0; } public boolean canBeAddedToCalendar() { // Is event type with event date set? return isEventCache() && hidden != null; } public boolean isPastEvent() { final Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); assert hidden != null; // Eclipse compiler issue return hidden.compareTo(cal.getTime()) < 0; } public boolean isEventCache() { return cacheType.getValue().isEvent(); } public void logVisit(final Activity fromActivity) { if (!getConnector().canLog(this)) { ActivityMixin.showToast(fromActivity, fromActivity.getString(R.string.err_cannot_log_visit)); return; } fromActivity.startActivity(LogCacheActivity.getLogCacheIntent(fromActivity, cacheId, geocode)); } public void logOffline(final Activity fromActivity, final LogType logType) { final boolean mustIncludeSignature = StringUtils.isNotBlank(Settings.getSignature()) && Settings.isAutoInsertSignature(); final String initial = mustIncludeSignature ? LogTemplateProvider.applyTemplates(Settings.getSignature(), new LogContext(this, null, true)) : ""; logOffline(fromActivity, initial, Calendar.getInstance(), logType); } public void logOffline(final Activity fromActivity, final String log, final Calendar date, final LogType logType) { if (logType == LogType.UNKNOWN) { return; } if (!isOffline()) { getLists().add(StoredList.STANDARD_LIST_ID); DataStore.saveCache(this, LoadFlags.SAVE_ALL); } final boolean status = DataStore.saveLogOffline(geocode, date.getTime(), logType, log); final Resources res = fromActivity.getResources(); if (status) { ActivityMixin.showToast(fromActivity, res.getString(R.string.info_log_saved)); DataStore.saveVisitDate(geocode, date.getTimeInMillis()); logOffline = Boolean.TRUE; offlineLog = DataStore.loadLogOffline(geocode); notifyChange(); } else { ActivityMixin.showToast(fromActivity, res.getString(R.string.err_log_post_failed)); } } /** * Get the Offline Log entry if any. * * @return * The Offline LogEntry */ @Nullable public LogEntry getOfflineLog() { if (isLogOffline() && offlineLog == null) { offlineLog = DataStore.loadLogOffline(geocode); } return offlineLog; } /** * Get the Offline Log entry if any. * * @return * The Offline LogEntry else Null */ @Nullable public LogType getOfflineLogType() { final LogEntry offlineLog = getOfflineLog(); if (offlineLog == null) { return null; } return offlineLog.getType(); } /** * Drop offline log for a given geocode. */ public void clearOfflineLog() { DataStore.clearLogOffline(geocode); setLogOffline(false); notifyChange(); } @NonNull public List<LogType> getPossibleLogTypes() { return getConnector().getPossibleLogTypes(this); } public void openInBrowser(final Context context) { if (getUrl() == null) { return; } final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getLongUrl())); // Check if cgeo is the default, show the chooser to let the user choose a browser if (viewIntent.resolveActivity(context.getPackageManager()).getPackageName().equals(context.getPackageName())) { final Intent chooser = Intent.createChooser(viewIntent, context.getString(R.string.cache_menu_browser)); final Intent internalBrowser = new Intent(context, SimpleWebviewActivity.class); internalBrowser.setData(Uri.parse(getUrl())); chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[] {internalBrowser}); context.startActivity(chooser); } else { context.startActivity(viewIntent); } } @NonNull private IConnector getConnector() { return ConnectorFactory.getConnector(this); } public boolean supportsRefresh() { return getConnector() instanceof ISearchByGeocode; } public boolean supportsWatchList() { final IConnector connector = getConnector(); return (connector instanceof WatchListCapability) && ((WatchListCapability) connector).canAddToWatchList(this); } public boolean supportsFavoritePoints() { return getConnector().supportsFavoritePoints(this); } public boolean supportsLogging() { return getConnector().supportsLogging(); } public boolean supportsLogImages() { return getConnector().supportsLogImages(); } public boolean supportsOwnCoordinates() { return getConnector().supportsOwnCoordinates(); } @NonNull public ILoggingManager getLoggingManager(final LogCacheActivity activity) { return getConnector().getLoggingManager(activity, this); } public float getDifficulty() { return difficulty; } @Override @NonNull public String getGeocode() { return geocode; } /** * @return displayed owner, might differ from the real owner */ public String getOwnerDisplayName() { return ownerDisplayName; } @NonNull public CacheSize getSize() { return size; } public float getTerrain() { return terrain; } public boolean isArchived() { return BooleanUtils.isTrue(archived); } public boolean isDisabled() { return BooleanUtils.isTrue(disabled); } public boolean isPremiumMembersOnly() { return BooleanUtils.isTrue(premiumMembersOnly); } public void setPremiumMembersOnly(final boolean members) { this.premiumMembersOnly = members; } /** * * @return {@code true} if the user is the owner of the cache, {@code false} otherwise */ public boolean isOwner() { return getConnector().isOwner(this); } /** * @return GC username of the (actual) owner, might differ from the owner. Never empty. */ @NonNull public String getOwnerUserId() { return ownerUserId; } /** * Attention, calling this method may trigger a database access for the cache! * * @return the decrypted hint */ public String getHint() { initializeCacheTexts(); assertTextNotNull(hint, "Hint"); return hint; } /** * After lazy loading the lazily loaded field must be non {@code null}. * */ private static void assertTextNotNull(final String field, final String name) throws InternalError { if (field == null) { throw new InternalError(name + " field is not allowed to be null here"); } } /** * Attention, calling this method may trigger a database access for the cache! */ public String getDescription() { initializeCacheTexts(); assertTextNotNull(description, "Description"); return description; } /** * loads long text parts of a cache on demand (but all fields together) */ private void initializeCacheTexts() { if (description == null || shortdesc == null || hint == null || location == null) { if (inDatabase()) { final Geocache partial = DataStore.loadCacheTexts(this.getGeocode()); if (description == null) { setDescription(partial.getDescription()); } if (shortdesc == null) { setShortDescription(partial.getShortDescription()); } if (hint == null) { setHint(partial.getHint()); } if (location == null) { setLocation(partial.getLocation()); } } else { description = StringUtils.defaultString(description); shortdesc = StringUtils.defaultString(shortdesc); hint = StringUtils.defaultString(hint); location = StringUtils.defaultString(location); } } } /** * Attention, calling this method may trigger a database access for the cache! */ public String getShortDescription() { initializeCacheTexts(); assertTextNotNull(shortdesc, "Short description"); return shortdesc; } @Override public String getName() { return name; } public String getCacheId() { if (StringUtils.isBlank(cacheId) && getConnector().equals(GCConnector.getInstance())) { return String.valueOf(GCConstants.gccodeToGCId(geocode)); } return cacheId; } public String getGuid() { return guid; } /** * Attention, calling this method may trigger a database access for the cache! */ public String getLocation() { initializeCacheTexts(); assertTextNotNull(location, "Location"); return location; } public String getPersonalNote() { // non premium members have no personal notes, premium members have an empty string by default. // map both to null, so other code doesn't need to differentiate return StringUtils.defaultIfBlank(personalNote, null); } public boolean supportsCachesAround() { return getConnector() instanceof ISearchByCenter; } public void shareCache(@NonNull final Activity fromActivity, final Resources res) { final Intent intent = getShareIntent(); fromActivity.startActivity(Intent.createChooser(intent, res.getText(R.string.cache_menu_share))); } @NonNull public Intent getShareIntent() { final StringBuilder subject = new StringBuilder("Geocache "); subject.append(geocode); if (StringUtils.isNotBlank(name)) { subject.append(" - ").append(name); } final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject.toString()); intent.putExtra(Intent.EXTRA_TEXT, StringUtils.defaultString(getUrl())); return intent; } @Nullable public String getUrl() { return getConnector().getCacheUrl(this); } @Nullable public String getLongUrl() { return getConnector().getLongCacheUrl(this); } @Nullable public String getCgeoUrl() { return getConnector().getCacheUrl(this); } public boolean supportsGCVote() { return StringUtils.startsWithIgnoreCase(geocode, "GC"); } public void setDescription(final String description) { this.description = description; } public boolean isFound() { return BooleanUtils.isTrue(found); } /** * * @return {@code true} if the user has put a favorite point onto this cache */ public boolean isFavorite() { return BooleanUtils.isTrue(favorite); } public void setFavorite(final boolean favorite) { this.favorite = favorite; } @Nullable public Date getHiddenDate() { if (hidden != null) { return new Date(hidden.getTime()); } return null; } @NonNull public List<String> getAttributes() { return attributes.getUnderlyingList(); } public void addSpoiler(final Image spoiler) { if (spoilers == null) { spoilers = new ArrayList<>(); } spoilers.add(spoiler); } @NonNull public List<Image> getSpoilers() { return ListUtils.unmodifiableList(ListUtils.emptyIfNull(spoilers)); } /** * @return a statistic how often the caches has been found, disabled, archived etc. */ public Map<LogType, Integer> getLogCounts() { return logCounts; } public int getFavoritePoints() { return favoritePoints; } /** * @return the normalized cached name to be used for sorting, taking into account the numerical parts in the name */ public String getNameForSorting() { if (nameForSorting == null) { nameForSorting = name; // pad each number part to a fixed size of 6 digits, so that numerical sorting becomes equivalent to string sorting MatcherWrapper matcher = new MatcherWrapper(NUMBER_PATTERN, nameForSorting); int start = 0; while (matcher.find(start)) { final String number = matcher.group(); nameForSorting = StringUtils.substring(nameForSorting, 0, matcher.start()) + StringUtils.leftPad(number, 6, '0') + StringUtils.substring(nameForSorting, matcher.start() + number.length()); start = matcher.start() + Math.max(6, number.length()); matcher = new MatcherWrapper(NUMBER_PATTERN, nameForSorting); } } return nameForSorting; } public boolean isVirtual() { return cacheType.getValue().isVirtual(); } public boolean showSize() { return !(size == CacheSize.NOT_CHOSEN || isEventCache() || isVirtual()); } public long getUpdated() { return updated; } public void setUpdated(final long updated) { this.updated = updated; } public long getDetailedUpdate() { return detailedUpdate; } public void setDetailedUpdate(final long detailedUpdate) { this.detailedUpdate = detailedUpdate; } public long getVisitedDate() { return visitedDate; } public void setVisitedDate(final long visitedDate) { this.visitedDate = visitedDate; } public Set<Integer> getLists() { return lists; } public void setLists(final Set<Integer> lists) { // Create a new set to allow immutable structures such as SingletonSet to be // given by the caller. We want the value returned by getLists() to be mutable // since remove or add operations may be done on it. this.lists = new HashSet<>(lists); } public boolean isDetailed() { return detailed; } public void setDetailed(final boolean detailed) { this.detailed = detailed; } public void setHidden(@Nullable final Date hidden) { this.hidden = hidden != null ? new Date(hidden.getTime()) : null; } public Float getDirection() { return direction; } public void setDirection(final Float direction) { this.direction = direction; } public Float getDistance() { return distance; } public void setDistance(final Float distance) { this.distance = distance; } @Override public Geopoint getCoords() { return coords.getValue(); } public int getCoordZoomLevel() { return coords.getCertaintyLevel(); } /** * Set reliable coordinates */ public void setCoords(final Geopoint coords) { this.coords = new UncertainProperty<>(coords); } /** * Set unreliable coordinates from a certain map zoom level */ public void setCoords(final Geopoint coords, final int zoomlevel) { this.coords = new UncertainProperty<>(coords, zoomlevel); } /** * @return true if the coordinates are from the cache details page and the user has been logged in */ public boolean isReliableLatLon() { return getConnector().isReliableLatLon(reliableLatLon); } public void setReliableLatLon(final boolean reliableLatLon) { this.reliableLatLon = reliableLatLon; } public void setShortDescription(final String shortdesc) { this.shortdesc = shortdesc; } public void setFavoritePoints(final int favoriteCnt) { this.favoritePoints = favoriteCnt; } public float getRating() { return rating; } public void setRating(final float rating) { this.rating = rating; } public int getVotes() { return votes; } public void setVotes(final int votes) { this.votes = votes; } public float getMyVote() { return myVote; } public void setMyVote(final float myVote) { this.myVote = myVote; } /** * Get the current inventory count * * @return the inventory size */ public int getInventoryItems() { return inventoryItems; } /** * Set the current inventory count * * @param inventoryItems the new inventory size */ public void setInventoryItems(final int inventoryItems) { this.inventoryItems = inventoryItems; } /** * Get the current inventory * * @return the inventory of Trackables as unmodifiable collection. Use {@link #setInventory(List)} or * {@link #addInventoryItem(Trackable)} for modifications. */ @NonNull public List<Trackable> getInventory() { return inventory == null ? Collections.<Trackable> emptyList() : Collections.unmodifiableList(inventory); } /** * Replace the inventory with new content. * No checks are performed. * * @param newInventory * to set on Geocache */ public void setInventory(final List<Trackable> newInventory) { inventory = newInventory; inventoryItems = CollectionUtils.size(inventory); } /** * Add new Trackables to inventory safely. * This takes care of removing old items if they are from the same brand. * If items are present, data is merged, not duplicated. * * @param newTrackables * to be added to the Geocache */ public void mergeInventory(@NonNull final List<Trackable> newTrackables, final EnumSet<TrackableBrand> processedBrands) { final List<Trackable> mergedTrackables = new ArrayList<>(newTrackables); for (final Trackable trackable : ListUtils.emptyIfNull(inventory)) { if (processedBrands.contains(trackable.getBrand())) { final ListIterator<Trackable> iterator = mergedTrackables.listIterator(); while (iterator.hasNext()) { final Trackable newTrackable = iterator.next(); if (trackable.getUniqueID().equals(newTrackable.getUniqueID())) { // Respect the merge order. New Values replace existing values. trackable.mergeTrackable(newTrackable); iterator.set(trackable); break; } } } else { mergedTrackables.add(trackable); } } setInventory(mergedTrackables); } /** * Add new Trackable to inventory safely. * If items are present, data are merged, not duplicated. * * @param newTrackable to be added to the Geocache */ public void addInventoryItem(final Trackable newTrackable) { if (inventory == null) { inventory = new ArrayList<>(); } boolean foundTrackable = false; for (final Trackable trackable: inventory) { if (trackable.getUniqueID().equals(newTrackable.getUniqueID())) { // Trackable already present, merge data foundTrackable = true; trackable.mergeTrackable(newTrackable); break; } } if (!foundTrackable) { inventory.add(newTrackable); } inventoryItems = inventory.size(); } /** * @return {@code true} if the cache is on the user's watchlist, {@code false} otherwise */ public boolean isOnWatchlist() { return BooleanUtils.isTrue(onWatchlist); } public void setOnWatchlist(final boolean onWatchlist) { this.onWatchlist = onWatchlist; } /** * * Set the number of users watching this geocache * @param watchlistCount Number of users watching this geocache */ public void setWatchlistCount(final int watchlistCount) { this.watchlistCount = watchlistCount; } /** * * get the number of users watching this geocache * @return watchlistCount Number of users watching this geocache */ public int getWatchlistCount() { return watchlistCount; } /** * return an immutable list of waypoints. * * @return always non {@code null} */ @NonNull public List<Waypoint> getWaypoints() { return waypoints.getUnderlyingList(); } /** * @param waypoints * List of waypoints to set for cache * @param saveToDatabase * Indicates whether to add the waypoints to the database. Should be false if * called while loading or building a cache * @return {@code true} if waypoints successfully added to waypoint database */ public boolean setWaypoints(@Nullable final List<Waypoint> waypoints, final boolean saveToDatabase) { this.waypoints.clear(); if (waypoints != null) { this.waypoints.addAll(waypoints); } finalDefined = false; if (waypoints != null) { for (final Waypoint waypoint : waypoints) { waypoint.setGeocode(geocode); if (waypoint.isFinalWithCoords()) { finalDefined = true; } } } return saveToDatabase && DataStore.saveWaypoints(this); } /** * The list of logs is immutable, because it is directly fetched from the database on demand, and not stored at this * object. If you want to modify logs, you have to load all logs of the cache, create a new list from the existing * list and store that new list in the database. * * @return immutable list of logs */ @NonNull public List<LogEntry> getLogs() { return inDatabase() ? DataStore.loadLogs(geocode) : Collections.<LogEntry>emptyList(); } /** * @return only the logs of friends */ @NonNull public List<LogEntry> getFriendsLogs() { final List<LogEntry> friendLogs = new ArrayList<>(); for (final LogEntry log : getLogs()) { if (log.friend) { friendLogs.add(log); } } return Collections.unmodifiableList(friendLogs); } public boolean isLogOffline() { return BooleanUtils.isTrue(logOffline); } public void setLogOffline(final boolean logOffline) { this.logOffline = logOffline; } public boolean isStatusChecked() { return statusChecked; } public void setStatusChecked(final boolean statusChecked) { this.statusChecked = statusChecked; } public String getDirectionImg() { return directionImg; } public void setDirectionImg(final String directionImg) { this.directionImg = directionImg; } public void setGeocode(@NonNull final String geocode) { this.geocode = StringUtils.upperCase(geocode); } public void setCacheId(final String cacheId) { this.cacheId = cacheId; } public void setGuid(final String guid) { this.guid = guid; } public void setName(final String name) { this.name = name; } public void setOwnerDisplayName(final String ownerDisplayName) { this.ownerDisplayName = ownerDisplayName; } public void setOwnerUserId(final String ownerUserId) { this.ownerUserId = ownerUserId; } public void setHint(final String hint) { this.hint = hint; } public void setSize(@NonNull final CacheSize size) { this.size = size; } public void setDifficulty(final float difficulty) { this.difficulty = difficulty; } public void setTerrain(final float terrain) { this.terrain = terrain; } public void setLocation(final String location) { this.location = location; } public void setPersonalNote(final String personalNote) { this.personalNote = StringUtils.trimToNull(personalNote); } public void setDisabled(final boolean disabled) { this.disabled = disabled; } public void setArchived(final boolean archived) { this.archived = archived; } public void setFound(final boolean found) { this.found = found; } public void setAttributes(final List<String> attributes) { this.attributes.clear(); if (attributes != null) { this.attributes.addAll(attributes); } } public void setSpoilers(final List<Image> spoilers) { this.spoilers = spoilers; } public boolean hasSpoilersSet() { return this.spoilers != null; } public void setLogCounts(final Map<LogType, Integer> logCounts) { this.logCounts = logCounts; } /* * (non-Javadoc) * * @see cgeo.geocaching.IBasicCache#getType() * * @returns Never null */ public CacheType getType() { return cacheType.getValue(); } public void setType(final CacheType cacheType) { if (cacheType == null || cacheType == CacheType.ALL) { throw new IllegalArgumentException("Illegal cache type"); } this.cacheType = new UncertainProperty<>(cacheType); } public void setType(final CacheType cacheType, final int zoomlevel) { if (cacheType == null || cacheType == CacheType.ALL) { throw new IllegalArgumentException("Illegal cache type"); } this.cacheType = new UncertainProperty<>(cacheType, zoomlevel); } public boolean hasDifficulty() { return difficulty > 0f; } public boolean hasTerrain() { return terrain > 0f; } /** * @return the storageLocation */ public EnumSet<StorageLocation> getStorageLocation() { return storageLocation; } /** * @param storageLocation * the storageLocation to set */ public void addStorageLocation(final StorageLocation storageLocation) { this.storageLocation.add(storageLocation); } /** * Check if this cache instance comes from or has been stored into the database. */ public boolean inDatabase() { return storageLocation.contains(StorageLocation.DATABASE); } /** * @param waypoint * Waypoint to add to the cache * @param saveToDatabase * Indicates whether to add the waypoint to the database. Should be false if * called while loading or building a cache * @return {@code true} if waypoint successfully added to waypoint database */ public boolean addOrChangeWaypoint(final Waypoint waypoint, final boolean saveToDatabase) { waypoint.setGeocode(geocode); if (waypoint.getId() < 0) { // this is a new waypoint if (StringUtils.isBlank(waypoint.getPrefix())) { assignUniquePrefix(waypoint); } waypoints.add(waypoint); if (waypoint.isFinalWithCoords()) { finalDefined = true; } } else { // this is a waypoint being edited final int index = getWaypointIndex(waypoint); if (index >= 0) { final Waypoint oldWaypoint = waypoints.remove(index); waypoint.setPrefix(oldWaypoint.getPrefix()); //migration if (StringUtils.isBlank(waypoint.getPrefix()) || StringUtils.equalsIgnoreCase(waypoint.getPrefix(), Waypoint.PREFIX_OWN)) { assignUniquePrefix(waypoint); } } waypoints.add(waypoint); // when waypoint was edited, finalDefined may have changed resetFinalDefined(); } return saveToDatabase && DataStore.saveWaypoint(waypoint.getId(), geocode, waypoint); } /* * Assigns a unique two-digit (compatibility with gc.com) * prefix within the scope of this cache. */ private void assignUniquePrefix(final Waypoint waypoint) { // gather existing prefixes final Set<String> assignedPrefixes = new HashSet<>(); for (final Waypoint wp : waypoints) { assignedPrefixes.add(wp.getPrefix()); } for (int i = OWN_WP_PREFIX_OFFSET; i < 100; i++) { final String prefixCandidate = String.valueOf(i); if (!assignedPrefixes.contains(prefixCandidate)) { waypoint.setPrefix(prefixCandidate); return; } } throw new IllegalStateException("too many waypoints, unable to assign unique prefix"); } public boolean hasWaypoints() { return !waypoints.isEmpty(); } public boolean hasFinalDefined() { return finalDefined; } // Only for loading public void setFinalDefined(final boolean finalDefined) { this.finalDefined = finalDefined; } /** * Reset {@code finalDefined} based on current list of stored waypoints */ private void resetFinalDefined() { finalDefined = false; for (final Waypoint wp : waypoints) { if (wp.isFinalWithCoords()) { finalDefined = true; break; } } } public boolean hasUserModifiedCoords() { return userModifiedCoords; } public void setUserModifiedCoords(final boolean coordsChanged) { userModifiedCoords = coordsChanged; } /** * Duplicate a waypoint. * * @param original * the waypoint to duplicate * @return the copy of the waypoint if it was duplicated, {@code null} otherwise (invalid index) */ public Waypoint duplicateWaypoint(final Waypoint original) { if (original == null) { return null; } final int index = getWaypointIndex(original); final Waypoint copy = new Waypoint(original); copy.setUserDefined(); copy.setName(CgeoApplication.getInstance().getString(R.string.waypoint_copy_of) + " " + copy.getName()); waypoints.add(index + 1, copy); return DataStore.saveWaypoint(-1, geocode, copy) ? copy : null; } /** * delete a user defined waypoint * * @param waypoint * to be removed from cache * @return {@code true}, if the waypoint was deleted */ public boolean deleteWaypoint(final Waypoint waypoint) { if (waypoint == null) { return false; } if (waypoint.getId() < 0) { return false; } if (waypoint.getWaypointType() != WaypointType.ORIGINAL) { final int index = getWaypointIndex(waypoint); waypoints.remove(index); DataStore.deleteWaypoint(waypoint.getId()); DataStore.removeCache(geocode, EnumSet.of(RemoveFlag.CACHE)); // Check status if Final is defined if (waypoint.isFinalWithCoords()) { resetFinalDefined(); } return true; } return false; } /** * deletes any waypoint */ public void deleteWaypointForce(final Waypoint waypoint) { final int index = getWaypointIndex(waypoint); waypoints.remove(index); DataStore.deleteWaypoint(waypoint.getId()); DataStore.removeCache(geocode, EnumSet.of(RemoveFlag.CACHE)); resetFinalDefined(); } /** * Find index of given {@code waypoint} in cache's {@code waypoints} list * * @param waypoint * to find index for * @return index in {@code waypoints} if found, -1 otherwise */ private int getWaypointIndex(final Waypoint waypoint) { final int id = waypoint.getId(); for (int index = 0; index < waypoints.size(); index++) { if (waypoints.get(index).getId() == id) { return index; } } return -1; } /** * Lookup a waypoint by its id. * * @param id * the id of the waypoint to look for * @return waypoint or {@code null} */ public Waypoint getWaypointById(final int id) { for (final Waypoint waypoint : waypoints) { if (waypoint.getId() == id) { return waypoint; } } return null; } /** * Detect coordinates in the personal note and add them to user defined waypoints. */ public boolean addWaypointsFromNote() { return addWaypointsFromText(getPersonalNote(), false, CgeoApplication.getInstance().getString(R.string.cache_personal_note)); } /** * Detect coordinates in the given text and add them to user defined waypoints. * * @param text text which might contain coordinates * @param updateDb if true the added waypoints are stored in DB right away * @param namePrefix prefix for waypoint names */ public boolean addWaypointsFromText(@Nullable final String text, final boolean updateDb, @NonNull final String namePrefix) { boolean changed = false; for (final Waypoint waypoint : Waypoint.parseWaypoints(StringUtils.defaultString(text), namePrefix)) { if (!hasIdenticalWaypoint(waypoint.getCoords())) { addOrChangeWaypoint(waypoint, updateDb); changed = true; } } return changed; } private boolean hasIdenticalWaypoint(final Geopoint point) { for (final Waypoint waypoint: waypoints) { // waypoint can have no coords such as a Final set by cache owner final Geopoint coords = waypoint.getCoords(); if (coords != null && coords.equals(point)) { return true; } } return false; } /* * For working in the debugger * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return this.geocode + " " + this.name; } @Override public int hashCode() { return StringUtils.defaultString(geocode).hashCode(); } @Override public boolean equals(final Object obj) { // TODO: explain the following line or remove this non-standard equality method // just compare the geocode even if that is not what "equals" normally does return this == obj || (obj instanceof Geocache && StringUtils.isNotEmpty(geocode) && geocode.equals(((Geocache) obj).geocode)); } public void store() { if (lists.isEmpty()) { lists.add(StoredList.STANDARD_LIST_ID); } storeCache(this, null, lists, false, null); } public void store(final Set<Integer> listIds, final DisposableHandler handler) { lists.clear(); lists.addAll(listIds); storeCache(this, null, lists, false, handler); } @Override public int getId() { return 0; } @Override public WaypointType getWaypointType() { return null; } @Override public CoordinatesType getCoordType() { return CoordinatesType.CACHE; } public Disposable drop(final Handler handler) { return Schedulers.io().scheduleDirect(new Runnable() { @Override public void run() { try { dropSynchronous(); handler.sendMessage(Message.obtain()); } catch (final Exception e) { Log.e("cache.drop: ", e); } } }); } public void dropSynchronous() { DataStore.markDropped(Collections.singletonList(this)); DataStore.removeCache(getGeocode(), EnumSet.of(RemoveFlag.CACHE)); } private void warnIncorrectParsingIf(final boolean incorrect, final String field) { if (incorrect) { Log.w(field + " not parsed correctly for " + geocode); } } private void warnIncorrectParsingIfBlank(final String str, final String field) { warnIncorrectParsingIf(StringUtils.isBlank(str), field); } public void checkFields() { warnIncorrectParsingIfBlank(getGeocode(), "geo"); warnIncorrectParsingIfBlank(getName(), "name"); warnIncorrectParsingIfBlank(getGuid(), "guid"); warnIncorrectParsingIf(getTerrain() == 0.0, "terrain"); warnIncorrectParsingIf(getDifficulty() == 0.0, "difficulty"); warnIncorrectParsingIfBlank(getOwnerDisplayName(), "owner"); warnIncorrectParsingIfBlank(getOwnerUserId(), "owner"); warnIncorrectParsingIf(getHiddenDate() == null, "hidden"); warnIncorrectParsingIf(getFavoritePoints() < 0, "favoriteCount"); warnIncorrectParsingIf(getSize() == CacheSize.UNKNOWN, "size"); warnIncorrectParsingIf(getType() == null || getType() == CacheType.UNKNOWN, "type"); warnIncorrectParsingIf(getCoords() == null, "coordinates"); warnIncorrectParsingIfBlank(getLocation(), "location"); } public Disposable refresh(final DisposableHandler handler, final Scheduler scheduler) { return scheduler.scheduleDirect(new Runnable() { @Override public void run() { refreshSynchronous(handler); } }); } public void refreshSynchronous(final DisposableHandler handler) { refreshSynchronous(handler, lists); } public void refreshSynchronous(final DisposableHandler handler, final Set<Integer> additionalListIds) { final Set<Integer> combinedListIds = new HashSet<>(lists); combinedListIds.addAll(additionalListIds); storeCache(null, geocode, combinedListIds, true, handler); } public static void storeCache(final Geocache origCache, final String geocode, final Set<Integer> lists, final boolean forceRedownload, final DisposableHandler handler) { try { final Geocache cache; // get cache details, they may not yet be complete if (origCache != null) { // only reload the cache if it was already stored or doesn't have full details (by checking the description) if (origCache.isOffline() || StringUtils.isBlank(origCache.getDescription())) { final SearchResult search = searchByGeocode(origCache.getGeocode(), null, false, handler); cache = search != null ? search.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB) : origCache; } else { cache = origCache; } } else if (StringUtils.isNotBlank(geocode)) { final SearchResult search = searchByGeocode(geocode, null, forceRedownload, handler); cache = search != null ? search.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB) : null; } else { cache = null; } if (cache == null) { if (handler != null) { handler.sendMessage(Message.obtain()); } return; } if (DisposableHandler.isDisposed(handler)) { return; } final HtmlImage imgGetter = new HtmlImage(cache.getGeocode(), false, true, forceRedownload); // store images from description if (StringUtils.isNotBlank(cache.getDescription())) { Html.fromHtml(cache.getDescription(), imgGetter, null); } if (DisposableHandler.isDisposed(handler)) { return; } // store spoilers if (CollectionUtils.isNotEmpty(cache.getSpoilers())) { for (final Image oneSpoiler : cache.getSpoilers()) { imgGetter.getDrawable(oneSpoiler.getUrl()); } } if (DisposableHandler.isDisposed(handler)) { return; } // store images from logs if (Settings.isStoreLogImages()) { for (final LogEntry log : cache.getLogs()) { if (log.hasLogImages()) { for (final Image oneLogImg : log.getLogImages()) { imgGetter.getDrawable(oneLogImg.getUrl()); } } } } if (DisposableHandler.isDisposed(handler)) { return; } cache.setLists(lists); DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); if (DisposableHandler.isDisposed(handler)) { return; } StaticMapsProvider.downloadMaps(cache).mergeWith(imgGetter.waitForEndCompletable(handler)).blockingAwait(); if (handler != null) { handler.sendEmptyMessage(DisposableHandler.DONE); } } catch (final Exception e) { Log.e("Geocache.storeCache", e); } } public static SearchResult searchByGeocode(final String geocode, final String guid, final boolean forceReload, final DisposableHandler handler) { if (StringUtils.isBlank(geocode) && StringUtils.isBlank(guid)) { Log.e("Geocache.searchByGeocode: No geocode nor guid given"); return null; } if (!forceReload && (DataStore.isOffline(geocode, guid) || DataStore.isThere(geocode, guid, true))) { final SearchResult search = new SearchResult(); final String realGeocode = StringUtils.isNotBlank(geocode) ? geocode : DataStore.getGeocodeForGuid(guid); search.addGeocode(realGeocode); return search; } // if we have no geocode, we can't dynamically select the handler, but must explicitly use GC if (geocode == null) { return GCConnector.getInstance().searchByGeocode(null, guid, handler); } final IConnector connector = ConnectorFactory.getConnector(geocode); if (connector instanceof ISearchByGeocode) { return ((ISearchByGeocode) connector).searchByGeocode(geocode, guid, handler); } return null; } public boolean isOffline() { return !lists.isEmpty() && (lists.size() > 1 || lists.iterator().next() != StoredList.TEMPORARY_LIST.id); } /** * guess an event start time from the description * * @return start time in minutes after midnight */ public int guessEventTimeMinutes() { if (!isEventCache()) { return -1; } final String hourLocalized = CgeoApplication.getInstance().getString(R.string.cache_time_full_hours); final List<Pattern> patterns = new ArrayList<>(); // 12:34 patterns.add(Pattern.compile("\\b(\\d{1,2})\\:(\\d\\d)\\b")); if (StringUtils.isNotBlank(hourLocalized)) { // 12:34o'clock patterns.add(Pattern.compile("\\b(\\d{1,2})\\:(\\d\\d)" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); // 17 - 20 o'clock patterns.add(Pattern.compile("\\b(\\d{1,2})(?:\\.00)?" + "\\s*(?:-|[a-z]+)\\s?" + "(?:\\d{1,2})(?:\\.00)?" + "\\s?" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); // 12 o'clock, 12.00 o'clock patterns.add(Pattern.compile("\\b(\\d{1,2})(?:\\.(00|15|30|45))?\\s?" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); } final String searchText = getShortDescription() + ' ' + getDescription(); int start = -1; int eventTimeMinutes = -1; for (final Pattern pattern : patterns) { final MatcherWrapper matcher = new MatcherWrapper(pattern, searchText); while (matcher.find()) { try { final int hours = Integer.parseInt(matcher.group(1)); int minutes = 0; if (matcher.groupCount() >= 2 && StringUtils.isNotEmpty(matcher.group(2))) { minutes = Integer.parseInt(matcher.group(2)); } if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60 && (eventTimeMinutes == -1 || matcher.start() < start)) { eventTimeMinutes = hours * 60 + minutes; start = matcher.start(); } } catch (final NumberFormatException ignored) { // cannot happen, but static code analysis doesn't know } } } return eventTimeMinutes; } public boolean hasStaticMap() { return StaticMapsProvider.hasStaticMap(this); } @NonNull public Collection<Image> getImages() { final List<Image> result = new LinkedList<>(getSpoilers()); addLocalSpoilersTo(result); for (final LogEntry log : getLogs()) { result.addAll(log.getLogImages()); } ImageUtils.addImagesFromHtml(result, geocode, getShortDescription(), getDescription()); return result; } @NonNull public Collection<Image> getNonStaticImages() { final ArrayList<Image> result = new ArrayList<>(); for (final Image image : getImages()) { // search strings fit geocaching.com and opencaching, may need to add others // Xiaomi does not support java.lang.CharSequence#containsAny(java.lang.CharSequence[]), // which is called by StringUtils.containsAny(CharSequence, CharSequence...). // Thus, we have to use StringUtils.contains(...) instead (see issue #5766). final String url = image.getUrl(); if (!StringUtils.contains(url, "/static") && !StringUtils.contains(url, "/resource") && !StringUtils.contains(url, "/icons/")) { result.add(image); } } return result; } /** * Add spoilers stored locally in <tt>/sdcard/cgeo/GeocachePhotos</tt>. If a cache is named GC123ABC, the * directory will be <tt>/sdcard/cgeo/GeocachePhotos/C/B/GC123ABC/</tt>. * * @param spoilers the list to add to */ private void addLocalSpoilersTo(final List<Image> spoilers) { if (StringUtils.length(geocode) >= 2) { final String suffix = StringUtils.right(geocode, 2); final File lastCharDir = new File(LocalStorage.getLocalSpoilersDirectory(), suffix.substring(1)); final File secondToLastCharDir = new File(lastCharDir, suffix.substring(0, 1)); final File finalDir = new File(secondToLastCharDir, geocode); final File[] files = finalDir.listFiles(); if (files != null) { for (final File image : files) { spoilers.add(new Image.Builder() .setUrl("file://" + image.getAbsolutePath()) .setTitle(image.getName()) .build()); } } } } public void setDetailedUpdatedNow() { final long now = System.currentTimeMillis(); setUpdated(now); setDetailedUpdate(now); setDetailed(true); } /** * Gets whether the user has logged the specific log type for this cache. Only checks the currently stored logs of * the cache, so the result might be wrong. */ public boolean hasOwnLog(final LogType logType) { for (final LogEntry logEntry : getLogs()) { if (logEntry.getType() == logType && logEntry.isOwn()) { return true; } } return false; } public int getMapMarkerId() { return getConnector().getCacheMapMarkerId(isDisabled() || isArchived()); } public boolean isLogPasswordRequired() { return logPasswordRequired; } public void setLogPasswordRequired(final boolean required) { logPasswordRequired = required; } public String getWaypointGpxId(final String prefix) { return getConnector().getWaypointGpxId(prefix, geocode); } @NonNull public String getWaypointPrefix(final String name) { return getConnector().getWaypointPrefix(name); } /** * Get number of overall finds for a cache, or 0 if the number of finds is not known. */ public int getFindsCount() { if (getLogCounts().isEmpty()) { setLogCounts(inDatabase() ? DataStore.loadLogCounts(getGeocode()) : Collections.<LogType, Integer>emptyMap()); } final Integer logged = getLogCounts().get(LogType.FOUND_IT); if (logged != null) { return logged; } return 0; } public boolean applyDistanceRule() { return (getType().applyDistanceRule() || hasUserModifiedCoords()) && getConnector() == GCConnector.getInstance(); } @NonNull public LogType getDefaultLogType() { if (isEventCache()) { final Date eventDate = getHiddenDate(); final boolean expired = CalendarUtils.isPastEvent(this); if (hasOwnLog(LogType.WILL_ATTEND) || expired || (eventDate != null && CalendarUtils.daysSince(eventDate.getTime()) == 0)) { return hasOwnLog(LogType.ATTENDED) ? LogType.NOTE : LogType.ATTENDED; } return LogType.WILL_ATTEND; } if (isFound()) { return LogType.NOTE; } if (getType() == CacheType.WEBCAM) { return LogType.WEBCAM_PHOTO_TAKEN; } return LogType.FOUND_IT; } /** * Get the geocodes of a collection of caches. * * @param caches a collection of caches * @return the non-blank geocodes of the caches */ @NonNull public static Set<String> getGeocodes(@NonNull final Collection<Geocache> caches) { final Set<String> geocodes = new HashSet<>(caches.size()); for (final Geocache cache : caches) { final String geocode = cache.getGeocode(); if (StringUtils.isNotBlank(geocode)) { geocodes.add(geocode); } } return geocodes; } /** * Show the hint as toast message. If no hint is available, a default "no hint available" will be shown instead. */ public void showHintToast(final Activity activity) { final String hint = getHint(); ActivityMixin.showToast(activity, StringUtils.defaultIfBlank(hint, activity.getString(R.string.cache_hint_not_available))); } public GeoitemRef getGeoitemRef() { return new GeoitemRef(getGeocode(), getCoordType(), getGeocode(), 0, getName(), getType().markerId); } }