package cgeo.geocaching.connector.gc;
import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.DataStore;
import cgeo.geocaching.Geocache;
import cgeo.geocaching.ICache;
import cgeo.geocaching.LogCacheActivity;
import cgeo.geocaching.R;
import cgeo.geocaching.SearchResult;
import cgeo.geocaching.connector.AbstractConnector;
import cgeo.geocaching.connector.ILoggingManager;
import cgeo.geocaching.connector.UserAction;
import cgeo.geocaching.connector.capability.FieldNotesCapability;
import cgeo.geocaching.connector.capability.ICredentials;
import cgeo.geocaching.connector.capability.ILogin;
import cgeo.geocaching.connector.capability.ISearchByCenter;
import cgeo.geocaching.connector.capability.ISearchByFinder;
import cgeo.geocaching.connector.capability.ISearchByGeocode;
import cgeo.geocaching.connector.capability.ISearchByKeyword;
import cgeo.geocaching.connector.capability.ISearchByOwner;
import cgeo.geocaching.connector.capability.ISearchByViewPort;
import cgeo.geocaching.enumerations.StatusCode;
import cgeo.geocaching.geopoint.Geopoint;
import cgeo.geocaching.geopoint.Viewport;
import cgeo.geocaching.loaders.RecaptchaReceiver;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.network.Parameters;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.settings.SettingsActivity;
import cgeo.geocaching.utils.CancellableHandler;
import cgeo.geocaching.utils.Log;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import rx.functions.Action1;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import java.io.File;
import java.util.List;
import java.util.regex.Pattern;
public class GCConnector extends AbstractConnector implements ISearchByGeocode, ISearchByCenter, ISearchByViewPort, ISearchByKeyword, ILogin, ICredentials, ISearchByOwner, ISearchByFinder, FieldNotesCapability {
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=";
/**
* Pocket queries downloaded from the website use a numeric prefix. The pocket query creator Android app adds a
* verbatim "pocketquery" prefix.
*/
private static final Pattern GPX_ZIP_FILE_PATTERN = Pattern.compile("((\\d{7,})|(pocketquery))" + "(_.+)?" + "\\.zip", Pattern.CASE_INSENSITIVE);
/**
* Pattern for GC codes
*/
private final static Pattern PATTERN_GC_CODE = Pattern.compile("GC[0-9A-Z]+", Pattern.CASE_INSENSITIVE);
private GCConnector() {
// singleton
}
/**
* initialization on demand holder pattern
*/
private static class Holder {
private static final @NonNull GCConnector INSTANCE = new GCConnector();
}
public static @NonNull
GCConnector getInstance() {
return Holder.INSTANCE;
}
@Override
public boolean canHandle(@NonNull String geocode) {
return GCConnector.PATTERN_GC_CODE.matcher(geocode).matches();
}
@Override
public String getLongCacheUrl(@NonNull Geocache cache) {
return CACHE_URL_LONG + cache.getGeocode();
}
@Override
public String getCacheUrl(@NonNull Geocache cache) {
return CACHE_URL_SHORT + cache.getGeocode();
}
@Override
public boolean supportsPersonalNote() {
return Settings.isGCPremiumMember();
}
@Override
public boolean supportsOwnCoordinates() {
return true;
}
@Override
public boolean supportsWatchList() {
return true;
}
@Override
public boolean supportsLogging() {
return true;
}
@Override
public boolean supportsLogImages() {
return true;
}
@Override
public ILoggingManager getLoggingManager(final LogCacheActivity activity, final Geocache cache) {
return new GCLoggingManager(activity, cache);
}
@Override
public boolean canLog(Geocache cache) {
return StringUtils.isNotBlank(cache.getCacheId());
}
@Override
public String getName() {
return "geocaching.com";
}
@Override
public String getHost() {
return "www.geocaching.com";
}
@Override
public SearchResult searchByGeocode(final @Nullable String geocode, final @Nullable String guid, final CancellableHandler handler) {
CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_loadpage);
final String page = GCParser.requestHtmlPage(geocode, guid, "y", String.valueOf(GCConstants.NUMBER_OF_LOGS));
if (StringUtils.isEmpty(page)) {
final SearchResult search = new SearchResult();
if (DataStore.isThere(geocode, guid, true, false)) {
if (StringUtils.isBlank(geocode) && StringUtils.isNotBlank(guid)) {
Log.i("Loading old cache from cache.");
search.addGeocode(DataStore.getGeocodeForGuid(guid));
} else {
search.addGeocode(geocode);
}
search.setError(StatusCode.NO_ERROR);
return search;
}
Log.e("GCConnector.searchByGeocode: No data from server");
search.setError(StatusCode.COMMUNICATION_ERROR);
return search;
}
assert page != null;
final SearchResult searchResult = GCParser.parseCache(page, handler);
if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) {
Log.w("GCConnector.searchByGeocode: No cache parsed");
return searchResult;
}
// do not filter when searching for one specific cache
return searchResult;
}
@Override
public SearchResult searchByViewport(@NonNull Viewport viewport, final MapTokens tokens) {
return GCMap.searchByViewport(viewport, tokens);
}
@Override
public boolean isZippedGPXFile(final String fileName) {
return GPX_ZIP_FILE_PATTERN.matcher(fileName).matches();
}
@Override
public boolean isReliableLatLon(boolean cacheHasReliableLatLon) {
return cacheHasReliableLatLon;
}
@Override
public boolean isOwner(final ICache cache) {
final String user = Settings.getUsername();
return StringUtils.isNotEmpty(user) && StringUtils.equalsIgnoreCase(cache.getOwnerUserId(), user);
}
@Override
public boolean addToWatchlist(Geocache cache) {
final boolean added = GCParser.addToWatchlist(cache);
if (added) {
DataStore.saveChangedCache(cache);
}
return added;
}
@Override
public boolean removeFromWatchlist(Geocache cache) {
final boolean removed = GCParser.removeFromWatchlist(cache);
if (removed) {
DataStore.saveChangedCache(cache);
}
return removed;
}
/**
* Add a cache to the favorites list.
*
* This must not be called from the UI thread.
*
* @param cache
* the cache to add
* @return true if the cache was successfully added, false otherwise
*/
public static boolean addToFavorites(Geocache cache) {
final boolean added = GCParser.addToFavorites(cache);
if (added) {
DataStore.saveChangedCache(cache);
}
return added;
}
/**
* Remove a cache from the favorites list.
*
* This must not be called from the UI thread.
*
* @param cache
* the cache to add
* @return true if the cache was successfully added, false otherwise
*/
public static boolean removeFromFavorites(Geocache cache) {
final boolean removed = GCParser.removeFromFavorites(cache);
if (removed) {
DataStore.saveChangedCache(cache);
}
return removed;
}
@Override
public boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt) {
final boolean uploaded = GCParser.uploadModifiedCoordinates(cache, wpt);
if (uploaded) {
DataStore.saveChangedCache(cache);
}
return uploaded;
}
@Override
public boolean deleteModifiedCoordinates(Geocache cache) {
final boolean deleted = GCParser.deleteModifiedCoordinates(cache);
if (deleted) {
DataStore.saveChangedCache(cache);
}
return deleted;
}
@Override
public boolean uploadPersonalNote(Geocache cache) {
final boolean uploaded = GCParser.uploadPersonalNote(cache);
if (uploaded) {
DataStore.saveChangedCache(cache);
}
return uploaded;
}
@Override
public SearchResult searchByCenter(@NonNull Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) {
return GCParser.searchByCoords(center, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver);
}
@Override
public boolean supportsFavoritePoints() {
return true;
}
@Override
protected String getCacheUrlPrefix() {
return null; // UNUSED
}
@Override
public String getGeocodeFromUrl(String url) {
// coord.info URLs
String code = StringUtils.substringAfterLast(url, "coord.info/");
if (code != null && canHandle(code)) {
return code;
}
// expanded geocaching.com URLs
code = StringUtils.substringBetween(url, "/geocache/", "_");
if (code != null && canHandle(code)) {
return code;
}
return null;
}
@Override
public boolean isActive() {
return Settings.isGCConnectorActive();
}
@Override
public int getCacheMapMarkerId(boolean disabled) {
if (disabled) {
return R.drawable.marker_disabled;
}
return R.drawable.marker;
}
@Override
public boolean login(Handler handler, Context fromActivity) {
// login
final StatusCode status = GCLogin.getInstance().login();
if (status == StatusCode.NO_ERROR) {
GCLogin.detectGcCustomDate();
}
if (CgeoApplication.getInstance().showLoginToast && handler != null) {
handler.sendMessage(handler.obtainMessage(0, status));
CgeoApplication.getInstance().showLoginToast = false;
// invoke settings activity to insert login details
if (status == StatusCode.NO_LOGIN_INFO_STORED && fromActivity != null) {
SettingsActivity.openForScreen(R.string.preference_screen_gc, fromActivity);
}
}
return status == StatusCode.NO_ERROR;
}
@Override
public String getUserName() {
return GCLogin.getInstance().getActualUserName();
}
@Override
public int getCachesFound() {
return GCLogin.getInstance().getActualCachesFound();
}
@Override
public String getLoginStatusString() {
return GCLogin.getInstance().getActualStatus();
}
@Override
public boolean isLoggedIn() {
return GCLogin.getInstance().isActualLoginStatus();
}
@Override
public String getWaypointGpxId(String prefix, String geocode) {
String gpxId = prefix;
if (StringUtils.isNotBlank(geocode) && geocode.length() > 2) {
gpxId += geocode.substring(2);
}
return gpxId;
}
@Override
public String getWaypointPrefix(String name) {
String prefix = name;
if (StringUtils.isNotBlank(prefix) && prefix.length() >= 2) {
prefix = name.substring(0, 2);
}
return prefix;
}
@Override
public SearchResult searchByKeyword(@NonNull String keyword, final @NonNull RecaptchaReceiver recaptchaReceiver) {
return GCParser.searchByKeyword(keyword, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver);
}
@Override
public int getUsernamePreferenceKey() {
return R.string.pref_username;
}
@Override
public int getPasswordPreferenceKey() {
return R.string.pref_password;
}
@Override
public @NonNull
List getUserActions() {
List actions = super.getUserActions();
actions.add(new UserAction(R.string.user_menu_open_browser, new Action1() {
@Override
public void call(cgeo.geocaching.connector.UserAction.Context context) {
context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/profile/?u=" + Network.encode(context.userName))));
}
}));
actions.add(new UserAction(R.string.user_menu_send_message, new Action1() {
@Override
public void call(cgeo.geocaching.connector.UserAction.Context context) {
context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/email/?u=" + Network.encode(context.userName))));
}
}));
return actions;
}
@Override
public SearchResult searchByOwner(final @NonNull String username, final @NonNull RecaptchaReceiver recaptchaReceiver) {
return GCParser.searchByOwner(username, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver);
}
@Override
public SearchResult searchByFinder(final @NonNull String username, final @NonNull RecaptchaReceiver recaptchaReceiver) {
return GCParser.searchByUsername(username, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver);
}
@Override
public boolean uploadFieldNotes(final File exportFile) {
if (!GCLogin.getInstance().isActualLoginStatus()) {
// no need to upload (possibly large file) if we're not logged in
final StatusCode loginState = GCLogin.getInstance().login();
if (loginState != StatusCode.NO_ERROR) {
Log.e("FieldnoteExport.ExportTask upload: Login failed");
}
}
final String uri = "http://www.geocaching.com/my/uploadfieldnotes.aspx";
final String page = GCLogin.getInstance().getRequestLogged(uri, null);
if (StringUtils.isBlank(page)) {
Log.e("FieldnoteExport.ExportTask get page: No data from server");
return false;
}
final String[] viewstates = GCLogin.getViewstates(page);
final Parameters uploadParams = new Parameters(
"__EVENTTARGET", "",
"__EVENTARGUMENT", "",
"ctl00$ContentBody$btnUpload", "Upload Field Note");
GCLogin.putViewstates(uploadParams, viewstates);
Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$FieldNoteLoader", "text/plain", exportFile));
if (StringUtils.isBlank(page)) {
Log.e("FieldnoteExport.ExportTask upload: No data from server");
return false;
}
return true;
}
}