diff options
Diffstat (limited to 'main/src/cgeo/geocaching/connector')
8 files changed, 279 insertions, 190 deletions
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 5422c11..6887654 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCParser.java +++ b/main/src/cgeo/geocaching/connector/gc/GCParser.java @@ -47,6 +47,14 @@ 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 rx.schedulers.Schedulers; + 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.SAVE_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) { @@ -1622,116 +1639,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(Schedulers.io()); + } - 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 @@ -1823,32 +1866,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(Schedulers.computation()); + 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) { @@ -1857,6 +1907,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..5327bea 100644 --- a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java +++ b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java @@ -57,7 +57,7 @@ public class RecaptchaHandler extends Handler { return Observable.empty(); } }); - AndroidObservable.bindActivity(activity, captcha).subscribe(new Action1<Bitmap>() { + AndroidObservable.bindActivity(activity, captcha).subscribeOn(Schedulers.io()).subscribe(new Action1<Bitmap>() { @Override public void call(final Bitmap bitmap) { imageView.setImageBitmap(bitmap); @@ -67,7 +67,7 @@ public class RecaptchaHandler extends Handler { public void call(final Throwable throwable) { // Do nothing } - }, Schedulers.io()); + }); reloadButton.setEnabled(true); } 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..3771443 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; @@ -149,6 +150,11 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override + public boolean isOwner(ICache cache) { + return StringUtils.isNotEmpty(getUserName()) && StringUtils.equals(cache.getOwnerDisplayName(), getUserName()); + } + + @Override public String getUserName() { return userInfo.getName(); } diff --git a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java index 3c93488..477830c 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,10 +119,10 @@ 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_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 = "is_watched|my_notes"; private static final String METHOD_SEARCH_ALL = "services/caches/search/all"; @@ -328,11 +335,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 +387,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)); } @@ -409,6 +424,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 +495,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 +631,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); } @@ -702,7 +740,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 +807,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 +830,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 +866,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 +884,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) { |
