diff options
Diffstat (limited to 'main/src/cgeo/geocaching/connector')
11 files changed, 333 insertions, 228 deletions
diff --git a/main/src/cgeo/geocaching/connector/ConnectorFactory.java b/main/src/cgeo/geocaching/connector/ConnectorFactory.java index 1f23df8..a6deba9 100644 --- a/main/src/cgeo/geocaching/connector/ConnectorFactory.java +++ b/main/src/cgeo/geocaching/connector/ConnectorFactory.java @@ -13,7 +13,6 @@ import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.connector.ec.ECConnector; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.MapTokens; -import cgeo.geocaching.connector.oc.OCApiConnector; import cgeo.geocaching.connector.oc.OCApiConnector.ApiSupport; import cgeo.geocaching.connector.oc.OCApiLiveConnector; import cgeo.geocaching.connector.oc.OCConnector; @@ -45,7 +44,9 @@ public final class ConnectorFactory { R.string.oc_de_okapi_consumer_key, R.string.oc_de_okapi_consumer_secret, R.string.pref_connectorOCActive, R.string.pref_ocde_tokenpublic, R.string.pref_ocde_tokensecret, ApiSupport.current), new OCConnector("OpenCaching.CZ", "www.opencaching.cz", "OZ"), - new OCApiConnector("OpenCaching.CO.UK", "www.opencaching.org.uk", "OK", "arU4okouc4GEjMniE2fq", "CC BY-NC-SA 2.5", ApiSupport.oldapi), + new OCApiLiveConnector("opencaching.org.uk", "www.opencaching.org.uk", "OK", "CC BY-NC-SA 2.5", + R.string.oc_uk_okapi_consumer_key, R.string.oc_uk_okapi_consumer_secret, + R.string.pref_connectorOCUKActive, R.string.pref_ocuk_tokenpublic, R.string.pref_ocuk_tokensecret, ApiSupport.oldapi), new OCConnector("OpenCaching.ES", "www.opencachingspain.es", "OC"), new OCConnector("OpenCaching.IT", "www.opencaching.it", "OC"), new OCConnector("OpenCaching.JP", "www.opencaching.jp", "OJ"), @@ -143,16 +144,16 @@ public final class ConnectorFactory { } public static @NonNull - IConnector getConnector(ICache cache) { + IConnector getConnector(final ICache cache) { return getConnector(cache.getGeocode()); } - public static TrackableConnector getConnector(Trackable trackable) { + public static TrackableConnector getConnector(final Trackable trackable) { return getTrackableConnector(trackable.getGeocode()); } @NonNull - public static TrackableConnector getTrackableConnector(String geocode) { + public static TrackableConnector getTrackableConnector(final String geocode) { for (final TrackableConnector connector : TRACKABLE_CONNECTORS) { if (connector.canHandleTrackable(geocode)) { return connector; @@ -188,7 +189,7 @@ public final class ConnectorFactory { public static SearchResult searchByViewport(final @NonNull Viewport viewport, final MapTokens tokens) { return SearchResult.parallelCombineActive(searchByViewPortConns, new Func1<ISearchByViewPort, SearchResult>() { @Override - public SearchResult call(ISearchByViewPort connector) { + public SearchResult call(final ISearchByViewPort connector) { return connector.searchByViewport(viewport, tokens); } }); diff --git a/main/src/cgeo/geocaching/connector/ec/ECApi.java b/main/src/cgeo/geocaching/connector/ec/ECApi.java index 702e557..5202184 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECApi.java +++ b/main/src/cgeo/geocaching/connector/ec/ECApi.java @@ -205,7 +205,7 @@ public class ECApi { cache.setTerrain((float) response.getDouble("terrain")); cache.setSize(CacheSize.getById(response.getString("size"))); cache.setFound(response.getInt("found") == 1); - DataStore.saveCache(cache, EnumSet.of(SaveFlag.SAVE_CACHE)); + DataStore.saveCache(cache, EnumSet.of(SaveFlag.CACHE)); } catch (final JSONException e) { Log.e("ECApi.parseCache", e); return null; diff --git a/main/src/cgeo/geocaching/connector/gc/GCConnector.java b/main/src/cgeo/geocaching/connector/gc/GCConnector.java index 925f6f0..294e969 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConnector.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConnector.java @@ -50,7 +50,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, private static final String CACHE_URL_SHORT = "http://coord.info/"; // Double slash is used to force open in browser - private static final String CACHE_URL_LONG = "http://www.geocaching.com//seek/cache_details.aspx?wp="; + private static final String CACHE_URL_LONG = "http://www.geocaching.com/seek/cache_details.aspx?wp="; /** * Pocket queries downloaded from the website use a numeric prefix. The pocket query creator Android app adds a * verbatim "pocketquery" prefix. @@ -192,8 +192,8 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, @Override public boolean isOwner(final ICache cache) { - return StringUtils.equalsIgnoreCase(cache.getOwnerUserId(), Settings.getUsername()); - + final String user = Settings.getUsername(); + return StringUtils.isNotEmpty(user) && StringUtils.equalsIgnoreCase(cache.getOwnerUserId(), user); } @Override diff --git a/main/src/cgeo/geocaching/connector/gc/GCMap.java b/main/src/cgeo/geocaching/connector/gc/GCMap.java index dc2408f..dd81507 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCMap.java +++ b/main/src/cgeo/geocaching/connector/gc/GCMap.java @@ -146,30 +146,6 @@ public class GCMap { throw new JSONException("No data inside JSON"); } - /* - * Optimization: the grid can get ignored. The keys are the grid position in the format x_y - * It's not used at the moment due to optimizations - * But maybe we need it some day... - * - * // attach all keys with the cache positions in the tile - * Map<String, UTFGridPosition> keyPositions = new HashMap<String, UTFGridPosition>(); // JSON key, (x/y) in - * grid - * for (int y = 0; y < grid.length(); y++) { - * String rowUTF8 = grid.getString(y); - * if (rowUTF8.length() != (UTFGrid.GRID_MAXX + 1)) { - * throw new JSONException("Grid has wrong size"); - * } - * - * for (int x = 0; x < UTFGrid.GRID_MAXX; x++) { - * char c = rowUTF8.charAt(x); - * if (c != ' ') { - * short id = UTFGrid.getUTFGridId(c); - * keyPositions.put(keys.getString(id), new UTFGridPosition(x, y)); - * } - * } - * } - */ - // iterate over the data and construct all caches in this tile Map<String, List<UTFGridPosition>> positions = new HashMap<String, List<UTFGridPosition>>(); // JSON id as key Map<String, List<UTFGridPosition>> singlePositions = new HashMap<String, List<UTFGridPosition>>(); // JSON id as key diff --git a/main/src/cgeo/geocaching/connector/gc/GCParser.java b/main/src/cgeo/geocaching/connector/gc/GCParser.java index ec3a4bd..26c8175 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCParser.java +++ b/main/src/cgeo/geocaching/connector/gc/GCParser.java @@ -32,6 +32,7 @@ import cgeo.geocaching.ui.DirectionImage; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; +import cgeo.geocaching.utils.RxUtils; import cgeo.geocaching.utils.SynchronizedDateFormat; import cgeo.geocaching.utils.TextUtils; @@ -47,6 +48,13 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func2; + import android.net.Uri; import android.text.Html; @@ -136,7 +144,6 @@ public abstract class GCParser { while (matcherGuidAndDisabled.find()) { if (matcherGuidAndDisabled.groupCount() > 0) { - //cache.setGuid(matcherGuidAndDisabled.group(1)); if (matcherGuidAndDisabled.group(2) != null) { cache.setName(Html.fromHtml(matcherGuidAndDisabled.group(2).trim()).toString()); } @@ -335,32 +342,50 @@ public abstract class GCParser { } static SearchResult parseCache(final String page, final CancellableHandler handler) { - final SearchResult searchResult = parseCacheFromText(page, handler); + final ImmutablePair<StatusCode, Geocache> parsed = parseCacheFromText(page, handler); // attention: parseCacheFromText already stores implicitly through searchResult.addCache - if (searchResult != null && !searchResult.getGeocodes().isEmpty()) { - final Geocache cache = searchResult.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB); - if (cache == null) { - return null; - } - getExtraOnlineInfo(cache, page, handler); - // too late: it is already stored through parseCacheFromText - cache.setDetailedUpdatedNow(); - if (CancellableHandler.isCancelled(handler)) { - return null; - } + if (parsed.left != StatusCode.NO_ERROR) { + return new SearchResult(parsed.left); + } + + final Geocache cache = parsed.right; + getExtraOnlineInfo(cache, page, handler); + // too late: it is already stored through parseCacheFromText + cache.setDetailedUpdatedNow(); + if (CancellableHandler.isCancelled(handler)) { + return null; + } - // save full detailed caches - CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_cache); - DataStore.saveCache(cache, EnumSet.of(SaveFlag.SAVE_DB)); + // save full detailed caches + CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_cache); + DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); - // update progress message so user knows we're still working. This is more of a place holder than - // actual indication of what the program is doing - CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_render); + // update progress message so user knows we're still working. This is more of a place holder than + // actual indication of what the program is doing + CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_render); + return new SearchResult(cache); + } + + static SearchResult parseAndSaveCacheFromText(final String page, @Nullable final CancellableHandler handler) { + final ImmutablePair<StatusCode, Geocache> parsed = parseCacheFromText(page, handler); + final SearchResult result = new SearchResult(parsed.left); + if (parsed.left == StatusCode.NO_ERROR) { + result.addAndPutInCache(Collections.singletonList(parsed.right)); + DataStore.saveLogsWithoutTransaction(parsed.right.getGeocode(), getLogsFromDetails(page).toBlocking().toIterable()); } - return searchResult; + return result; } - static SearchResult parseCacheFromText(final String pageIn, final CancellableHandler handler) { + /** + * Parse cache from text and return either an error code or a cache object in a pair. Note that inline logs are + * not parsed nor saved, while the cache itself is. + * + * @param pageIn the page text to parse + * @param handler the handler to send the progress notifications to + * @return a pair, with a {@link StatusCode} on the left, and a non-nulll cache objet on the right + * iff the status code is {@link StatusCode.NO_ERROR}. + */ + static private ImmutablePair<StatusCode, Geocache> parseCacheFromText(final String pageIn, @Nullable final CancellableHandler handler) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_details); if (StringUtils.isBlank(pageIn)) { @@ -368,22 +393,17 @@ public abstract class GCParser { return null; } - final SearchResult searchResult = new SearchResult(); - if (pageIn.contains(GCConstants.STRING_UNPUBLISHED_OTHER) || pageIn.contains(GCConstants.STRING_UNPUBLISHED_FROM_SEARCH)) { - searchResult.setError(StatusCode.UNPUBLISHED_CACHE); - return searchResult; + return ImmutablePair.of(StatusCode.UNPUBLISHED_CACHE, null); } if (pageIn.contains(GCConstants.STRING_PREMIUMONLY_1) || pageIn.contains(GCConstants.STRING_PREMIUMONLY_2)) { - searchResult.setError(StatusCode.PREMIUM_ONLY); - return searchResult; + return ImmutablePair.of(StatusCode.PREMIUM_ONLY, null); } final String cacheName = Html.fromHtml(TextUtils.getMatch(pageIn, GCConstants.PATTERN_NAME, true, "")).toString(); if (GCConstants.STRING_UNKNOWN_ERROR.equalsIgnoreCase(cacheName)) { - searchResult.setError(StatusCode.UNKNOWN_ERROR); - return searchResult; + return ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); } // first handle the content with line breaks, then trim everything for easier matching and reduced memory consumption in parsed fields @@ -726,14 +746,11 @@ public abstract class GCParser { // last check for necessary cache conditions if (StringUtils.isBlank(cache.getGeocode())) { - searchResult.setError(StatusCode.UNKNOWN_ERROR); - return searchResult; + return ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); } cache.setDetailedUpdatedNow(); - searchResult.addAndPutInCache(Collections.singletonList(cache)); - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), getLogsFromDetails(page, false)); - return searchResult; + return ImmutablePair.of(StatusCode.NO_ERROR, cache); } private static String getNumberString(final String numberWithPunctuation) { @@ -1619,116 +1636,142 @@ public abstract class GCParser { * * @param page * the text of the details page - * @param friends - * return friends logs only (will require a network request) - * @return a list of log entries or <code>null</code> if the logs could not be retrieved + * @return a list of log entries which will be empty if the logs could not be retrieved * */ - @Nullable - private static List<LogEntry> getLogsFromDetails(final String page, final boolean friends) { - String rawResponse; + @NonNull + private static Observable<LogEntry> getLogsFromDetails(final String page) { + // extract embedded JSON data from page + return parseLogs(false, TextUtils.getMatch(page, GCConstants.PATTERN_LOGBOOK, "")); + } - if (friends) { - final MatcherWrapper userTokenMatcher = new MatcherWrapper(GCConstants.PATTERN_USERTOKEN, page); - if (!userTokenMatcher.find()) { - Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); - return null; - } + private enum SpecialLogs { + FRIENDS("sf"), + OWN("sp"); - final String userToken = userTokenMatcher.group(1); - final Parameters params = new Parameters( - "tkn", userToken, - "idx", "1", - "num", String.valueOf(GCConstants.NUMBER_OF_LOGS), - "decrypt", "true", - // "sp", Boolean.toString(personal), // personal logs - "sf", Boolean.toString(friends)); + final String paramName; - final HttpResponse response = Network.getRequest("http://www.geocaching.com/seek/geocache.logbook", params); - if (response == null) { - Log.e("GCParser.loadLogsFromDetails: cannot log logs, response is null"); - return null; - } - final int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode != 200) { - Log.e("GCParser.loadLogsFromDetails: error " + statusCode + " when requesting log information"); - return null; - } - rawResponse = Network.getResponseData(response); - if (rawResponse == null) { - Log.e("GCParser.loadLogsFromDetails: unable to read whole response"); - return null; - } - } else { - // extract embedded JSON data from page - rawResponse = TextUtils.getMatch(page, GCConstants.PATTERN_LOGBOOK, ""); + SpecialLogs(String paramName) { + this.paramName = paramName; } - return parseLogs(friends, rawResponse); + private String getParamName() { + return this.paramName; + } } - private static List<LogEntry> parseLogs(final boolean friends, String rawResponse) { - final List<LogEntry> logs = new ArrayList<LogEntry>(); - - // for non logged in users the log book is not shown - if (StringUtils.isBlank(rawResponse)) { - return logs; - } + /** + * Extract special logs (friends, own) through seperate request. + * + * @param page + * The page to extrat userToken from + * @param logType + * The logType to request + * @return Observable<LogEntry> The logs + */ + private static Observable<LogEntry> getSpecialLogs(final String page, final SpecialLogs logType) { + return Observable.defer(new Func0<Observable<? extends LogEntry>>() { + @Override + public Observable<? extends LogEntry> call() { + final MatcherWrapper userTokenMatcher = new MatcherWrapper(GCConstants.PATTERN_USERTOKEN, page); + if (!userTokenMatcher.find()) { + Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); + return Observable.empty(); + } - try { - final JSONObject resp = new JSONObject(rawResponse); - if (!resp.getString("status").equals("success")) { - Log.e("GCParser.loadLogsFromDetails: status is " + resp.getString("status")); - return null; + final String userToken = userTokenMatcher.group(1); + final Parameters params = new Parameters( + "tkn", userToken, + "idx", "1", + "num", String.valueOf(GCConstants.NUMBER_OF_LOGS), + logType.getParamName(), Boolean.toString(Boolean.TRUE), + "decrypt", "true"); + final HttpResponse response = Network.getRequest("http://www.geocaching.com/seek/geocache.logbook", params); + if (response == null) { + Log.e("GCParser.loadLogsFromDetails: cannot log logs, response is null"); + return Observable.empty(); + } + final int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + Log.e("GCParser.loadLogsFromDetails: error " + statusCode + " when requesting log information"); + return Observable.empty(); + } + String rawResponse = Network.getResponseData(response); + if (rawResponse == null) { + Log.e("GCParser.loadLogsFromDetails: unable to read whole response"); + return Observable.empty(); + } + return parseLogs(true, rawResponse); } + }).subscribeOn(RxUtils.networkScheduler); + } - final JSONArray data = resp.getJSONArray("data"); + private static Observable<LogEntry> parseLogs(final boolean markAsFriendsLog, final String rawResponse) { + return Observable.create(new OnSubscribe<LogEntry>() { + @Override + public void call(final Subscriber<? super LogEntry> subscriber) { + // for non logged in users the log book is not shown + if (StringUtils.isBlank(rawResponse)) { + subscriber.onCompleted(); + return; + } - for (int index = 0; index < data.length(); index++) { - final JSONObject entry = data.getJSONObject(index); + try { + final JSONObject resp = new JSONObject(rawResponse); + if (!resp.getString("status").equals("success")) { + Log.e("GCParser.loadLogsFromDetails: status is " + resp.getString("status")); + subscriber.onCompleted(); + return; + } - // FIXME: use the "LogType" field instead of the "LogTypeImage" one. - final String logIconNameExt = entry.optString("LogTypeImage", ".gif"); - final String logIconName = logIconNameExt.substring(0, logIconNameExt.length() - 4); + final JSONArray data = resp.getJSONArray("data"); - long date = 0; - try { - date = GCLogin.parseGcCustomDate(entry.getString("Visited")).getTime(); - } catch (final ParseException e) { - Log.e("GCParser.loadLogsFromDetails: failed to parse log date."); - } + for (int index = 0; index < data.length(); index++) { + final JSONObject entry = data.getJSONObject(index); - // TODO: we should update our log data structure to be able to record - // proper coordinates, and make them clickable. In the meantime, it is - // better to integrate those coordinates into the text rather than not - // display them at all. - final String latLon = entry.getString("LatLonString"); - final String logText = (StringUtils.isEmpty(latLon) ? "" : (latLon + "<br/><br/>")) + TextUtils.removeControlCharacters(entry.getString("LogText")); - final LogEntry logDone = new LogEntry( - TextUtils.removeControlCharacters(entry.getString("UserName")), - date, - LogType.getByIconName(logIconName), - logText); - logDone.found = entry.getInt("GeocacheFindCount"); - logDone.friend = friends; - - final JSONArray images = entry.getJSONArray("Images"); - for (int i = 0; i < images.length(); i++) { - final JSONObject image = images.getJSONObject(i); - final String url = "http://imgcdn.geocaching.com/cache/log/large/" + image.getString("FileName"); - final String title = TextUtils.removeControlCharacters(image.getString("Name")); - final Image logImage = new Image(url, title); - logDone.addLogImage(logImage); - } + // FIXME: use the "LogType" field instead of the "LogTypeImage" one. + final String logIconNameExt = entry.optString("LogTypeImage", ".gif"); + final String logIconName = logIconNameExt.substring(0, logIconNameExt.length() - 4); - logs.add(logDone); - } - } catch (final JSONException e) { - // failed to parse logs - Log.w("GCParser.loadLogsFromDetails: Failed to parse cache logs", e); - } + long date = 0; + try { + date = GCLogin.parseGcCustomDate(entry.getString("Visited")).getTime(); + } catch (final ParseException e) { + Log.e("GCParser.loadLogsFromDetails: failed to parse log date."); + } - return logs; + // TODO: we should update our log data structure to be able to record + // proper coordinates, and make them clickable. In the meantime, it is + // better to integrate those coordinates into the text rather than not + // display them at all. + final String latLon = entry.getString("LatLonString"); + final String logText = (StringUtils.isEmpty(latLon) ? "" : (latLon + "<br/><br/>")) + TextUtils.removeControlCharacters(entry.getString("LogText")); + final LogEntry logDone = new LogEntry( + TextUtils.removeControlCharacters(entry.getString("UserName")), + date, + LogType.getByIconName(logIconName), + logText); + logDone.found = entry.getInt("GeocacheFindCount"); + logDone.friend = markAsFriendsLog; + + final JSONArray images = entry.getJSONArray("Images"); + for (int i = 0; i < images.length(); i++) { + final JSONObject image = images.getJSONObject(i); + final String url = "http://imgcdn.geocaching.com/cache/log/large/" + image.getString("FileName"); + final String title = TextUtils.removeControlCharacters(image.getString("Name")); + final Image logImage = new Image(url, title); + logDone.addLogImage(logImage); + } + + subscriber.onNext(logDone); + } + } catch (final JSONException e) { + // failed to parse logs + Log.w("GCParser.loadLogsFromDetails: Failed to parse cache logs", e); + } + subscriber.onCompleted(); + } + }); } @NonNull @@ -1820,32 +1863,39 @@ public abstract class GCParser { } private static void getExtraOnlineInfo(final Geocache cache, final String page, final CancellableHandler handler) { + // This method starts the page parsing for logs in the background, as well as retrieve the friends and own logs + // if requested. It merges them and stores them in the background, while the rating is retrieved if needed and + // stored. Then we wait for the log merging and saving to be completed before returning. if (CancellableHandler.isCancelled(handler)) { return; } - //cache.setLogs(loadLogsFromDetails(page, cache, false)); + final Observable<LogEntry> logs = getLogsFromDetails(page).subscribeOn(RxUtils.computationScheduler); + Observable<LogEntry> specialLogs; if (Settings.isFriendLogsWanted()) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); - final List<LogEntry> friendLogs = getLogsFromDetails(page, true); - if (friendLogs != null && !friendLogs.isEmpty()) { - // create new list, as the existing log list is immutable - ArrayList<LogEntry> mergedLogs = new ArrayList<LogEntry>(cache.getLogs()); - for (final LogEntry log : friendLogs) { - if (mergedLogs.contains(log)) { - mergedLogs.get(mergedLogs.indexOf(log)).friend = true; - } else { - mergedLogs.add(log); + specialLogs = Observable.merge(getSpecialLogs(page, SpecialLogs.FRIENDS), + getSpecialLogs(page, SpecialLogs.OWN)); + } else { + CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); + specialLogs = Observable.empty(); + } + final Observable<List<LogEntry>> mergedLogs = Observable.zip(logs.toList(), specialLogs.toList(), + new Func2<List<LogEntry>, List<LogEntry>, List<LogEntry>>() { + @Override + public List<LogEntry> call(final List<LogEntry> logEntries, final List<LogEntry> specialLogEntries) { + mergeFriendsLogs(logEntries, specialLogEntries); + return logEntries; } - } - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), mergedLogs); - } - } - - if (Settings.isRatingWanted()) { - if (CancellableHandler.isCancelled(handler)) { - return; - } + }).cache(); + mergedLogs.subscribe(new Action1<List<LogEntry>>() { + @Override + public void call(final List<LogEntry> logEntries) { + DataStore.saveLogsWithoutTransaction(cache.getGeocode(), logEntries); + } + }); + + if (Settings.isRatingWanted() && !CancellableHandler.isCancelled(handler)) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_gcvote); final GCVoteRating rating = GCVote.getRating(cache.getGuid(), cache.getGeocode()); if (rating != null) { @@ -1854,6 +1904,28 @@ public abstract class GCParser { cache.setMyVote(rating.getMyVote()); } } + + // Wait for completion of logs parsing, retrieving and merging + mergedLogs.toBlocking().last(); + } + + /** + * Merge log entries and mark them as friends logs (personal and friends) to identify + * them on friends/personal logs tab. + * + * @param mergedLogs + * the list to merge logs with + * @param logsToMerge + * the list of logs to merge + */ + private static void mergeFriendsLogs(final List<LogEntry> mergedLogs, final Iterable<LogEntry> logsToMerge) { + for (final LogEntry log : logsToMerge) { + if (mergedLogs.contains(log)) { + mergedLogs.get(mergedLogs.indexOf(log)).friend = true; + } else { + mergedLogs.add(log); + } + } } public static boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt) { diff --git a/main/src/cgeo/geocaching/connector/gc/IconDecoder.java b/main/src/cgeo/geocaching/connector/gc/IconDecoder.java index c7b470a..c6a2afc 100644 --- a/main/src/cgeo/geocaching/connector/gc/IconDecoder.java +++ b/main/src/cgeo/geocaching/connector/gc/IconDecoder.java @@ -35,7 +35,7 @@ public abstract class IconDecoder { return false; //out of image position } - int numberOfDetections = 7; //for level 12 and 13; + int numberOfDetections = 7; //for level 12 and 13 if (zoomlevel < 12) { numberOfDetections = 5; } diff --git a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java index 7cced74..6095514 100644 --- a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java +++ b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java @@ -1,17 +1,21 @@ package cgeo.geocaching.connector.gc; +import butterknife.ButterKnife; + import cgeo.geocaching.R; import cgeo.geocaching.loaders.RecaptchaReceiver; import cgeo.geocaching.network.Network; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; import org.apache.commons.io.IOUtils; + import rx.Observable; import rx.android.observables.AndroidObservable; import rx.functions.Action1; import rx.functions.Func0; -import rx.schedulers.Schedulers; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; @@ -57,7 +61,7 @@ public class RecaptchaHandler extends Handler { return Observable.empty(); } }); - AndroidObservable.bindActivity(activity, captcha).subscribe(new Action1<Bitmap>() { + AndroidObservable.bindActivity(activity, captcha).subscribeOn(RxUtils.networkScheduler).subscribe(new Action1<Bitmap>() { @Override public void call(final Bitmap bitmap) { imageView.setImageBitmap(bitmap); @@ -67,23 +71,24 @@ public class RecaptchaHandler extends Handler { public void call(final Throwable throwable) { // Do nothing } - }, Schedulers.io()); + }); reloadButton.setEnabled(true); } + @SuppressLint("InflateParams") @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { if (msg.what == SHOW_CAPTCHA) { final AlertDialog.Builder dlg = new AlertDialog.Builder(activity); - final View view = activity.getLayoutInflater().inflate(R.layout.recaptcha_dialog, null); + final View view = activity.getLayoutInflater().inflate(R.layout.recaptcha_dialog, null, false); - final ImageView imageView = (ImageView) view.findViewById(R.id.image); + final ImageView imageView = ButterKnife.findById(view, R.id.image); - final ImageButton reloadButton = (ImageButton) view.findViewById(R.id.button_recaptcha_refresh); + final ImageButton reloadButton = ButterKnife.findById(view, R.id.button_recaptcha_refresh); reloadButton.setEnabled(false); reloadButton.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { recaptchaReceiver.fetchChallenge(); loadChallenge(imageView, reloadButton); } @@ -95,8 +100,9 @@ public class RecaptchaHandler extends Handler { dlg.setView(view); dlg.setNeutralButton(activity.getString(R.string.caches_recaptcha_continue), new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int id) { - final String text = ((EditText) view.findViewById(R.id.text)).getText().toString(); + public void onClick(final DialogInterface dialog, final int id) { + final EditText editText = ButterKnife.findById(view, R.id.text); + final String text = editText.getText().toString(); recaptchaReceiver.setText(text); dialog.cancel(); } diff --git a/main/src/cgeo/geocaching/connector/gc/Tile.java b/main/src/cgeo/geocaching/connector/gc/Tile.java index ca70111..d7b3a48 100644 --- a/main/src/cgeo/geocaching/connector/gc/Tile.java +++ b/main/src/cgeo/geocaching/connector/gc/Tile.java @@ -9,6 +9,7 @@ import cgeo.geocaching.utils.LeastRecentlyUsedSet; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; + import org.eclipse.jdt.annotation.NonNull; import android.graphics.Bitmap; @@ -88,10 +89,6 @@ public class Tile { * */ private static int calcY(final Geopoint origin, final int zoomlevel) { - - // double latRad = Math.toRadians(origin.getLatitude()); - // return (int) ((1 - (Math.log(Math.tan(latRad) + (1 / Math.cos(latRad))) / Math.PI)) / 2 * numberOfTiles); - // Optimization from Bing double sinLatRad = Math.sin(Math.toRadians(origin.getLatitude())); // The cut of the fractional part instead of rounding to the nearest integer is intentional and part of the algorithm diff --git a/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java b/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java index 049c633..dd25c5e 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java +++ b/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java @@ -3,6 +3,7 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; +import cgeo.geocaching.ICache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.ILoggingManager; @@ -34,7 +35,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente private final int tokenSecretPrefKeyId; private UserInfo userInfo = new UserInfo(StringUtils.EMPTY, 0, UserInfoStatus.NOT_RETRIEVED); - public OCApiLiveConnector(String name, String host, String prefix, String licenseString, int cKResId, int cSResId, int isActivePrefKeyId, int tokenPublicPrefKeyId, int tokenSecretPrefKeyId, ApiSupport apiSupport) { + public OCApiLiveConnector(final String name, final String host, final String prefix, final String licenseString, final int cKResId, final int cSResId, final int isActivePrefKeyId, final int tokenPublicPrefKeyId, final int tokenSecretPrefKeyId, final ApiSupport apiSupport) { super(name, host, prefix, CryptUtils.rot13(CgeoApplication.getInstance().getResources().getString(cKResId)), licenseString, apiSupport); cS = CryptUtils.rot13(CgeoApplication.getInstance().getResources().getString(cSResId)); @@ -49,22 +50,22 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public SearchResult searchByViewport(@NonNull Viewport viewport, MapTokens tokens) { + public SearchResult searchByViewport(@NonNull final Viewport viewport, final MapTokens tokens) { return new SearchResult(OkapiClient.getCachesBBox(viewport, this)); } @Override - public SearchResult searchByCenter(@NonNull Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { + public SearchResult searchByCenter(@NonNull final Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { return new SearchResult(OkapiClient.getCachesAround(center, this)); } @Override - public SearchResult searchByOwner(@NonNull String username, final @NonNull RecaptchaReceiver recaptchaReceiver) { + public SearchResult searchByOwner(@NonNull final String username, final @NonNull RecaptchaReceiver recaptchaReceiver) { return new SearchResult(OkapiClient.getCachesByOwner(username, this)); } @Override - public SearchResult searchByFinder(@NonNull String username, final @NonNull RecaptchaReceiver recaptchaReceiver) { + public SearchResult searchByFinder(@NonNull final String username, final @NonNull RecaptchaReceiver recaptchaReceiver) { return new SearchResult(OkapiClient.getCachesByFinder(username, this)); } @@ -94,11 +95,11 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente @Override public boolean supportsWatchList() { - return true; + return ApiSupport.current == getApiSupport(); } @Override - public boolean addToWatchlist(Geocache cache) { + public boolean addToWatchlist(final Geocache cache) { final boolean added = OkapiClient.setWatchState(cache, true, this); if (added) { @@ -109,7 +110,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean removeFromWatchlist(Geocache cache) { + public boolean removeFromWatchlist(final Geocache cache) { final boolean removed = OkapiClient.setWatchState(cache, false, this); if (removed) { @@ -130,7 +131,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean canLog(Geocache cache) { + public boolean canLog(final Geocache cache) { return true; } @@ -139,7 +140,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean login(Handler handler, Context fromActivity) { + public boolean login(final Handler handler, final Context fromActivity) { if (supportsPersonalization()) { userInfo = OkapiClient.getUserInfo(this); } else { @@ -149,6 +150,11 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override + public boolean isOwner(final ICache cache) { + return StringUtils.isNotEmpty(getUserName()) && StringUtils.equals(cache.getOwnerDisplayName(), getUserName()); + } + + @Override public String getUserName() { return userInfo.getName(); } @@ -175,7 +181,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean isSearchForMyCaches(String username) { + public boolean isSearchForMyCaches(final String username) { return StringUtils.equalsIgnoreCase(username, getUserName()); } diff --git a/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java b/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java index 131ddad..a1030f0 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java +++ b/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java @@ -31,14 +31,18 @@ public class OCAuthParams extends OAuthParameters { R.string.oc_ro_okapi_consumer_key, R.string.oc_ro_okapi_consumer_secret, "callback://www.cgeo.org/opencaching.ro/", R.string.auth_ocro, R.string.pref_ocro_tokenpublic, R.string.pref_ocro_tokensecret, R.string.pref_temp_ocro_token_public, R.string.pref_temp_ocro_token_secret); + public static final OCAuthParams OC_UK_AUTH_PARAMS = new OCAuthParams("www.opencaching.org.uk", false, + R.string.oc_uk_okapi_consumer_key, R.string.oc_uk_okapi_consumer_secret, "callback://www.cgeo.org/opencaching.org.uk/", + R.string.auth_ocuk, R.string.pref_ocuk_tokenpublic, R.string.pref_ocuk_tokensecret, R.string.pref_temp_ocuk_token_public, R.string.pref_temp_ocuk_token_secret); + public final int authTitleResId; public final int tokenPublicPrefKey; public final int tokenSecretPrefKey; public final int tempTokenPublicPrefKey; public final int tempTokenSecretPrefKey; - public OCAuthParams(@NonNull String host, boolean https, int consumerKeyResId, int consumerSecretResId, @NonNull String callback, - int authTitleResId, int tokenPublicPrefKey, int tokenSecretPrefKey, int tempTokePublicPrefKey, int tempTokenSecretPrefKey) { + public OCAuthParams(@NonNull final String host, final boolean https, final int consumerKeyResId, final int consumerSecretResId, @NonNull final String callback, + final int authTitleResId, final int tokenPublicPrefKey, final int tokenSecretPrefKey, final int tempTokePublicPrefKey, final int tempTokenSecretPrefKey) { super(host, "/okapi/services/oauth/request_token", "/okapi/services/oauth/authorize", "/okapi/services/oauth/access_token", @@ -54,7 +58,7 @@ public class OCAuthParams extends OAuthParameters { } @Override - public void setOAuthExtras(Intent intent) { + public void setOAuthExtras(final Intent intent) { super.setOAuthExtras(intent); if (intent != null) { diff --git a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java index 3c93488..910654d 100644 --- a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java +++ b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java @@ -6,6 +6,7 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.Image; import cgeo.geocaching.LogEntry; import cgeo.geocaching.R; +import cgeo.geocaching.Trackable; import cgeo.geocaching.Waypoint; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; @@ -79,6 +80,7 @@ final class OkapiClient { private static final String CACHE_VOTES = "rating_votes"; private static final String CACHE_NOTFOUNDS = "notfounds"; private static final String CACHE_FOUNDS = "founds"; + private static final String CACHE_WILLATTENDS = "willattends"; private static final String CACHE_HIDDEN = "date_hidden"; private static final String CACHE_LATEST_LOGS = "latest_logs"; private static final String CACHE_IMAGE_URL = "url"; @@ -98,6 +100,11 @@ final class OkapiClient { private static final String CACHE_CODE = "code"; private static final String CACHE_REQ_PASSWORD = "req_passwd"; private static final String CACHE_MY_NOTES = "my_notes"; + private static final String CACHE_TRACKABLES_COUNT = "trackables_count"; + private static final String CACHE_TRACKABLES = "trackables"; + + private static final String TRK_GEOCODE = "code"; + private static final String TRK_NAME = "name"; private static final String LOG_TYPE = "type"; private static final String LOG_COMMENT = "comment"; @@ -112,11 +119,12 @@ final class OkapiClient { // the several realms of possible fields for cache retrieval: // Core: for livemap requests (L3 - only with level 3 auth) // Additional: additional fields for full cache (L3 - only for level 3 auth, current - only for connectors with current api) - private static final String SERVICE_CACHE_CORE_FIELDS = "code|name|location|type|status|difficulty|terrain|size|size2|date_hidden"; + private static final String SERVICE_CACHE_CORE_FIELDS = "code|name|location|type|status|difficulty|terrain|size|size2|date_hidden|trackables_count"; private static final String SERVICE_CACHE_CORE_L3_FIELDS = "is_found"; - private static final String SERVICE_CACHE_ADDITIONAL_FIELDS = "owner|founds|notfounds|rating|rating_votes|recommendations|description|hint|images|latest_logs|alt_wpts|attrnames|req_passwd"; - private static final String SERVICE_CACHE_ADDITIONAL_CURRENT_FIELDS = "gc_code|attribution_note|attr_acodes"; - private static final String SERVICE_CACHE_ADDITIONAL_L3_FIELDS = "is_watched|my_notes"; + private static final String SERVICE_CACHE_ADDITIONAL_FIELDS = "owner|founds|notfounds|rating|rating_votes|recommendations|description|hint|images|latest_logs|alt_wpts|attrnames|req_passwd|trackables"; + private static final String SERVICE_CACHE_ADDITIONAL_CURRENT_FIELDS = "gc_code|attribution_note|attr_acodes|willattends"; + private static final String SERVICE_CACHE_ADDITIONAL_L3_FIELDS = "my_notes"; + private static final String SERVICE_CACHE_ADDITIONAL_CURRENT_L3_FIELDS = "is_watched"; private static final String METHOD_SEARCH_ALL = "services/caches/search/all"; private static final String METHOD_SEARCH_BBOX = "services/caches/search/bbox"; @@ -290,13 +298,14 @@ final class OkapiClient { final JSONObject cachesResponse = response.getJSONObject("results"); if (cachesResponse != null) { final List<Geocache> caches = new ArrayList<Geocache>(cachesResponse.length()); - @SuppressWarnings("unchecked") - final - Iterator<String> keys = cachesResponse.keys(); + final Iterator<?> keys = cachesResponse.keys(); while (keys.hasNext()) { - final String key = keys.next(); - final Geocache cache = parseSmallCache(cachesResponse.getJSONObject(key)); - caches.add(cache); + final Object next = keys.next(); + if (next instanceof String) { + final String key = (String) next; + final Geocache cache = parseSmallCache(cachesResponse.getJSONObject(key)); + caches.add(cache); + } } return caches; } @@ -313,7 +322,7 @@ final class OkapiClient { parseCoreCache(response, cache); - DataStore.saveCache(cache, EnumSet.of(SaveFlag.SAVE_CACHE)); + DataStore.saveCache(cache, EnumSet.of(SaveFlag.CACHE)); } catch (final JSONException e) { Log.e("OkapiClient.parseSmallCache", e); } @@ -328,11 +337,16 @@ final class OkapiClient { parseCoreCache(response, cache); // not used: url - final JSONObject owner = response.getJSONObject(CACHE_OWNER); - cache.setOwnerDisplayName(parseUser(owner)); + final JSONObject ownerObject = response.getJSONObject(CACHE_OWNER); + final String owner = parseUser(ownerObject); + cache.setOwnerDisplayName(owner); + // OpenCaching has no distinction between user id and user display name. Set the ID anyway to simplify c:geo workflows. + cache.setOwnerUserId(owner); cache.getLogCounts().put(LogType.FOUND_IT, response.getInt(CACHE_FOUNDS)); cache.getLogCounts().put(LogType.DIDNT_FIND_IT, response.getInt(CACHE_NOTFOUNDS)); + // only current Api + cache.getLogCounts().put(LogType.WILL_ATTEND, response.optInt(CACHE_WILLATTENDS)); if (!response.isNull(CACHE_RATING)) { cache.setRating((float) response.getDouble(CACHE_RATING)); @@ -375,6 +389,9 @@ final class OkapiClient { //TODO: Store license per cache //cache.setLicense(response.getString("attribution_note")); cache.setWaypoints(parseWaypoints(response.getJSONArray(CACHE_WPTS)), false); + + cache.setInventory(parseTrackables(response.getJSONArray(CACHE_TRACKABLES))); + if (!response.isNull(CACHE_IS_WATCHED)) { cache.setOnWatchlist(response.getBoolean(CACHE_IS_WATCHED)); } @@ -386,7 +403,7 @@ final class OkapiClient { cache.setDetailedUpdatedNow(); // save full detailed caches - DataStore.saveCache(cache, EnumSet.of(SaveFlag.SAVE_DB)); + DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); DataStore.saveLogsWithoutTransaction(cache.getGeocode(), parseLogs(response.getJSONArray(CACHE_LATEST_LOGS))); } catch (final JSONException e) { Log.e("OkapiClient.parseCache", e); @@ -409,6 +426,8 @@ final class OkapiClient { cache.setDifficulty((float) response.getDouble(CACHE_DIFFICULTY)); cache.setTerrain((float) response.getDouble(CACHE_TERRAIN)); + cache.setInventoryItems(response.getInt(CACHE_TRACKABLES_COUNT)); + if (!response.isNull(CACHE_IS_FOUND)) { cache.setFound(response.getBoolean(CACHE_IS_FOUND)); } @@ -478,6 +497,27 @@ final class OkapiClient { return result; } + private static List<Trackable> parseTrackables(final JSONArray trackablesJson) { + if (trackablesJson.length() == 0) { + return Collections.emptyList(); + } + final List<Trackable> result = new ArrayList<Trackable>(); + for (int i = 0; i < trackablesJson.length(); i++) { + try { + final JSONObject trackableResponse = trackablesJson.getJSONObject(i); + final Trackable trk = new Trackable(); + trk.setGeocode(trackableResponse.getString(TRK_GEOCODE)); + trk.setName(trackableResponse.getString(TRK_NAME)); + result.add(trk); + } catch (final JSONException e) { + Log.e("OkapiClient.parseWaypoints", e); + // Don't overwrite internal state with possibly partial result + return null; + } + } + return result; + } + private static LogType parseLogType(final String logType) { if ("Found it".equalsIgnoreCase(logType)) { return LogType.FOUND_IT; @@ -593,7 +633,7 @@ final class OkapiClient { try { final String size = response.getString(CACHE_SIZE2); return CacheSize.getById(size); - } catch (JSONException e) { + } catch (final JSONException e) { Log.e("OkapiClient.getCacheSize", e); return getCacheSizeDeprecated(response); } @@ -683,6 +723,9 @@ final class OkapiClient { } if (connector.getApiSupport() == ApiSupport.current) { res.append(SEPARATOR).append(SERVICE_CACHE_ADDITIONAL_CURRENT_FIELDS); + if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { + res.append(SEPARATOR).append(SERVICE_CACHE_ADDITIONAL_CURRENT_L3_FIELDS); + } } return res.toString(); @@ -702,7 +745,7 @@ final class OkapiClient { params.add("langpref", getPreferredLanguage()); if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { - ImmutablePair<String, String> tokens = Settings.getTokenPair(connector.getTokenPublicPrefKeyId(), connector.getTokenSecretPrefKeyId()); + final ImmutablePair<String, String> tokens = Settings.getTokenPair(connector.getTokenPublicPrefKeyId(), connector.getTokenSecretPrefKeyId()); OAuth.signOAuth(host, service.methodName, "GET", false, params, tokens.left, tokens.right, connector.getCK(), connector.getCS()); } else { connector.addAuthentication(params); @@ -769,7 +812,7 @@ final class OkapiClient { return null; } - JSONObject data = result.data; + final JSONObject data = result.data; if (!data.isNull(USER_UUID)) { try { return data.getString(USER_UUID); @@ -792,7 +835,7 @@ final class OkapiClient { return new UserInfo(StringUtils.EMPTY, 0, UserInfoStatus.getFromOkapiError(error.getResult())); } - JSONObject data = result.data; + final JSONObject data = result.data; String name = StringUtils.EMPTY; boolean successUserName = false; @@ -828,7 +871,7 @@ final class OkapiClient { * response containing an error object * @return OkapiError object with detailed information */ - public static OkapiError decodeErrorResponse(HttpResponse response) { + public static OkapiError decodeErrorResponse(final HttpResponse response) { final JSONResult result = new JSONResult(response); if (!result.isSuccess) { return new OkapiError(result.data); @@ -846,7 +889,7 @@ final class OkapiClient { public final JSONObject data; public JSONResult(final @Nullable HttpResponse response) { - boolean isSuccess = Network.isSuccess(response); + final boolean isSuccess = Network.isSuccess(response); final String responseData = Network.getResponseDataAlways(response); JSONObject data = null; if (responseData != null) { |
