aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBananeweizen <bananeweizen@gmx.de>2015-08-29 11:43:38 +0200
committerBananeweizen <bananeweizen@gmx.de>2015-08-29 11:43:38 +0200
commit45e9235419f5a3d1232e4c557f6a85534d822d3f (patch)
tree8b0399e25c669b3c7c65eec96f1a3c82a6214c04
parent8ad70af3f005579769b8198741d589f0a1c3230f (diff)
downloadcgeo-45e9235419f5a3d1232e4c557f6a85534d822d3f.zip
cgeo-45e9235419f5a3d1232e4c557f6a85534d822d3f.tar.gz
cgeo-45e9235419f5a3d1232e4c557f6a85534d822d3f.tar.bz2
refactoring: more null annotations
-rw-r--r--main/src/cgeo/geocaching/location/DistanceParser.java102
-rw-r--r--main/src/cgeo/geocaching/settings/Settings.java2413
-rw-r--r--main/src/cgeo/geocaching/utils/CryptUtils.java309
-rw-r--r--main/src/cgeo/geocaching/utils/EnvironmentUtils.java58
-rw-r--r--main/src/cgeo/geocaching/utils/FileUtils.java427
-rw-r--r--main/src/cgeo/geocaching/utils/Formatter.java492
-rw-r--r--main/src/cgeo/geocaching/utils/HtmlUtils.java149
-rw-r--r--main/src/cgeo/geocaching/utils/ImageUtils.java976
-rw-r--r--main/src/cgeo/geocaching/utils/LogTemplateProvider.java551
-rw-r--r--main/src/cgeo/geocaching/utils/MapUtils.java580
-rw-r--r--main/src/cgeo/geocaching/utils/MatcherWrapper.java6
11 files changed, 3054 insertions, 3009 deletions
diff --git a/main/src/cgeo/geocaching/location/DistanceParser.java b/main/src/cgeo/geocaching/location/DistanceParser.java
index 7cbc19f..80e82f2 100644
--- a/main/src/cgeo/geocaching/location/DistanceParser.java
+++ b/main/src/cgeo/geocaching/location/DistanceParser.java
@@ -1,51 +1,51 @@
-package cgeo.geocaching.location;
-
-import cgeo.geocaching.utils.MatcherWrapper;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-public final class DistanceParser {
-
- private static final Pattern pattern = Pattern.compile("^([0-9.,]+)[ ]*(m|km|ft|yd|mi|)?$", Pattern.CASE_INSENSITIVE);
-
- /**
- * Parse a distance string composed by a number and an optional suffix
- * (such as "1.2km").
- *
- * @param distanceText
- * the string to analyze
- * @return the distance in kilometers
- *
- * @throws NumberFormatException
- * if the given number is invalid
- */
- public static float parseDistance(final String distanceText, final boolean metricUnit)
- throws NumberFormatException {
- final MatcherWrapper matcher = new MatcherWrapper(pattern, distanceText);
-
- if (!matcher.find()) {
- throw new NumberFormatException(distanceText);
- }
-
- final float value = Float.parseFloat(matcher.group(1).replace(',', '.'));
- final String unit = matcher.group(2).toLowerCase(Locale.US);
-
- if (unit.equals("m") || (StringUtils.isEmpty(unit) && metricUnit)) {
- return value / 1000;
- }
- if (unit.equals("km")) {
- return value;
- }
- if (unit.equals("yd")) {
- return value * IConversion.YARDS_TO_KILOMETER;
- }
- if (unit.equals("mi")) {
- return value * IConversion.MILES_TO_KILOMETER;
- }
- return value * IConversion.FEET_TO_KILOMETER;
- }
-
-}
+package cgeo.geocaching.location;
+
+import cgeo.geocaching.utils.MatcherWrapper;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+public final class DistanceParser {
+
+ private static final Pattern pattern = Pattern.compile("^([0-9.,]+)[ ]*(m|km|ft|yd|mi|)?$", Pattern.CASE_INSENSITIVE);
+
+ /**
+ * Parse a distance string composed by a number and an optional suffix
+ * (such as "1.2km").
+ *
+ * @param distanceText
+ * the string to analyze
+ * @return the distance in kilometers
+ *
+ * @throws NumberFormatException
+ * if the given number is invalid
+ */
+ public static float parseDistance(final String distanceText, final boolean metricUnit)
+ throws NumberFormatException {
+ final MatcherWrapper matcher = new MatcherWrapper(pattern, distanceText);
+
+ if (!matcher.find()) {
+ throw new NumberFormatException(distanceText);
+ }
+
+ final float value = Float.parseFloat(matcher.group(1).replace(',', '.'));
+ final String unit = StringUtils.lowerCase(matcher.group(2), Locale.US);
+
+ if ("m".equals(unit) || (StringUtils.isEmpty(unit) && metricUnit)) {
+ return value / 1000;
+ }
+ if ("km".equals(unit)) {
+ return value;
+ }
+ if ("yd".equals(unit)) {
+ return value * IConversion.YARDS_TO_KILOMETER;
+ }
+ if ("mi".equals(unit)) {
+ return value * IConversion.MILES_TO_KILOMETER;
+ }
+ return value * IConversion.FEET_TO_KILOMETER;
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/settings/Settings.java b/main/src/cgeo/geocaching/settings/Settings.java
index 6fad160..71f4ebb 100644
--- a/main/src/cgeo/geocaching/settings/Settings.java
+++ b/main/src/cgeo/geocaching/settings/Settings.java
@@ -1,1205 +1,1208 @@
-package cgeo.geocaching.settings;
-
-import cgeo.geocaching.CgeoApplication;
-import cgeo.geocaching.R;
-import cgeo.geocaching.apps.navi.NavigationAppFactory.NavigationAppsEnum;
-import cgeo.geocaching.connector.capability.ICredentials;
-import cgeo.geocaching.connector.gc.GCConnector;
-import cgeo.geocaching.connector.gc.GCConstants;
-import cgeo.geocaching.enumerations.CacheType;
-import cgeo.geocaching.enumerations.LogTypeTrackable;
-import cgeo.geocaching.list.StoredList;
-import cgeo.geocaching.location.Geopoint;
-import cgeo.geocaching.maps.CGeoMap.MapMode;
-import cgeo.geocaching.maps.LivemapStrategy;
-import cgeo.geocaching.maps.MapProviderFactory;
-import cgeo.geocaching.maps.google.v1.GoogleMapProvider;
-import cgeo.geocaching.maps.interfaces.GeoPointImpl;
-import cgeo.geocaching.maps.interfaces.MapProvider;
-import cgeo.geocaching.maps.interfaces.MapSource;
-import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider;
-import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider.OfflineMapSource;
-import cgeo.geocaching.sensors.OrientationProvider;
-import cgeo.geocaching.sensors.RotationProvider;
-import cgeo.geocaching.utils.CryptUtils;
-import cgeo.geocaching.utils.FileUtils;
-import cgeo.geocaching.utils.FileUtils.FileSelector;
-import cgeo.geocaching.utils.Log;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.ImmutablePair;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.os.Build;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Environment;
-import android.preference.PreferenceManager;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * General c:geo preferences/settings set by the user
- */
-public class Settings {
-
- /**
- * On opening a map, we limit the _initial_ zoom. The user can still zoom out afterwards.
- */
- private static final int INITIAL_MAP_ZOOM_LIMIT = 16;
- private static final char HISTORY_SEPARATOR = ',';
- private static final int SHOW_WP_THRESHOLD_DEFAULT = 10;
- public static final int SHOW_WP_THRESHOLD_MAX = 50;
- private static final int MAP_SOURCE_DEFAULT = GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
-
- public static final boolean HW_ACCEL_DISABLED_BY_DEFAULT =
- Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
- StringUtils.equals(Build.MODEL, "HTC One X") || // HTC One X
- StringUtils.equals(Build.MODEL, "HTC One S") || // HTC One S
- StringUtils.equals(Build.MODEL, "GT-I8190") || // Samsung S3 mini
- StringUtils.equals(Build.MODEL, "GT-S6310L") || // Samsung Galaxy Young
- StringUtils.equals(Build.MODEL, "GT-P5210") || // Samsung Galaxy Tab 3
- StringUtils.equals(Build.MODEL, "GT-S7580") || // Samsung Galaxy Trend Plus
- StringUtils.equals(Build.MODEL, "GT-I9105P") || // Samsung Galaxy SII Plus
- StringUtils.equals(Build.MODEL, "ST25i") || // Sony Xperia U
- StringUtils.equals(Build.MODEL, "bq Aquaris 5") || // bq Aquaris 5
- StringUtils.equals(Build.MODEL, "A1-810") || // Unknown A1-810
- StringUtils.equals(Build.MODEL, "GT-I9195") || // Samsung S4 mini
- StringUtils.equals(Build.MODEL, "GT-I8200N"); // Samsung S3 mini
-
- // twitter api keys
- private final static @NonNull String TWITTER_KEY_CONSUMER_PUBLIC = CryptUtils.rot13("ESnsCvAv3kEupF1GCR3jGj");
- private final static @NonNull String TWITTER_KEY_CONSUMER_SECRET = CryptUtils.rot13("7vQWceACV9umEjJucmlpFe9FCMZSeqIqfkQ2BnhV9x");
-
- private static boolean useCompass = true;
-
- public enum CoordInputFormatEnum {
- Plain,
- Deg,
- Min,
- Sec;
-
- static final int DEFAULT_INT_VALUE = Min.ordinal();
-
- public static CoordInputFormatEnum fromInt(final int id) {
- final CoordInputFormatEnum[] values = CoordInputFormatEnum.values();
- if (id < 0 || id >= values.length) {
- return Min;
- }
- return values[id];
- }
- }
-
- private static final SharedPreferences sharedPrefs = PreferenceManager
- .getDefaultSharedPreferences(CgeoApplication.getInstance().getBaseContext());
- static {
- migrateSettings();
- final boolean isDebug = sharedPrefs.getBoolean(getKey(R.string.pref_debug), false);
- Log.setDebug(isDebug);
- CgeoApplication.dumpOnOutOfMemory(isDebug);
- }
-
- /**
- * Cache the mapsource locally. If that is an offline map source, each request would potentially access the
- * underlying map file, leading to delays.
- */
- private static MapSource mapSource;
-
- protected Settings() {
- throw new InstantiationError();
- }
-
- private static void migrateSettings() {
- final int LATEST_PREFERENCES_VERSION = 2;
- final int currentVersion = getInt(R.string.pref_settingsversion, 0);
-
- // No need to migrate if we are up to date.
- if (currentVersion == LATEST_PREFERENCES_VERSION) {
- return;
- }
-
- // No need to migrate if we don't have older settings, defaults will be used instead.
- final String preferencesNameV0 = "cgeo.pref";
- final SharedPreferences prefsV0 = CgeoApplication.getInstance().getSharedPreferences(preferencesNameV0, Context.MODE_PRIVATE);
- if (currentVersion == 0 && prefsV0.getAll().isEmpty()) {
- final Editor e = sharedPrefs.edit();
- e.putInt(getKey(R.string.pref_settingsversion), LATEST_PREFERENCES_VERSION);
- e.apply();
- return;
- }
-
- if (currentVersion < 1) {
- // migrate from non standard file location and integer based boolean types
- final Editor e = sharedPrefs.edit();
-
- e.putString(getKey(R.string.pref_temp_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_temp_twitter_token_secret), null));
- e.putString(getKey(R.string.pref_temp_twitter_token_public), prefsV0.getString(getKey(R.string.pref_temp_twitter_token_public), null));
- e.putBoolean(getKey(R.string.pref_help_shown), prefsV0.getInt(getKey(R.string.pref_help_shown), 0) != 0);
- e.putFloat(getKey(R.string.pref_anylongitude), prefsV0.getFloat(getKey(R.string.pref_anylongitude), 0));
- e.putFloat(getKey(R.string.pref_anylatitude), prefsV0.getFloat(getKey(R.string.pref_anylatitude), 0));
- e.putBoolean(getKey(R.string.pref_offlinemaps), 0 != prefsV0.getInt(getKey(R.string.pref_offlinemaps), 1));
- e.putBoolean(getKey(R.string.pref_offlinewpmaps), 0 != prefsV0.getInt(getKey(R.string.pref_offlinewpmaps), 0));
- e.putString(getKey(R.string.pref_webDeviceCode), prefsV0.getString(getKey(R.string.pref_webDeviceCode), null));
- e.putString(getKey(R.string.pref_webDeviceName), prefsV0.getString(getKey(R.string.pref_webDeviceName), null));
- e.putBoolean(getKey(R.string.pref_maplive), prefsV0.getInt(getKey(R.string.pref_maplive), 1) != 0);
- e.putInt(getKey(R.string.pref_mapsource), prefsV0.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT));
- e.putBoolean(getKey(R.string.pref_twitter), 0 != prefsV0.getInt(getKey(R.string.pref_twitter), 0));
- e.putBoolean(getKey(R.string.pref_showaddress), 0 != prefsV0.getInt(getKey(R.string.pref_showaddress), 1));
- e.putBoolean(getKey(R.string.pref_showcaptcha), prefsV0.getBoolean(getKey(R.string.pref_showcaptcha), false));
- e.putBoolean(getKey(R.string.pref_maptrail), prefsV0.getInt(getKey(R.string.pref_maptrail), 1) != 0);
- e.putInt(getKey(R.string.pref_lastmapzoom), prefsV0.getInt(getKey(R.string.pref_lastmapzoom), 14));
- e.putBoolean(getKey(R.string.pref_livelist), 0 != prefsV0.getInt(getKey(R.string.pref_livelist), 1));
- e.putBoolean(getKey(R.string.pref_units_imperial), prefsV0.getInt(getKey(R.string.pref_units_imperial), 1) != 1);
- e.putBoolean(getKey(R.string.pref_skin), prefsV0.getInt(getKey(R.string.pref_skin), 0) != 0);
- e.putInt(getKey(R.string.pref_lastusedlist), prefsV0.getInt(getKey(R.string.pref_lastusedlist), StoredList.STANDARD_LIST_ID));
- e.putString(getKey(R.string.pref_cachetype), prefsV0.getString(getKey(R.string.pref_cachetype), CacheType.ALL.id));
- e.putString(getKey(R.string.pref_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_twitter_token_secret), null));
- e.putString(getKey(R.string.pref_twitter_token_public), prefsV0.getString(getKey(R.string.pref_twitter_token_public), null));
- e.putInt(getKey(R.string.pref_version), prefsV0.getInt(getKey(R.string.pref_version), 0));
- e.putBoolean(getKey(R.string.pref_autoloaddesc), 0 != prefsV0.getInt(getKey(R.string.pref_autoloaddesc), 1));
- e.putBoolean(getKey(R.string.pref_ratingwanted), prefsV0.getBoolean(getKey(R.string.pref_ratingwanted), true));
- e.putBoolean(getKey(R.string.pref_friendlogswanted), prefsV0.getBoolean(getKey(R.string.pref_friendlogswanted), true));
- e.putBoolean(getKey(R.string.pref_useenglish), prefsV0.getBoolean(getKey(R.string.pref_useenglish), false));
- e.putBoolean(getKey(R.string.pref_usecompass), 0 != prefsV0.getInt(getKey(R.string.pref_usecompass), 1));
- e.putBoolean(getKey(R.string.pref_trackautovisit), prefsV0.getBoolean(getKey(R.string.pref_trackautovisit), false));
- e.putBoolean(getKey(R.string.pref_sigautoinsert), prefsV0.getBoolean(getKey(R.string.pref_sigautoinsert), false));
- e.putBoolean(getKey(R.string.pref_logimages), prefsV0.getBoolean(getKey(R.string.pref_logimages), false));
- e.putBoolean(getKey(R.string.pref_excludedisabled), 0 != prefsV0.getInt(getKey(R.string.pref_excludedisabled), 0));
- e.putBoolean(getKey(R.string.pref_excludemine), 0 != prefsV0.getInt(getKey(R.string.pref_excludemine), 0));
- e.putString(getKey(R.string.pref_mapfile), prefsV0.getString(getKey(R.string.pref_mapfile), null));
- e.putString(getKey(R.string.pref_signature), prefsV0.getString(getKey(R.string.pref_signature), null));
- e.putString(getKey(R.string.pref_pass_vote), prefsV0.getString(getKey(R.string.pref_pass_vote), null));
- e.putString(getKey(R.string.pref_password), prefsV0.getString(getKey(R.string.pref_password), null));
- e.putString(getKey(R.string.pref_username), prefsV0.getString(getKey(R.string.pref_username), null));
- e.putString(getKey(R.string.pref_memberstatus), prefsV0.getString(getKey(R.string.pref_memberstatus), ""));
- e.putInt(getKey(R.string.pref_coordinputformat), prefsV0.getInt(getKey(R.string.pref_coordinputformat), CoordInputFormatEnum.DEFAULT_INT_VALUE));
- e.putBoolean(getKey(R.string.pref_log_offline), prefsV0.getBoolean(getKey(R.string.pref_log_offline), false));
- e.putBoolean(getKey(R.string.pref_choose_list), prefsV0.getBoolean(getKey(R.string.pref_choose_list), true));
- e.putBoolean(getKey(R.string.pref_loaddirectionimg), prefsV0.getBoolean(getKey(R.string.pref_loaddirectionimg), true));
- e.putString(getKey(R.string.pref_gccustomdate), prefsV0.getString(getKey(R.string.pref_gccustomdate), GCConstants.DEFAULT_GC_DATE));
- e.putInt(getKey(R.string.pref_showwaypointsthreshold), prefsV0.getInt(getKey(R.string.pref_showwaypointsthreshold), SHOW_WP_THRESHOLD_DEFAULT));
- e.putString(getKey(R.string.pref_cookiestore), prefsV0.getString(getKey(R.string.pref_cookiestore), null));
- e.putBoolean(getKey(R.string.pref_opendetailslastpage), prefsV0.getBoolean(getKey(R.string.pref_opendetailslastpage), false));
- e.putInt(getKey(R.string.pref_lastdetailspage), prefsV0.getInt(getKey(R.string.pref_lastdetailspage), 1));
- e.putInt(getKey(R.string.pref_defaultNavigationTool), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id));
- e.putInt(getKey(R.string.pref_defaultNavigationTool2), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id));
- e.putInt(getKey(R.string.pref_livemapstrategy), prefsV0.getInt(getKey(R.string.pref_livemapstrategy), LivemapStrategy.AUTO.id));
- e.putBoolean(getKey(R.string.pref_debug), prefsV0.getBoolean(getKey(R.string.pref_debug), false));
- e.putInt(getKey(R.string.pref_livemaphintshowcount), prefsV0.getInt(getKey(R.string.pref_livemaphintshowcount), 0));
-
- e.putInt(getKey(R.string.pref_settingsversion), 1); // mark migrated
- e.apply();
- }
-
- // changes for new settings dialog
- if (currentVersion < 2) {
- final Editor e = sharedPrefs.edit();
-
- e.putBoolean(getKey(R.string.pref_units_imperial), useImperialUnits());
-
- // show waypoints threshold now as a slider
- int wpThreshold = getWayPointsThreshold();
- if (wpThreshold < 0) {
- wpThreshold = 0;
- } else if (wpThreshold > SHOW_WP_THRESHOLD_MAX) {
- wpThreshold = SHOW_WP_THRESHOLD_MAX;
- }
- e.putInt(getKey(R.string.pref_showwaypointsthreshold), wpThreshold);
-
- // KEY_MAP_SOURCE must be string, because it is the key for a ListPreference now
- final int ms = sharedPrefs.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT);
- e.remove(getKey(R.string.pref_mapsource));
- e.putString(getKey(R.string.pref_mapsource), String.valueOf(ms));
-
- // navigation tool ids must be string, because ListPreference uses strings as keys
- final int dnt1 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id);
- final int dnt2 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id);
- e.remove(getKey(R.string.pref_defaultNavigationTool));
- e.remove(getKey(R.string.pref_defaultNavigationTool2));
- e.putString(getKey(R.string.pref_defaultNavigationTool), String.valueOf(dnt1));
- e.putString(getKey(R.string.pref_defaultNavigationTool2), String.valueOf(dnt2));
-
- // defaults for gpx directories
- e.putString(getKey(R.string.pref_gpxImportDir), getGpxImportDir());
- e.putString(getKey(R.string.pref_gpxExportDir), getGpxExportDir());
-
- e.putInt(getKey(R.string.pref_settingsversion), 2); // mark migrated
- e.apply();
- }
- }
-
- private static String getKey(final int prefKeyId) {
- return CgeoApplication.getInstance().getString(prefKeyId);
- }
-
- static String getString(final int prefKeyId, final String defaultValue) {
- return sharedPrefs.getString(getKey(prefKeyId), defaultValue);
- }
-
- private static int getInt(final int prefKeyId, final int defaultValue) {
- return sharedPrefs.getInt(getKey(prefKeyId), defaultValue);
- }
-
- private static long getLong(final int prefKeyId, final long defaultValue) {
- return sharedPrefs.getLong(getKey(prefKeyId), defaultValue);
- }
-
- private static boolean getBoolean(final int prefKeyId, final boolean defaultValue) {
- return sharedPrefs.getBoolean(getKey(prefKeyId), defaultValue);
- }
-
- private static float getFloat(final int prefKeyId, final float defaultValue) {
- return sharedPrefs.getFloat(getKey(prefKeyId), defaultValue);
- }
-
- protected static void putString(final int prefKeyId, final String value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putString(getKey(prefKeyId), value);
- edit.apply();
- }
-
- protected static void putBoolean(final int prefKeyId, final boolean value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putBoolean(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void putInt(final int prefKeyId, final int value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putInt(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void putLong(final int prefKeyId, final long value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putLong(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void putFloat(final int prefKeyId, final float value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putFloat(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void remove(final int prefKeyId) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.remove(getKey(prefKeyId));
- edit.apply();
- }
-
- private static boolean contains(final int prefKeyId) {
- return sharedPrefs.contains(getKey(prefKeyId));
- }
-
- public static boolean hasGCCredentials() {
- final String preUsername = getString(R.string.pref_username, null);
- final String prePassword = getString(R.string.pref_password, null);
-
- return !StringUtils.isBlank(preUsername) && !StringUtils.isBlank(prePassword);
- }
-
- /**
- * Get login and password information of Geocaching.com.
- *
- * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
- */
- public static ImmutablePair<String, String> getGcCredentials() {
- return getCredentials(GCConnector.getInstance());
- }
-
- /**
- * Get login and password information.
- *
- * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
- */
- public static ImmutablePair<String, String> getCredentials(final @NonNull ICredentials connector) {
- final String username = getString(connector.getUsernamePreferenceKey(), null);
- final String password = getString(connector.getPasswordPreferenceKey(), null);
-
- if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
- return new ImmutablePair<>(StringUtils.EMPTY, StringUtils.EMPTY);
- }
-
- return new ImmutablePair<>(username, password);
- }
-
- public static String getUsername() {
- return getString(R.string.pref_username, StringUtils.EMPTY);
- }
-
- public static boolean isGCConnectorActive() {
- return getBoolean(R.string.pref_connectorGCActive, true);
- }
-
- public static boolean isECConnectorActive() {
- return getBoolean(R.string.pref_connectorECActive, false);
- }
-
- public static boolean isGCPremiumMember() {
- final String memberStatus = getGCMemberStatus();
- return StringUtils.equalsIgnoreCase(memberStatus, GCConstants.MEMBER_STATUS_PREMIUM) ||
- StringUtils.equalsIgnoreCase(memberStatus, GCConstants.MEMBER_STATUS_CHARTER);
- }
-
- public static String getGCMemberStatus() {
- return getString(R.string.pref_memberstatus, "");
- }
-
- public static void setGCMemberStatus(final String memberStatus) {
- if (StringUtils.isBlank(memberStatus)) {
- remove(R.string.pref_memberstatus);
- }
- putString(R.string.pref_memberstatus, memberStatus);
- }
-
- public static ImmutablePair<String, String> getTokenPair(final int tokenPublicPrefKey, final int tokenSecretPrefKey) {
- return new ImmutablePair<>(getString(tokenPublicPrefKey, null), getString(tokenSecretPrefKey, null));
- }
-
- public static void setTokens(final int tokenPublicPrefKey, @Nullable final String tokenPublic, final int tokenSecretPrefKey, @Nullable final String tokenSecret) {
- if (tokenPublic == null) {
- remove(tokenPublicPrefKey);
- } else {
- putString(tokenPublicPrefKey, tokenPublic);
- }
- if (tokenSecret == null) {
- remove(tokenSecretPrefKey);
- } else {
- putString(tokenSecretPrefKey, tokenSecret);
- }
- }
-
- public static boolean isOCConnectorActive(final int isActivePrefKeyId) {
- return getBoolean(isActivePrefKeyId, false);
- }
-
- public static boolean hasOCAuthorization(final int tokenPublicPrefKeyId, final int tokenSecretPrefKeyId) {
- return StringUtils.isNotBlank(getString(tokenPublicPrefKeyId, ""))
- && StringUtils.isNotBlank(getString(tokenSecretPrefKeyId, ""));
- }
-
- public static boolean isGCVoteLogin() {
- return getGCVoteLogin() != null;
- }
-
- public static ImmutablePair<String, String> getGCVoteLogin() {
- final String username = getString(R.string.pref_username, null);
- final String password = getString(R.string.pref_pass_vote, null);
-
- if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
- return null;
- }
-
- return new ImmutablePair<>(username, password);
- }
-
- public static String getSignature() {
- return getString(R.string.pref_signature, StringUtils.EMPTY);
- }
-
- public static void setCookieStore(final String cookies) {
- if (StringUtils.isBlank(cookies)) {
- // erase cookies
- remove(R.string.pref_cookiestore);
- }
- // save cookies
- putString(R.string.pref_cookiestore, cookies);
- }
-
- public static String getCookieStore() {
- return getString(R.string.pref_cookiestore, null);
- }
-
- public static void setUseGooglePlayServices(final boolean value) {
- putBoolean(R.string.pref_googleplayservices, value);
- }
-
- public static boolean useGooglePlayServices() {
- // By defaut, enable play services starting from ICS.
- return CgeoApplication.getInstance().isGooglePlayServicesAvailable() &&
- getBoolean(R.string.pref_googleplayservices, VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH);
- }
-
- public static boolean useLowPowerMode() {
- return getBoolean(R.string.pref_lowpowermode, false);
- }
-
- /**
- * @param cacheType
- * The cache type used for future filtering
- */
- public static void setCacheType(final CacheType cacheType) {
- if (cacheType == null) {
- remove(R.string.pref_cachetype);
- } else {
- putString(R.string.pref_cachetype, cacheType.id);
- }
- }
-
- public static int getLastList() {
- return getInt(R.string.pref_lastusedlist, StoredList.STANDARD_LIST_ID);
- }
-
- public static void saveLastList(final int listId) {
- putInt(R.string.pref_lastusedlist, listId);
- }
-
- public static void setWebNameCode(final String name, final String code) {
- putString(R.string.pref_webDeviceName, name);
- putString(R.string.pref_webDeviceCode, code);
- }
-
- public static MapProvider getMapProvider() {
- return getMapSource().getMapProvider();
- }
-
- public static String getMapFile() {
- return getString(R.string.pref_mapfile, null);
- }
-
- static void setMapFile(final String mapFile) {
- putString(R.string.pref_mapfile, mapFile);
- if (mapFile != null) {
- setMapFileDirectory(new File(mapFile).getParent());
- }
- }
-
- public static String getMapFileDirectory() {
- final String mapDir = getString(R.string.pref_mapDirectory, null);
- if (mapDir != null) {
- return mapDir;
- }
- final String mapFile = getMapFile();
- if (mapFile != null) {
- return new File(mapFile).getParent();
- }
- return null;
- }
-
- static void setMapFileDirectory(final String mapFileDirectory) {
- putString(R.string.pref_mapDirectory, mapFileDirectory);
- MapsforgeMapProvider.getInstance().updateOfflineMaps();
- }
-
- private static boolean isValidMapFile() {
- return isValidMapFile(getMapFile());
- }
-
- public static boolean isValidMapFile(final String mapFileIn) {
- return MapsforgeMapProvider.isValidMapFile(mapFileIn);
- }
-
- public static boolean isScaleMapsforgeText() {
- return getBoolean(R.string.pref_mapsforge_scale_text, true);
- }
-
- public static CoordInputFormatEnum getCoordInputFormat() {
- return CoordInputFormatEnum.fromInt(getInt(R.string.pref_coordinputformat, CoordInputFormatEnum.DEFAULT_INT_VALUE));
- }
-
- public static void setCoordInputFormat(final CoordInputFormatEnum format) {
- putInt(R.string.pref_coordinputformat, format.ordinal());
- }
-
- public static boolean getLogOffline() {
- return getBoolean(R.string.pref_log_offline, false);
- }
-
- public static boolean getChooseList() {
- return getBoolean(R.string.pref_choose_list, false);
- }
-
- public static boolean getLoadDirImg() {
- return !isGCPremiumMember() && getBoolean(R.string.pref_loaddirectionimg, true);
- }
-
- public static void setGcCustomDate(final String format) {
- putString(R.string.pref_gccustomdate, format);
- }
-
- /**
- * @return User selected date format on GC.com
- */
- public static String getGcCustomDate() {
- // We might have some users whose stored value is null, which is invalid. In this case, we use the default.
- return StringUtils.defaultString(getString(R.string.pref_gccustomdate, GCConstants.DEFAULT_GC_DATE),
- GCConstants.DEFAULT_GC_DATE);
- }
-
- public static boolean isExcludeMyCaches() {
- return getBoolean(R.string.pref_excludemine, false);
- }
-
- public static boolean useEnglish() {
- return getBoolean(R.string.pref_useenglish, false);
- }
-
- public static boolean isShowAddress() {
- return getBoolean(R.string.pref_showaddress, true);
- }
-
- public static boolean isShowCaptcha() {
- return !isGCPremiumMember() && getBoolean(R.string.pref_showcaptcha, false);
- }
-
- public static boolean isExcludeDisabledCaches() {
- return getBoolean(R.string.pref_excludedisabled, false);
- }
-
- public static boolean isStoreOfflineMaps() {
- return getBoolean(R.string.pref_offlinemaps, true);
- }
-
- public static boolean isStoreOfflineWpMaps() {
- return getBoolean(R.string.pref_offlinewpmaps, false);
- }
-
- public static boolean isStoreLogImages() {
- return getBoolean(R.string.pref_logimages, false);
- }
-
- public static boolean isAutoLoadDescription() {
- return getBoolean(R.string.pref_autoloaddesc, true);
- }
-
- public static boolean isRatingWanted() {
- return getBoolean(R.string.pref_ratingwanted, true);
- }
-
- public static boolean isGeokretyConnectorActive() {
- return getBoolean(R.string.pref_connectorGeokretyActive, false);
- }
-
- public static boolean isGeokretyCacheActive() {
- return getBoolean(R.string.pref_geokrety_cache, true);
- }
-
- static boolean hasGeokretyAuthorization() {
- return StringUtils.isNotBlank(getGeokretySecId());
- }
-
- public static String getGeokretySecId() {
- return getString(R.string.pref_fakekey_geokrety_authorization, null);
- }
-
- public static void setGeokretySecId(final String secid) {
- putString(R.string.pref_fakekey_geokrety_authorization, secid);
- }
-
- public static boolean isRegisteredForGeokretyLogging() {
- return getGeokretySecId() != null;
- }
-
- /**
- * Retrieve showed popup counter for warning about logging Trackable recommend Geocode
- *
- * @return number of times the popup has appeared
- */
- public static int getLogTrackableWithoutGeocodeShowCount() {
- return getInt(R.string.pref_logtrackablewithoutgeocodeshowcount, 0);
- }
-
- /**
- * Store showed popup counter for warning about logging Trackable recommend Geocode
- *
- * @param showCount the count to save
- */
- public static void setLogTrackableWithoutGeocodeShowCount(final int showCount) {
- putInt(R.string.pref_logtrackablewithoutgeocodeshowcount, showCount);
- }
-
- public static boolean isFriendLogsWanted() {
- if (!hasGCCredentials()) {
- // don't show a friends log if the user is anonymous
- return false;
- }
- return getBoolean(R.string.pref_friendlogswanted, true);
- }
-
- public static boolean isLiveList() {
- return getBoolean(R.string.pref_livelist, true);
- }
-
- public static boolean isTrackableAutoVisit() {
- return getBoolean(R.string.pref_trackautovisit, false);
- }
-
- public static boolean isAutoInsertSignature() {
- return getBoolean(R.string.pref_sigautoinsert, false);
- }
-
- static void setUseImperialUnits(final boolean useImperialUnits) {
- putBoolean(R.string.pref_units_imperial, useImperialUnits);
- }
-
- public static boolean useImperialUnits() {
- return getBoolean(R.string.pref_units_imperial, useImperialUnitsByDefault());
- }
-
- private static boolean useImperialUnitsByDefault() {
- final String countryCode = Locale.getDefault().getCountry();
- return "US".equals(countryCode) // USA
- || "LR".equals(countryCode) // Liberia
- || "MM".equals(countryCode); // Burma
- }
-
- public static boolean isLiveMap() {
- return getBoolean(R.string.pref_maplive, true);
- }
-
- public static void setLiveMap(final boolean live) {
- putBoolean(R.string.pref_maplive, live);
- }
-
- public static boolean isMapTrail() {
- return getBoolean(R.string.pref_maptrail, false);
- }
-
- public static void setMapTrail(final boolean showTrail) {
- putBoolean(R.string.pref_maptrail, showTrail);
- }
-
- /**
- * whether to show a direction line on the map
- */
- public static boolean isMapDirection() {
- return getBoolean(R.string.pref_map_direction, true);
- }
-
- public static void setMapDirection(final boolean showDirection) {
- putBoolean(R.string.pref_map_direction, showDirection);
- }
-
- /**
- * Get last used zoom of the internal map. Differentiate between two use cases for a map of multiple caches (e.g.
- * live map) and the map of a single cache (which is often zoomed in more deep).
- */
- public static int getMapZoom(final MapMode mapMode) {
- if (mapMode == MapMode.SINGLE || mapMode == MapMode.COORDS) {
- return getCacheZoom();
- }
- return getMapZoom();
- }
-
- public static void setMapZoom(final MapMode mapMode, final int zoomLevel) {
- if (mapMode == MapMode.SINGLE || mapMode == MapMode.COORDS) {
- setCacheZoom(zoomLevel);
- }
- else {
- setMapZoom(zoomLevel);
- }
- }
-
- /**
- * @return zoom used for the (live) map
- */
- private static int getMapZoom() {
- return Math.max(getInt(R.string.pref_lastmapzoom, 14), INITIAL_MAP_ZOOM_LIMIT);
- }
-
- private static void setMapZoom(final int mapZoomLevel) {
- putInt(R.string.pref_lastmapzoom, mapZoomLevel);
- }
-
- /**
- * @return zoom used for the map of a single cache
- */
- private static int getCacheZoom() {
- return Math.max(getInt(R.string.pref_cache_zoom, 14), INITIAL_MAP_ZOOM_LIMIT);
- }
-
- private static void setCacheZoom(final int zoomLevel) {
- putInt(R.string.pref_cache_zoom, zoomLevel);
- }
-
- public static GeoPointImpl getMapCenter() {
- return getMapProvider().getMapItemFactory()
- .getGeoPointBase(new Geopoint(getInt(R.string.pref_lastmaplat, 0) / 1e6,
- getInt(R.string.pref_lastmaplon, 0) / 1e6));
- }
-
- public static void setMapCenter(final GeoPointImpl mapViewCenter) {
- putInt(R.string.pref_lastmaplat, mapViewCenter.getLatitudeE6());
- putInt(R.string.pref_lastmaplon, mapViewCenter.getLongitudeE6());
- }
-
- @NonNull
- public static synchronized MapSource getMapSource() {
- if (mapSource != null) {
- return mapSource;
- }
- final int id = getConvertedMapId();
- mapSource = MapProviderFactory.getMapSource(id);
- if (mapSource != null) {
- // don't use offline maps if the map file is not valid
- if (!(mapSource instanceof OfflineMapSource) || isValidMapFile()) {
- return mapSource;
- }
- }
- // fallback to first available map
- return MapProviderFactory.getDefaultSource();
- }
-
- private final static int GOOGLEMAP_BASEID = 30;
- private final static int MAP = 1;
- private final static int SATELLITE = 2;
-
- private final static int MFMAP_BASEID = 40;
- private final static int MAPNIK = 1;
- private final static int CYCLEMAP = 3;
- private final static int OFFLINE = 4;
- private static final int HISTORY_SIZE = 10;
-
- /**
- * Convert old preference ids for maps (based on constant values) into new hash based ids.
- */
- private static int getConvertedMapId() {
- final int id = Integer.parseInt(getString(R.string.pref_mapsource,
- String.valueOf(MAP_SOURCE_DEFAULT)));
- switch (id) {
- case GOOGLEMAP_BASEID + MAP:
- return GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
- case GOOGLEMAP_BASEID + SATELLITE:
- return GoogleMapProvider.GOOGLE_SATELLITE_ID.hashCode();
- case MFMAP_BASEID + MAPNIK:
- return MapsforgeMapProvider.MAPSFORGE_MAPNIK_ID.hashCode();
- case MFMAP_BASEID + CYCLEMAP:
- return MapsforgeMapProvider.MAPSFORGE_CYCLEMAP_ID.hashCode();
- case MFMAP_BASEID + OFFLINE: {
- final String mapFile = getMapFile();
- if (StringUtils.isNotEmpty(mapFile)) {
- return mapFile.hashCode();
- }
- break;
- }
- default:
- break;
- }
- return id;
- }
-
- public static synchronized void setMapSource(final MapSource newMapSource) {
- putString(R.string.pref_mapsource, String.valueOf(newMapSource.getNumericalId()));
- if (newMapSource instanceof OfflineMapSource) {
- setMapFile(((OfflineMapSource) newMapSource).getFileName());
- }
- // cache the value
- mapSource = newMapSource;
- }
-
- public static void setAnyCoordinates(final Geopoint coords) {
- if (null != coords) {
- putFloat(R.string.pref_anylatitude, (float) coords.getLatitude());
- putFloat(R.string.pref_anylongitude, (float) coords.getLongitude());
- } else {
- remove(R.string.pref_anylatitude);
- remove(R.string.pref_anylongitude);
- }
- }
-
- public static Geopoint getAnyCoordinates() {
- if (contains(R.string.pref_anylatitude) && contains(R.string.pref_anylongitude)) {
- final float lat = getFloat(R.string.pref_anylatitude, 0);
- final float lon = getFloat(R.string.pref_anylongitude, 0);
- return new Geopoint(lat, lon);
- }
- return null;
- }
-
- public static boolean isUseCompass() {
- return useCompass;
- }
-
- public static void setUseCompass(final boolean value) {
- useCompass = value;
- }
-
- public static boolean isLightSkin() {
- return getBoolean(R.string.pref_skin, false);
- }
-
- @NonNull
- public static String getTwitterKeyConsumerPublic() {
- return TWITTER_KEY_CONSUMER_PUBLIC;
- }
-
- @NonNull
- public static String getTwitterKeyConsumerSecret() {
- return TWITTER_KEY_CONSUMER_SECRET;
- }
-
- public static String getWebDeviceCode() {
- return getString(R.string.pref_webDeviceCode, null);
- }
-
- public static boolean isRegisteredForSend2cgeo() {
- return getWebDeviceCode() != null;
- }
-
- static String getWebDeviceName() {
- return getString(R.string.pref_webDeviceName, Build.MODEL);
- }
-
- /**
- * @return The cache type used for filtering or ALL if no filter is active.
- * Returns never null
- */
- @NonNull
- public static CacheType getCacheType() {
- return CacheType.getById(getString(R.string.pref_cachetype, CacheType.ALL.id));
- }
-
- /**
- * The Threshold for the showing of child waypoints
- */
- public static int getWayPointsThreshold() {
- return getInt(R.string.pref_showwaypointsthreshold, SHOW_WP_THRESHOLD_DEFAULT);
- }
-
- static void setShowWaypointsThreshold(final int threshold) {
- putInt(R.string.pref_showwaypointsthreshold, threshold);
- }
-
- public static boolean isUseTwitter() {
- return getBoolean(R.string.pref_twitter, false);
- }
-
- private static void setUseTwitter(final boolean useTwitter) {
- putBoolean(R.string.pref_twitter, useTwitter);
- }
-
- public static boolean isTwitterLoginValid() {
- return !StringUtils.isBlank(getTokenPublic())
- && !StringUtils.isBlank(getTokenSecret());
- }
-
- public static String getTokenPublic() {
- return getString(R.string.pref_twitter_token_public, null);
- }
-
- public static String getTokenSecret() {
- return getString(R.string.pref_twitter_token_secret, null);
-
- }
-
- static boolean hasTwitterAuthorization() {
- return StringUtils.isNotBlank(getTokenPublic())
- && StringUtils.isNotBlank(getTokenSecret());
- }
-
- public static void setTwitterTokens(@Nullable final String tokenPublic,
- @Nullable final String tokenSecret, final boolean enableTwitter) {
- putString(R.string.pref_twitter_token_public, tokenPublic);
- putString(R.string.pref_twitter_token_secret, tokenSecret);
- if (tokenPublic != null) {
- remove(R.string.pref_temp_twitter_token_public);
- remove(R.string.pref_temp_twitter_token_secret);
- }
- setUseTwitter(enableTwitter);
- }
-
- public static void setTwitterTempTokens(@Nullable final String tokenPublic,
- @Nullable final String tokenSecret) {
- putString(R.string.pref_temp_twitter_token_public, tokenPublic);
- putString(R.string.pref_temp_twitter_token_secret, tokenSecret);
- }
-
- public static ImmutablePair<String, String> getTempToken() {
- final String tokenPublic = getString(R.string.pref_temp_twitter_token_public, null);
- final String tokenSecret = getString(R.string.pref_temp_twitter_token_secret, null);
- return new ImmutablePair<>(tokenPublic, tokenSecret);
- }
-
- public static int getVersion() {
- return getInt(R.string.pref_version, 0);
- }
-
- public static void setVersion(final int version) {
- putInt(R.string.pref_version, version);
- }
-
- public static boolean isOpenLastDetailsPage() {
- return getBoolean(R.string.pref_opendetailslastpage, false);
- }
-
- public static int getLastDetailsPage() {
- return getInt(R.string.pref_lastdetailspage, 1);
- }
-
- public static void setLastDetailsPage(final int index) {
- putInt(R.string.pref_lastdetailspage, index);
- }
-
- public static int getDefaultNavigationTool() {
- return Integer.parseInt(getString(
- R.string.pref_defaultNavigationTool,
- String.valueOf(NavigationAppsEnum.COMPASS.id)));
- }
-
- public static int getDefaultNavigationTool2() {
- return Integer.parseInt(getString(
- R.string.pref_defaultNavigationTool2,
- String.valueOf(NavigationAppsEnum.INTERNAL_MAP.id)));
- }
-
- public static LivemapStrategy getLiveMapStrategy() {
- return LivemapStrategy.getById(getInt(R.string.pref_livemapstrategy, LivemapStrategy.AUTO.id));
- }
-
- public static void setLiveMapStrategy(final LivemapStrategy strategy) {
- putInt(R.string.pref_livemapstrategy, strategy.id);
- }
-
- public static boolean isDebug() {
- return Log.isDebug();
- }
-
- public static int getLiveMapHintShowCount() {
- return getInt(R.string.pref_livemaphintshowcount, 0);
- }
-
- public static void setLiveMapHintShowCount(final int showCount) {
- putInt(R.string.pref_livemaphintshowcount, showCount);
- }
-
- public static boolean isDbOnSDCard() {
- return getBoolean(R.string.pref_dbonsdcard, false);
- }
-
- public static void setDbOnSDCard(final boolean dbOnSDCard) {
- putBoolean(R.string.pref_dbonsdcard, dbOnSDCard);
- }
-
- public static String getGpxExportDir() {
- return getString(R.string.pref_gpxExportDir,
- Environment.getExternalStorageDirectory().getPath() + "/gpx");
- }
-
- public static String getGpxImportDir() {
- return getString(R.string.pref_gpxImportDir,
- Environment.getExternalStorageDirectory().getPath() + "/gpx");
- }
-
- public static boolean getShareAfterExport() {
- return getBoolean(R.string.pref_shareafterexport, true);
- }
-
- public static void setShareAfterExport(final boolean shareAfterExport) {
- putBoolean(R.string.pref_shareafterexport, shareAfterExport);
- }
-
- public static int getTrackableAction() {
- return getInt(R.string.pref_trackableaction, LogTypeTrackable.RETRIEVED_IT.id);
- }
-
- public static void setTrackableAction(final int trackableAction) {
- putInt(R.string.pref_trackableaction, trackableAction);
- }
-
- private static String getCustomRenderThemeBaseFolder() {
- return getString(R.string.pref_renderthemepath, "");
- }
-
- public static String getCustomRenderThemeFilePath() {
- return getString(R.string.pref_renderthemefile, "");
- }
-
- public static void setCustomRenderThemeFile(final String customRenderThemeFile) {
- putString(R.string.pref_renderthemefile, customRenderThemeFile);
- }
-
- public static File[] getMapThemeFiles() {
- final File directory = new File(getCustomRenderThemeBaseFolder());
- final List<File> result = new ArrayList<>();
- FileUtils.listDir(result, directory, new ExtensionsBasedFileSelector(new String[] { "xml" }), null);
-
- return result.toArray(new File[result.size()]);
- }
-
- private static class ExtensionsBasedFileSelector implements FileSelector {
- private final String[] extensions;
- public ExtensionsBasedFileSelector(final String[] extensions) {
- this.extensions = extensions;
- }
- @Override
- public boolean isSelected(final File file) {
- final String filename = file.getName();
- for (final String ext : extensions) {
- if (StringUtils.endsWithIgnoreCase(filename, ext)) {
- return true;
- }
- }
- return false;
- }
- @Override
- public boolean shouldEnd() {
- return false;
- }
- }
-
- /**
- * @return true if plain text log wanted
- */
- public static boolean getPlainLogs() {
- return getBoolean(R.string.pref_plainLogs, false);
- }
-
- /**
- * Force set the plain text log preference
- *
- * @param plainLogs
- * wanted or not
- */
- public static void setPlainLogs(final boolean plainLogs) {
- putBoolean(R.string.pref_plainLogs, plainLogs);
- }
-
- public static boolean getUseNativeUa() {
- return getBoolean(R.string.pref_nativeUa, false);
- }
-
- public static String getCacheTwitterMessage() {
- return getString(R.string.pref_twitter_cache_message, "I found [NAME] ([URL]).");
- }
-
- public static String getTrackableTwitterMessage() {
- return getString(R.string.pref_twitter_trackable_message, "I touched [NAME] ([URL]).");
- }
-
- public static int getLogImageScale() {
- return getInt(R.string.pref_logImageScale, -1);
- }
-
- public static void setLogImageScale(final int scale) {
- putInt(R.string.pref_logImageScale, scale);
- }
-
- public static void setExcludeMine(final boolean exclude) {
- putBoolean(R.string.pref_excludemine, exclude);
- }
-
- static void setLogin(final String username, final String password) {
- if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
- // erase username and password
- remove(R.string.pref_username);
- remove(R.string.pref_password);
- return;
- }
- // save username and password
- putString(R.string.pref_username, username);
- putString(R.string.pref_password, password);
- }
-
- public static long getFieldnoteExportDate() {
- return getLong(R.string.pref_fieldNoteExportDate, 0);
- }
-
- /**
- * Remember date of last field note export.
- */
- public static void setFieldnoteExportDate(final long date) {
- putLong(R.string.pref_fieldNoteExportDate, date);
- }
-
- public static boolean isUseNavigationApp(final NavigationAppsEnum navApp) {
- return getBoolean(navApp.preferenceKey, true);
- }
-
- /**
- * Remember the state of the "Upload" checkbox in the field notes export dialog.
- */
- public static void setFieldNoteExportUpload(final boolean upload) {
- putBoolean(R.string.pref_fieldNoteExportUpload, upload);
- }
-
- public static boolean getFieldNoteExportUpload() {
- return getBoolean(R.string.pref_fieldNoteExportUpload, true);
- }
-
- /**
- * Remember the state of the "Only new" checkbox in the field notes export dialog.
- */
- public static void setFieldNoteExportOnlyNew(final boolean onlyNew) {
- putBoolean(R.string.pref_fieldNoteExportOnlyNew, onlyNew);
- }
-
- public static boolean getFieldNoteExportOnlyNew() {
- return getBoolean(R.string.pref_fieldNoteExportOnlyNew, false);
- }
-
- public static String getECIconSet() {
- return getString(R.string.pref_ec_icons, "1");
- }
-
- /* Store last checksum of changelog for changelog display */
- public static long getLastChangelogChecksum() {
- return getLong(R.string.pref_changelog_last_checksum, 0);
- }
-
- public static void setLastChangelogChecksum(final long checksum) {
- putLong(R.string.pref_changelog_last_checksum, checksum);
- }
-
- public static List<String> getLastOpenedCaches() {
- final List<String> history = Arrays.asList(StringUtils.split(getString(R.string.pref_caches_history, StringUtils.EMPTY), HISTORY_SEPARATOR));
- return history.subList(0, Math.min(HISTORY_SIZE, history.size()));
- }
-
- public static void addCacheToHistory(@NonNull final String geocode) {
- final List<String> history = new ArrayList<>(getLastOpenedCaches());
- // bring entry to front, if it already existed
- history.remove(geocode);
- history.add(0, geocode);
- putString(R.string.pref_caches_history, StringUtils.join(history, HISTORY_SEPARATOR));
- }
-
- public static boolean useHardwareAcceleration() {
- return getBoolean(R.string.pref_hardware_acceleration, !HW_ACCEL_DISABLED_BY_DEFAULT);
- }
-
- static void setUseHardwareAcceleration(final boolean useHardwareAcceleration) {
- putBoolean(R.string.pref_hardware_acceleration, useHardwareAcceleration);
- }
-
- public static String getLastCacheLog() {
- return getString(R.string.pref_last_cache_log, StringUtils.EMPTY);
- }
-
- public static void setLastCacheLog(final String log) {
- putString(R.string.pref_last_cache_log, log);
- }
-
- public static String getLastTrackableLog() {
- return getString(R.string.pref_last_trackable_log, StringUtils.EMPTY);
- }
-
- public static void setLastTrackableLog(final String log) {
- putString(R.string.pref_last_trackable_log, log);
- }
-
- @Nullable
- public static String getHomeLocation() {
- return getString(R.string.pref_home_location, null);
- }
-
- public static void setHomeLocation(@NonNull final String homeLocation) {
- putString(R.string.pref_home_location, homeLocation);
- }
-
- public static void setForceOrientationSensor(final boolean forceOrientationSensor) {
- putBoolean(R.string.pref_force_orientation_sensor, forceOrientationSensor);
- }
-
- public static boolean useOrientationSensor(final Context context) {
- return OrientationProvider.hasOrientationSensor(context) && (getBoolean(R.string.pref_force_orientation_sensor, false) || !RotationProvider.hasRotationSensor(context));
- }
-}
+package cgeo.geocaching.settings;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.R;
+import cgeo.geocaching.apps.navi.NavigationAppFactory.NavigationAppsEnum;
+import cgeo.geocaching.connector.capability.ICredentials;
+import cgeo.geocaching.connector.gc.GCConnector;
+import cgeo.geocaching.connector.gc.GCConstants;
+import cgeo.geocaching.enumerations.CacheType;
+import cgeo.geocaching.enumerations.LogTypeTrackable;
+import cgeo.geocaching.list.StoredList;
+import cgeo.geocaching.location.Geopoint;
+import cgeo.geocaching.maps.CGeoMap.MapMode;
+import cgeo.geocaching.maps.LivemapStrategy;
+import cgeo.geocaching.maps.MapProviderFactory;
+import cgeo.geocaching.maps.google.v1.GoogleMapProvider;
+import cgeo.geocaching.maps.interfaces.GeoPointImpl;
+import cgeo.geocaching.maps.interfaces.MapProvider;
+import cgeo.geocaching.maps.interfaces.MapSource;
+import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider;
+import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider.OfflineMapSource;
+import cgeo.geocaching.sensors.OrientationProvider;
+import cgeo.geocaching.sensors.RotationProvider;
+import cgeo.geocaching.utils.CryptUtils;
+import cgeo.geocaching.utils.FileUtils;
+import cgeo.geocaching.utils.FileUtils.FileSelector;
+import cgeo.geocaching.utils.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * General c:geo preferences/settings set by the user
+ */
+public class Settings {
+
+ /**
+ * On opening a map, we limit the _initial_ zoom. The user can still zoom out afterwards.
+ */
+ private static final int INITIAL_MAP_ZOOM_LIMIT = 16;
+ private static final char HISTORY_SEPARATOR = ',';
+ private static final int SHOW_WP_THRESHOLD_DEFAULT = 10;
+ public static final int SHOW_WP_THRESHOLD_MAX = 50;
+ private static final int MAP_SOURCE_DEFAULT = GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
+
+ public static final boolean HW_ACCEL_DISABLED_BY_DEFAULT =
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
+ StringUtils.equals(Build.MODEL, "HTC One X") || // HTC One X
+ StringUtils.equals(Build.MODEL, "HTC One S") || // HTC One S
+ StringUtils.equals(Build.MODEL, "GT-I8190") || // Samsung S3 mini
+ StringUtils.equals(Build.MODEL, "GT-S6310L") || // Samsung Galaxy Young
+ StringUtils.equals(Build.MODEL, "GT-P5210") || // Samsung Galaxy Tab 3
+ StringUtils.equals(Build.MODEL, "GT-S7580") || // Samsung Galaxy Trend Plus
+ StringUtils.equals(Build.MODEL, "GT-I9105P") || // Samsung Galaxy SII Plus
+ StringUtils.equals(Build.MODEL, "ST25i") || // Sony Xperia U
+ StringUtils.equals(Build.MODEL, "bq Aquaris 5") || // bq Aquaris 5
+ StringUtils.equals(Build.MODEL, "A1-810") || // Unknown A1-810
+ StringUtils.equals(Build.MODEL, "GT-I9195") || // Samsung S4 mini
+ StringUtils.equals(Build.MODEL, "GT-I8200N"); // Samsung S3 mini
+
+ // twitter api keys
+ private final static @NonNull String TWITTER_KEY_CONSUMER_PUBLIC = CryptUtils.rot13("ESnsCvAv3kEupF1GCR3jGj");
+ private final static @NonNull String TWITTER_KEY_CONSUMER_SECRET = CryptUtils.rot13("7vQWceACV9umEjJucmlpFe9FCMZSeqIqfkQ2BnhV9x");
+
+ private static boolean useCompass = true;
+
+ public enum CoordInputFormatEnum {
+ Plain,
+ Deg,
+ Min,
+ Sec;
+
+ static final int DEFAULT_INT_VALUE = Min.ordinal();
+
+ public static CoordInputFormatEnum fromInt(final int id) {
+ final CoordInputFormatEnum[] values = CoordInputFormatEnum.values();
+ if (id < 0 || id >= values.length) {
+ return Min;
+ }
+ return values[id];
+ }
+ }
+
+ private static final SharedPreferences sharedPrefs = PreferenceManager
+ .getDefaultSharedPreferences(CgeoApplication.getInstance().getBaseContext());
+ static {
+ migrateSettings();
+ final boolean isDebug = sharedPrefs.getBoolean(getKey(R.string.pref_debug), false);
+ Log.setDebug(isDebug);
+ CgeoApplication.dumpOnOutOfMemory(isDebug);
+ }
+
+ /**
+ * Cache the mapsource locally. If that is an offline map source, each request would potentially access the
+ * underlying map file, leading to delays.
+ */
+ private static MapSource mapSource;
+
+ protected Settings() {
+ throw new InstantiationError();
+ }
+
+ private static void migrateSettings() {
+ final int LATEST_PREFERENCES_VERSION = 2;
+ final int currentVersion = getInt(R.string.pref_settingsversion, 0);
+
+ // No need to migrate if we are up to date.
+ if (currentVersion == LATEST_PREFERENCES_VERSION) {
+ return;
+ }
+
+ // No need to migrate if we don't have older settings, defaults will be used instead.
+ final String preferencesNameV0 = "cgeo.pref";
+ final SharedPreferences prefsV0 = CgeoApplication.getInstance().getSharedPreferences(preferencesNameV0, Context.MODE_PRIVATE);
+ if (currentVersion == 0 && prefsV0.getAll().isEmpty()) {
+ final Editor e = sharedPrefs.edit();
+ e.putInt(getKey(R.string.pref_settingsversion), LATEST_PREFERENCES_VERSION);
+ e.apply();
+ return;
+ }
+
+ if (currentVersion < 1) {
+ // migrate from non standard file location and integer based boolean types
+ final Editor e = sharedPrefs.edit();
+
+ e.putString(getKey(R.string.pref_temp_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_temp_twitter_token_secret), null));
+ e.putString(getKey(R.string.pref_temp_twitter_token_public), prefsV0.getString(getKey(R.string.pref_temp_twitter_token_public), null));
+ e.putBoolean(getKey(R.string.pref_help_shown), prefsV0.getInt(getKey(R.string.pref_help_shown), 0) != 0);
+ e.putFloat(getKey(R.string.pref_anylongitude), prefsV0.getFloat(getKey(R.string.pref_anylongitude), 0));
+ e.putFloat(getKey(R.string.pref_anylatitude), prefsV0.getFloat(getKey(R.string.pref_anylatitude), 0));
+ e.putBoolean(getKey(R.string.pref_offlinemaps), 0 != prefsV0.getInt(getKey(R.string.pref_offlinemaps), 1));
+ e.putBoolean(getKey(R.string.pref_offlinewpmaps), 0 != prefsV0.getInt(getKey(R.string.pref_offlinewpmaps), 0));
+ e.putString(getKey(R.string.pref_webDeviceCode), prefsV0.getString(getKey(R.string.pref_webDeviceCode), null));
+ e.putString(getKey(R.string.pref_webDeviceName), prefsV0.getString(getKey(R.string.pref_webDeviceName), null));
+ e.putBoolean(getKey(R.string.pref_maplive), prefsV0.getInt(getKey(R.string.pref_maplive), 1) != 0);
+ e.putInt(getKey(R.string.pref_mapsource), prefsV0.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT));
+ e.putBoolean(getKey(R.string.pref_twitter), 0 != prefsV0.getInt(getKey(R.string.pref_twitter), 0));
+ e.putBoolean(getKey(R.string.pref_showaddress), 0 != prefsV0.getInt(getKey(R.string.pref_showaddress), 1));
+ e.putBoolean(getKey(R.string.pref_showcaptcha), prefsV0.getBoolean(getKey(R.string.pref_showcaptcha), false));
+ e.putBoolean(getKey(R.string.pref_maptrail), prefsV0.getInt(getKey(R.string.pref_maptrail), 1) != 0);
+ e.putInt(getKey(R.string.pref_lastmapzoom), prefsV0.getInt(getKey(R.string.pref_lastmapzoom), 14));
+ e.putBoolean(getKey(R.string.pref_livelist), 0 != prefsV0.getInt(getKey(R.string.pref_livelist), 1));
+ e.putBoolean(getKey(R.string.pref_units_imperial), prefsV0.getInt(getKey(R.string.pref_units_imperial), 1) != 1);
+ e.putBoolean(getKey(R.string.pref_skin), prefsV0.getInt(getKey(R.string.pref_skin), 0) != 0);
+ e.putInt(getKey(R.string.pref_lastusedlist), prefsV0.getInt(getKey(R.string.pref_lastusedlist), StoredList.STANDARD_LIST_ID));
+ e.putString(getKey(R.string.pref_cachetype), prefsV0.getString(getKey(R.string.pref_cachetype), CacheType.ALL.id));
+ e.putString(getKey(R.string.pref_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_twitter_token_secret), null));
+ e.putString(getKey(R.string.pref_twitter_token_public), prefsV0.getString(getKey(R.string.pref_twitter_token_public), null));
+ e.putInt(getKey(R.string.pref_version), prefsV0.getInt(getKey(R.string.pref_version), 0));
+ e.putBoolean(getKey(R.string.pref_autoloaddesc), 0 != prefsV0.getInt(getKey(R.string.pref_autoloaddesc), 1));
+ e.putBoolean(getKey(R.string.pref_ratingwanted), prefsV0.getBoolean(getKey(R.string.pref_ratingwanted), true));
+ e.putBoolean(getKey(R.string.pref_friendlogswanted), prefsV0.getBoolean(getKey(R.string.pref_friendlogswanted), true));
+ e.putBoolean(getKey(R.string.pref_useenglish), prefsV0.getBoolean(getKey(R.string.pref_useenglish), false));
+ e.putBoolean(getKey(R.string.pref_usecompass), 0 != prefsV0.getInt(getKey(R.string.pref_usecompass), 1));
+ e.putBoolean(getKey(R.string.pref_trackautovisit), prefsV0.getBoolean(getKey(R.string.pref_trackautovisit), false));
+ e.putBoolean(getKey(R.string.pref_sigautoinsert), prefsV0.getBoolean(getKey(R.string.pref_sigautoinsert), false));
+ e.putBoolean(getKey(R.string.pref_logimages), prefsV0.getBoolean(getKey(R.string.pref_logimages), false));
+ e.putBoolean(getKey(R.string.pref_excludedisabled), 0 != prefsV0.getInt(getKey(R.string.pref_excludedisabled), 0));
+ e.putBoolean(getKey(R.string.pref_excludemine), 0 != prefsV0.getInt(getKey(R.string.pref_excludemine), 0));
+ e.putString(getKey(R.string.pref_mapfile), prefsV0.getString(getKey(R.string.pref_mapfile), null));
+ e.putString(getKey(R.string.pref_signature), prefsV0.getString(getKey(R.string.pref_signature), null));
+ e.putString(getKey(R.string.pref_pass_vote), prefsV0.getString(getKey(R.string.pref_pass_vote), null));
+ e.putString(getKey(R.string.pref_password), prefsV0.getString(getKey(R.string.pref_password), null));
+ e.putString(getKey(R.string.pref_username), prefsV0.getString(getKey(R.string.pref_username), null));
+ e.putString(getKey(R.string.pref_memberstatus), prefsV0.getString(getKey(R.string.pref_memberstatus), ""));
+ e.putInt(getKey(R.string.pref_coordinputformat), prefsV0.getInt(getKey(R.string.pref_coordinputformat), CoordInputFormatEnum.DEFAULT_INT_VALUE));
+ e.putBoolean(getKey(R.string.pref_log_offline), prefsV0.getBoolean(getKey(R.string.pref_log_offline), false));
+ e.putBoolean(getKey(R.string.pref_choose_list), prefsV0.getBoolean(getKey(R.string.pref_choose_list), true));
+ e.putBoolean(getKey(R.string.pref_loaddirectionimg), prefsV0.getBoolean(getKey(R.string.pref_loaddirectionimg), true));
+ e.putString(getKey(R.string.pref_gccustomdate), prefsV0.getString(getKey(R.string.pref_gccustomdate), GCConstants.DEFAULT_GC_DATE));
+ e.putInt(getKey(R.string.pref_showwaypointsthreshold), prefsV0.getInt(getKey(R.string.pref_showwaypointsthreshold), SHOW_WP_THRESHOLD_DEFAULT));
+ e.putString(getKey(R.string.pref_cookiestore), prefsV0.getString(getKey(R.string.pref_cookiestore), null));
+ e.putBoolean(getKey(R.string.pref_opendetailslastpage), prefsV0.getBoolean(getKey(R.string.pref_opendetailslastpage), false));
+ e.putInt(getKey(R.string.pref_lastdetailspage), prefsV0.getInt(getKey(R.string.pref_lastdetailspage), 1));
+ e.putInt(getKey(R.string.pref_defaultNavigationTool), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id));
+ e.putInt(getKey(R.string.pref_defaultNavigationTool2), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id));
+ e.putInt(getKey(R.string.pref_livemapstrategy), prefsV0.getInt(getKey(R.string.pref_livemapstrategy), LivemapStrategy.AUTO.id));
+ e.putBoolean(getKey(R.string.pref_debug), prefsV0.getBoolean(getKey(R.string.pref_debug), false));
+ e.putInt(getKey(R.string.pref_livemaphintshowcount), prefsV0.getInt(getKey(R.string.pref_livemaphintshowcount), 0));
+
+ e.putInt(getKey(R.string.pref_settingsversion), 1); // mark migrated
+ e.apply();
+ }
+
+ // changes for new settings dialog
+ if (currentVersion < 2) {
+ final Editor e = sharedPrefs.edit();
+
+ e.putBoolean(getKey(R.string.pref_units_imperial), useImperialUnits());
+
+ // show waypoints threshold now as a slider
+ int wpThreshold = getWayPointsThreshold();
+ if (wpThreshold < 0) {
+ wpThreshold = 0;
+ } else if (wpThreshold > SHOW_WP_THRESHOLD_MAX) {
+ wpThreshold = SHOW_WP_THRESHOLD_MAX;
+ }
+ e.putInt(getKey(R.string.pref_showwaypointsthreshold), wpThreshold);
+
+ // KEY_MAP_SOURCE must be string, because it is the key for a ListPreference now
+ final int ms = sharedPrefs.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT);
+ e.remove(getKey(R.string.pref_mapsource));
+ e.putString(getKey(R.string.pref_mapsource), String.valueOf(ms));
+
+ // navigation tool ids must be string, because ListPreference uses strings as keys
+ final int dnt1 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id);
+ final int dnt2 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id);
+ e.remove(getKey(R.string.pref_defaultNavigationTool));
+ e.remove(getKey(R.string.pref_defaultNavigationTool2));
+ e.putString(getKey(R.string.pref_defaultNavigationTool), String.valueOf(dnt1));
+ e.putString(getKey(R.string.pref_defaultNavigationTool2), String.valueOf(dnt2));
+
+ // defaults for gpx directories
+ e.putString(getKey(R.string.pref_gpxImportDir), getGpxImportDir());
+ e.putString(getKey(R.string.pref_gpxExportDir), getGpxExportDir());
+
+ e.putInt(getKey(R.string.pref_settingsversion), 2); // mark migrated
+ e.apply();
+ }
+ }
+
+ private static String getKey(final int prefKeyId) {
+ return CgeoApplication.getInstance().getString(prefKeyId);
+ }
+
+ static String getString(final int prefKeyId, final String defaultValue) {
+ return sharedPrefs.getString(getKey(prefKeyId), defaultValue);
+ }
+
+ private static int getInt(final int prefKeyId, final int defaultValue) {
+ return sharedPrefs.getInt(getKey(prefKeyId), defaultValue);
+ }
+
+ private static long getLong(final int prefKeyId, final long defaultValue) {
+ return sharedPrefs.getLong(getKey(prefKeyId), defaultValue);
+ }
+
+ private static boolean getBoolean(final int prefKeyId, final boolean defaultValue) {
+ return sharedPrefs.getBoolean(getKey(prefKeyId), defaultValue);
+ }
+
+ private static float getFloat(final int prefKeyId, final float defaultValue) {
+ return sharedPrefs.getFloat(getKey(prefKeyId), defaultValue);
+ }
+
+ protected static void putString(final int prefKeyId, final String value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putString(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ protected static void putBoolean(final int prefKeyId, final boolean value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putBoolean(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void putInt(final int prefKeyId, final int value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putInt(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void putLong(final int prefKeyId, final long value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putLong(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void putFloat(final int prefKeyId, final float value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putFloat(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void remove(final int prefKeyId) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.remove(getKey(prefKeyId));
+ edit.apply();
+ }
+
+ private static boolean contains(final int prefKeyId) {
+ return sharedPrefs.contains(getKey(prefKeyId));
+ }
+
+ public static boolean hasGCCredentials() {
+ final String preUsername = getString(R.string.pref_username, null);
+ final String prePassword = getString(R.string.pref_password, null);
+
+ return !StringUtils.isBlank(preUsername) && !StringUtils.isBlank(prePassword);
+ }
+
+ /**
+ * Get login and password information of Geocaching.com.
+ *
+ * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
+ */
+ public static ImmutablePair<String, String> getGcCredentials() {
+ return getCredentials(GCConnector.getInstance());
+ }
+
+ /**
+ * Get login and password information.
+ *
+ * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
+ */
+ public static ImmutablePair<String, String> getCredentials(final @NonNull ICredentials connector) {
+ final String username = getString(connector.getUsernamePreferenceKey(), null);
+ final String password = getString(connector.getPasswordPreferenceKey(), null);
+
+ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
+ return new ImmutablePair<>(StringUtils.EMPTY, StringUtils.EMPTY);
+ }
+
+ return new ImmutablePair<>(username, password);
+ }
+
+ public static String getUsername() {
+ return getString(R.string.pref_username, StringUtils.EMPTY);
+ }
+
+ public static boolean isGCConnectorActive() {
+ return getBoolean(R.string.pref_connectorGCActive, true);
+ }
+
+ public static boolean isECConnectorActive() {
+ return getBoolean(R.string.pref_connectorECActive, false);
+ }
+
+ public static boolean isGCPremiumMember() {
+ final String memberStatus = getGCMemberStatus();
+ return StringUtils.equalsIgnoreCase(memberStatus, GCConstants.MEMBER_STATUS_PREMIUM) ||
+ StringUtils.equalsIgnoreCase(memberStatus, GCConstants.MEMBER_STATUS_CHARTER);
+ }
+
+ public static String getGCMemberStatus() {
+ return getString(R.string.pref_memberstatus, "");
+ }
+
+ public static void setGCMemberStatus(final String memberStatus) {
+ if (StringUtils.isBlank(memberStatus)) {
+ remove(R.string.pref_memberstatus);
+ }
+ putString(R.string.pref_memberstatus, memberStatus);
+ }
+
+ public static ImmutablePair<String, String> getTokenPair(final int tokenPublicPrefKey, final int tokenSecretPrefKey) {
+ return new ImmutablePair<>(getString(tokenPublicPrefKey, null), getString(tokenSecretPrefKey, null));
+ }
+
+ public static void setTokens(final int tokenPublicPrefKey, @Nullable final String tokenPublic, final int tokenSecretPrefKey, @Nullable final String tokenSecret) {
+ if (tokenPublic == null) {
+ remove(tokenPublicPrefKey);
+ } else {
+ putString(tokenPublicPrefKey, tokenPublic);
+ }
+ if (tokenSecret == null) {
+ remove(tokenSecretPrefKey);
+ } else {
+ putString(tokenSecretPrefKey, tokenSecret);
+ }
+ }
+
+ public static boolean isOCConnectorActive(final int isActivePrefKeyId) {
+ return getBoolean(isActivePrefKeyId, false);
+ }
+
+ public static boolean hasOCAuthorization(final int tokenPublicPrefKeyId, final int tokenSecretPrefKeyId) {
+ return StringUtils.isNotBlank(getString(tokenPublicPrefKeyId, ""))
+ && StringUtils.isNotBlank(getString(tokenSecretPrefKeyId, ""));
+ }
+
+ public static boolean isGCVoteLogin() {
+ return getGCVoteLogin() != null;
+ }
+
+ public static ImmutablePair<String, String> getGCVoteLogin() {
+ final String username = getString(R.string.pref_username, null);
+ final String password = getString(R.string.pref_pass_vote, null);
+
+ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
+ return null;
+ }
+
+ return new ImmutablePair<>(username, password);
+ }
+
+ @NonNull
+ public static String getSignature() {
+ return StringUtils.defaultString(getString(R.string.pref_signature, StringUtils.EMPTY));
+ }
+
+ public static void setCookieStore(final String cookies) {
+ if (StringUtils.isBlank(cookies)) {
+ // erase cookies
+ remove(R.string.pref_cookiestore);
+ }
+ // save cookies
+ putString(R.string.pref_cookiestore, cookies);
+ }
+
+ public static String getCookieStore() {
+ return getString(R.string.pref_cookiestore, null);
+ }
+
+ public static void setUseGooglePlayServices(final boolean value) {
+ putBoolean(R.string.pref_googleplayservices, value);
+ }
+
+ public static boolean useGooglePlayServices() {
+ // By defaut, enable play services starting from ICS.
+ return CgeoApplication.getInstance().isGooglePlayServicesAvailable() &&
+ getBoolean(R.string.pref_googleplayservices, VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH);
+ }
+
+ public static boolean useLowPowerMode() {
+ return getBoolean(R.string.pref_lowpowermode, false);
+ }
+
+ /**
+ * @param cacheType
+ * The cache type used for future filtering
+ */
+ public static void setCacheType(final CacheType cacheType) {
+ if (cacheType == null) {
+ remove(R.string.pref_cachetype);
+ } else {
+ putString(R.string.pref_cachetype, cacheType.id);
+ }
+ }
+
+ public static int getLastList() {
+ return getInt(R.string.pref_lastusedlist, StoredList.STANDARD_LIST_ID);
+ }
+
+ public static void saveLastList(final int listId) {
+ putInt(R.string.pref_lastusedlist, listId);
+ }
+
+ public static void setWebNameCode(final String name, final String code) {
+ putString(R.string.pref_webDeviceName, name);
+ putString(R.string.pref_webDeviceCode, code);
+ }
+
+ public static MapProvider getMapProvider() {
+ return getMapSource().getMapProvider();
+ }
+
+ public static String getMapFile() {
+ return getString(R.string.pref_mapfile, null);
+ }
+
+ static void setMapFile(final String mapFile) {
+ putString(R.string.pref_mapfile, mapFile);
+ if (mapFile != null) {
+ setMapFileDirectory(new File(mapFile).getParent());
+ }
+ }
+
+ public static String getMapFileDirectory() {
+ final String mapDir = getString(R.string.pref_mapDirectory, null);
+ if (mapDir != null) {
+ return mapDir;
+ }
+ final String mapFile = getMapFile();
+ if (mapFile != null) {
+ return new File(mapFile).getParent();
+ }
+ return null;
+ }
+
+ static void setMapFileDirectory(final String mapFileDirectory) {
+ putString(R.string.pref_mapDirectory, mapFileDirectory);
+ MapsforgeMapProvider.getInstance().updateOfflineMaps();
+ }
+
+ private static boolean isValidMapFile() {
+ return isValidMapFile(getMapFile());
+ }
+
+ public static boolean isValidMapFile(final String mapFileIn) {
+ return MapsforgeMapProvider.isValidMapFile(mapFileIn);
+ }
+
+ public static boolean isScaleMapsforgeText() {
+ return getBoolean(R.string.pref_mapsforge_scale_text, true);
+ }
+
+ public static CoordInputFormatEnum getCoordInputFormat() {
+ return CoordInputFormatEnum.fromInt(getInt(R.string.pref_coordinputformat, CoordInputFormatEnum.DEFAULT_INT_VALUE));
+ }
+
+ public static void setCoordInputFormat(final CoordInputFormatEnum format) {
+ putInt(R.string.pref_coordinputformat, format.ordinal());
+ }
+
+ public static boolean getLogOffline() {
+ return getBoolean(R.string.pref_log_offline, false);
+ }
+
+ public static boolean getChooseList() {
+ return getBoolean(R.string.pref_choose_list, false);
+ }
+
+ public static boolean getLoadDirImg() {
+ return !isGCPremiumMember() && getBoolean(R.string.pref_loaddirectionimg, true);
+ }
+
+ public static void setGcCustomDate(final String format) {
+ putString(R.string.pref_gccustomdate, format);
+ }
+
+ /**
+ * @return User selected date format on GC.com
+ */
+ public static String getGcCustomDate() {
+ // We might have some users whose stored value is null, which is invalid. In this case, we use the default.
+ return StringUtils.defaultString(getString(R.string.pref_gccustomdate, GCConstants.DEFAULT_GC_DATE),
+ GCConstants.DEFAULT_GC_DATE);
+ }
+
+ public static boolean isExcludeMyCaches() {
+ return getBoolean(R.string.pref_excludemine, false);
+ }
+
+ public static boolean useEnglish() {
+ return getBoolean(R.string.pref_useenglish, false);
+ }
+
+ public static boolean isShowAddress() {
+ return getBoolean(R.string.pref_showaddress, true);
+ }
+
+ public static boolean isShowCaptcha() {
+ return !isGCPremiumMember() && getBoolean(R.string.pref_showcaptcha, false);
+ }
+
+ public static boolean isExcludeDisabledCaches() {
+ return getBoolean(R.string.pref_excludedisabled, false);
+ }
+
+ public static boolean isStoreOfflineMaps() {
+ return getBoolean(R.string.pref_offlinemaps, true);
+ }
+
+ public static boolean isStoreOfflineWpMaps() {
+ return getBoolean(R.string.pref_offlinewpmaps, false);
+ }
+
+ public static boolean isStoreLogImages() {
+ return getBoolean(R.string.pref_logimages, false);
+ }
+
+ public static boolean isAutoLoadDescription() {
+ return getBoolean(R.string.pref_autoloaddesc, true);
+ }
+
+ public static boolean isRatingWanted() {
+ return getBoolean(R.string.pref_ratingwanted, true);
+ }
+
+ public static boolean isGeokretyConnectorActive() {
+ return getBoolean(R.string.pref_connectorGeokretyActive, false);
+ }
+
+ public static boolean isGeokretyCacheActive() {
+ return getBoolean(R.string.pref_geokrety_cache, true);
+ }
+
+ static boolean hasGeokretyAuthorization() {
+ return StringUtils.isNotBlank(getGeokretySecId());
+ }
+
+ public static String getGeokretySecId() {
+ return getString(R.string.pref_fakekey_geokrety_authorization, null);
+ }
+
+ public static void setGeokretySecId(final String secid) {
+ putString(R.string.pref_fakekey_geokrety_authorization, secid);
+ }
+
+ public static boolean isRegisteredForGeokretyLogging() {
+ return getGeokretySecId() != null;
+ }
+
+ /**
+ * Retrieve showed popup counter for warning about logging Trackable recommend Geocode
+ *
+ * @return number of times the popup has appeared
+ */
+ public static int getLogTrackableWithoutGeocodeShowCount() {
+ return getInt(R.string.pref_logtrackablewithoutgeocodeshowcount, 0);
+ }
+
+ /**
+ * Store showed popup counter for warning about logging Trackable recommend Geocode
+ *
+ * @param showCount the count to save
+ */
+ public static void setLogTrackableWithoutGeocodeShowCount(final int showCount) {
+ putInt(R.string.pref_logtrackablewithoutgeocodeshowcount, showCount);
+ }
+
+ public static boolean isFriendLogsWanted() {
+ if (!hasGCCredentials()) {
+ // don't show a friends log if the user is anonymous
+ return false;
+ }
+ return getBoolean(R.string.pref_friendlogswanted, true);
+ }
+
+ public static boolean isLiveList() {
+ return getBoolean(R.string.pref_livelist, true);
+ }
+
+ public static boolean isTrackableAutoVisit() {
+ return getBoolean(R.string.pref_trackautovisit, false);
+ }
+
+ public static boolean isAutoInsertSignature() {
+ return getBoolean(R.string.pref_sigautoinsert, false);
+ }
+
+ static void setUseImperialUnits(final boolean useImperialUnits) {
+ putBoolean(R.string.pref_units_imperial, useImperialUnits);
+ }
+
+ public static boolean useImperialUnits() {
+ return getBoolean(R.string.pref_units_imperial, useImperialUnitsByDefault());
+ }
+
+ private static boolean useImperialUnitsByDefault() {
+ final String countryCode = Locale.getDefault().getCountry();
+ return "US".equals(countryCode) // USA
+ || "LR".equals(countryCode) // Liberia
+ || "MM".equals(countryCode); // Burma
+ }
+
+ public static boolean isLiveMap() {
+ return getBoolean(R.string.pref_maplive, true);
+ }
+
+ public static void setLiveMap(final boolean live) {
+ putBoolean(R.string.pref_maplive, live);
+ }
+
+ public static boolean isMapTrail() {
+ return getBoolean(R.string.pref_maptrail, false);
+ }
+
+ public static void setMapTrail(final boolean showTrail) {
+ putBoolean(R.string.pref_maptrail, showTrail);
+ }
+
+ /**
+ * whether to show a direction line on the map
+ */
+ public static boolean isMapDirection() {
+ return getBoolean(R.string.pref_map_direction, true);
+ }
+
+ public static void setMapDirection(final boolean showDirection) {
+ putBoolean(R.string.pref_map_direction, showDirection);
+ }
+
+ /**
+ * Get last used zoom of the internal map. Differentiate between two use cases for a map of multiple caches (e.g.
+ * live map) and the map of a single cache (which is often zoomed in more deep).
+ */
+ public static int getMapZoom(final MapMode mapMode) {
+ if (mapMode == MapMode.SINGLE || mapMode == MapMode.COORDS) {
+ return getCacheZoom();
+ }
+ return getMapZoom();
+ }
+
+ public static void setMapZoom(final MapMode mapMode, final int zoomLevel) {
+ if (mapMode == MapMode.SINGLE || mapMode == MapMode.COORDS) {
+ setCacheZoom(zoomLevel);
+ }
+ else {
+ setMapZoom(zoomLevel);
+ }
+ }
+
+ /**
+ * @return zoom used for the (live) map
+ */
+ private static int getMapZoom() {
+ return Math.max(getInt(R.string.pref_lastmapzoom, 14), INITIAL_MAP_ZOOM_LIMIT);
+ }
+
+ private static void setMapZoom(final int mapZoomLevel) {
+ putInt(R.string.pref_lastmapzoom, mapZoomLevel);
+ }
+
+ /**
+ * @return zoom used for the map of a single cache
+ */
+ private static int getCacheZoom() {
+ return Math.max(getInt(R.string.pref_cache_zoom, 14), INITIAL_MAP_ZOOM_LIMIT);
+ }
+
+ private static void setCacheZoom(final int zoomLevel) {
+ putInt(R.string.pref_cache_zoom, zoomLevel);
+ }
+
+ public static GeoPointImpl getMapCenter() {
+ return getMapProvider().getMapItemFactory()
+ .getGeoPointBase(new Geopoint(getInt(R.string.pref_lastmaplat, 0) / 1e6,
+ getInt(R.string.pref_lastmaplon, 0) / 1e6));
+ }
+
+ public static void setMapCenter(final GeoPointImpl mapViewCenter) {
+ putInt(R.string.pref_lastmaplat, mapViewCenter.getLatitudeE6());
+ putInt(R.string.pref_lastmaplon, mapViewCenter.getLongitudeE6());
+ }
+
+ @NonNull
+ public static synchronized MapSource getMapSource() {
+ if (mapSource != null) {
+ return mapSource;
+ }
+ final int id = getConvertedMapId();
+ mapSource = MapProviderFactory.getMapSource(id);
+ if (mapSource != null) {
+ // don't use offline maps if the map file is not valid
+ if (!(mapSource instanceof OfflineMapSource) || isValidMapFile()) {
+ return mapSource;
+ }
+ }
+ // fallback to first available map
+ return MapProviderFactory.getDefaultSource();
+ }
+
+ private final static int GOOGLEMAP_BASEID = 30;
+ private final static int MAP = 1;
+ private final static int SATELLITE = 2;
+
+ private final static int MFMAP_BASEID = 40;
+ private final static int MAPNIK = 1;
+ private final static int CYCLEMAP = 3;
+ private final static int OFFLINE = 4;
+ private static final int HISTORY_SIZE = 10;
+
+ /**
+ * Convert old preference ids for maps (based on constant values) into new hash based ids.
+ */
+ private static int getConvertedMapId() {
+ final int id = Integer.parseInt(getString(R.string.pref_mapsource,
+ String.valueOf(MAP_SOURCE_DEFAULT)));
+ switch (id) {
+ case GOOGLEMAP_BASEID + MAP:
+ return GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
+ case GOOGLEMAP_BASEID + SATELLITE:
+ return GoogleMapProvider.GOOGLE_SATELLITE_ID.hashCode();
+ case MFMAP_BASEID + MAPNIK:
+ return MapsforgeMapProvider.MAPSFORGE_MAPNIK_ID.hashCode();
+ case MFMAP_BASEID + CYCLEMAP:
+ return MapsforgeMapProvider.MAPSFORGE_CYCLEMAP_ID.hashCode();
+ case MFMAP_BASEID + OFFLINE: {
+ final String mapFile = getMapFile();
+ if (StringUtils.isNotEmpty(mapFile)) {
+ return mapFile.hashCode();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return id;
+ }
+
+ public static synchronized void setMapSource(final MapSource newMapSource) {
+ putString(R.string.pref_mapsource, String.valueOf(newMapSource.getNumericalId()));
+ if (newMapSource instanceof OfflineMapSource) {
+ setMapFile(((OfflineMapSource) newMapSource).getFileName());
+ }
+ // cache the value
+ mapSource = newMapSource;
+ }
+
+ public static void setAnyCoordinates(final Geopoint coords) {
+ if (null != coords) {
+ putFloat(R.string.pref_anylatitude, (float) coords.getLatitude());
+ putFloat(R.string.pref_anylongitude, (float) coords.getLongitude());
+ } else {
+ remove(R.string.pref_anylatitude);
+ remove(R.string.pref_anylongitude);
+ }
+ }
+
+ public static Geopoint getAnyCoordinates() {
+ if (contains(R.string.pref_anylatitude) && contains(R.string.pref_anylongitude)) {
+ final float lat = getFloat(R.string.pref_anylatitude, 0);
+ final float lon = getFloat(R.string.pref_anylongitude, 0);
+ return new Geopoint(lat, lon);
+ }
+ return null;
+ }
+
+ public static boolean isUseCompass() {
+ return useCompass;
+ }
+
+ public static void setUseCompass(final boolean value) {
+ useCompass = value;
+ }
+
+ public static boolean isLightSkin() {
+ return getBoolean(R.string.pref_skin, false);
+ }
+
+ @NonNull
+ public static String getTwitterKeyConsumerPublic() {
+ return TWITTER_KEY_CONSUMER_PUBLIC;
+ }
+
+ @NonNull
+ public static String getTwitterKeyConsumerSecret() {
+ return TWITTER_KEY_CONSUMER_SECRET;
+ }
+
+ public static String getWebDeviceCode() {
+ return getString(R.string.pref_webDeviceCode, null);
+ }
+
+ public static boolean isRegisteredForSend2cgeo() {
+ return getWebDeviceCode() != null;
+ }
+
+ static String getWebDeviceName() {
+ return getString(R.string.pref_webDeviceName, Build.MODEL);
+ }
+
+ /**
+ * @return The cache type used for filtering or ALL if no filter is active.
+ * Returns never null
+ */
+ @NonNull
+ public static CacheType getCacheType() {
+ return CacheType.getById(getString(R.string.pref_cachetype, CacheType.ALL.id));
+ }
+
+ /**
+ * The Threshold for the showing of child waypoints
+ */
+ public static int getWayPointsThreshold() {
+ return getInt(R.string.pref_showwaypointsthreshold, SHOW_WP_THRESHOLD_DEFAULT);
+ }
+
+ static void setShowWaypointsThreshold(final int threshold) {
+ putInt(R.string.pref_showwaypointsthreshold, threshold);
+ }
+
+ public static boolean isUseTwitter() {
+ return getBoolean(R.string.pref_twitter, false);
+ }
+
+ private static void setUseTwitter(final boolean useTwitter) {
+ putBoolean(R.string.pref_twitter, useTwitter);
+ }
+
+ public static boolean isTwitterLoginValid() {
+ return !StringUtils.isBlank(getTokenPublic())
+ && !StringUtils.isBlank(getTokenSecret());
+ }
+
+ public static String getTokenPublic() {
+ return getString(R.string.pref_twitter_token_public, null);
+ }
+
+ public static String getTokenSecret() {
+ return getString(R.string.pref_twitter_token_secret, null);
+
+ }
+
+ static boolean hasTwitterAuthorization() {
+ return StringUtils.isNotBlank(getTokenPublic())
+ && StringUtils.isNotBlank(getTokenSecret());
+ }
+
+ public static void setTwitterTokens(@Nullable final String tokenPublic,
+ @Nullable final String tokenSecret, final boolean enableTwitter) {
+ putString(R.string.pref_twitter_token_public, tokenPublic);
+ putString(R.string.pref_twitter_token_secret, tokenSecret);
+ if (tokenPublic != null) {
+ remove(R.string.pref_temp_twitter_token_public);
+ remove(R.string.pref_temp_twitter_token_secret);
+ }
+ setUseTwitter(enableTwitter);
+ }
+
+ public static void setTwitterTempTokens(@Nullable final String tokenPublic,
+ @Nullable final String tokenSecret) {
+ putString(R.string.pref_temp_twitter_token_public, tokenPublic);
+ putString(R.string.pref_temp_twitter_token_secret, tokenSecret);
+ }
+
+ public static ImmutablePair<String, String> getTempToken() {
+ final String tokenPublic = getString(R.string.pref_temp_twitter_token_public, null);
+ final String tokenSecret = getString(R.string.pref_temp_twitter_token_secret, null);
+ return new ImmutablePair<>(tokenPublic, tokenSecret);
+ }
+
+ public static int getVersion() {
+ return getInt(R.string.pref_version, 0);
+ }
+
+ public static void setVersion(final int version) {
+ putInt(R.string.pref_version, version);
+ }
+
+ public static boolean isOpenLastDetailsPage() {
+ return getBoolean(R.string.pref_opendetailslastpage, false);
+ }
+
+ public static int getLastDetailsPage() {
+ return getInt(R.string.pref_lastdetailspage, 1);
+ }
+
+ public static void setLastDetailsPage(final int index) {
+ putInt(R.string.pref_lastdetailspage, index);
+ }
+
+ public static int getDefaultNavigationTool() {
+ return Integer.parseInt(getString(
+ R.string.pref_defaultNavigationTool,
+ String.valueOf(NavigationAppsEnum.COMPASS.id)));
+ }
+
+ public static int getDefaultNavigationTool2() {
+ return Integer.parseInt(getString(
+ R.string.pref_defaultNavigationTool2,
+ String.valueOf(NavigationAppsEnum.INTERNAL_MAP.id)));
+ }
+
+ public static LivemapStrategy getLiveMapStrategy() {
+ return LivemapStrategy.getById(getInt(R.string.pref_livemapstrategy, LivemapStrategy.AUTO.id));
+ }
+
+ public static void setLiveMapStrategy(final LivemapStrategy strategy) {
+ putInt(R.string.pref_livemapstrategy, strategy.id);
+ }
+
+ public static boolean isDebug() {
+ return Log.isDebug();
+ }
+
+ public static int getLiveMapHintShowCount() {
+ return getInt(R.string.pref_livemaphintshowcount, 0);
+ }
+
+ public static void setLiveMapHintShowCount(final int showCount) {
+ putInt(R.string.pref_livemaphintshowcount, showCount);
+ }
+
+ public static boolean isDbOnSDCard() {
+ return getBoolean(R.string.pref_dbonsdcard, false);
+ }
+
+ public static void setDbOnSDCard(final boolean dbOnSDCard) {
+ putBoolean(R.string.pref_dbonsdcard, dbOnSDCard);
+ }
+
+ public static String getGpxExportDir() {
+ return getString(R.string.pref_gpxExportDir,
+ Environment.getExternalStorageDirectory().getPath() + "/gpx");
+ }
+
+ public static String getGpxImportDir() {
+ return getString(R.string.pref_gpxImportDir,
+ Environment.getExternalStorageDirectory().getPath() + "/gpx");
+ }
+
+ public static boolean getShareAfterExport() {
+ return getBoolean(R.string.pref_shareafterexport, true);
+ }
+
+ public static void setShareAfterExport(final boolean shareAfterExport) {
+ putBoolean(R.string.pref_shareafterexport, shareAfterExport);
+ }
+
+ public static int getTrackableAction() {
+ return getInt(R.string.pref_trackableaction, LogTypeTrackable.RETRIEVED_IT.id);
+ }
+
+ public static void setTrackableAction(final int trackableAction) {
+ putInt(R.string.pref_trackableaction, trackableAction);
+ }
+
+ private static String getCustomRenderThemeBaseFolder() {
+ return getString(R.string.pref_renderthemepath, "");
+ }
+
+ public static String getCustomRenderThemeFilePath() {
+ return getString(R.string.pref_renderthemefile, "");
+ }
+
+ public static void setCustomRenderThemeFile(final String customRenderThemeFile) {
+ putString(R.string.pref_renderthemefile, customRenderThemeFile);
+ }
+
+ public static File[] getMapThemeFiles() {
+ final File directory = new File(getCustomRenderThemeBaseFolder());
+ final List<File> result = new ArrayList<>();
+ FileUtils.listDir(result, directory, new ExtensionsBasedFileSelector(new String[] { "xml" }), null);
+
+ return result.toArray(new File[result.size()]);
+ }
+
+ private static class ExtensionsBasedFileSelector implements FileSelector {
+ private final String[] extensions;
+ public ExtensionsBasedFileSelector(final String[] extensions) {
+ this.extensions = extensions;
+ }
+ @Override
+ public boolean isSelected(final File file) {
+ final String filename = file.getName();
+ for (final String ext : extensions) {
+ if (StringUtils.endsWithIgnoreCase(filename, ext)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ @Override
+ public boolean shouldEnd() {
+ return false;
+ }
+ }
+
+ /**
+ * @return true if plain text log wanted
+ */
+ public static boolean getPlainLogs() {
+ return getBoolean(R.string.pref_plainLogs, false);
+ }
+
+ /**
+ * Force set the plain text log preference
+ *
+ * @param plainLogs
+ * wanted or not
+ */
+ public static void setPlainLogs(final boolean plainLogs) {
+ putBoolean(R.string.pref_plainLogs, plainLogs);
+ }
+
+ public static boolean getUseNativeUa() {
+ return getBoolean(R.string.pref_nativeUa, false);
+ }
+
+ @NonNull
+ public static String getCacheTwitterMessage() {
+ return StringUtils.defaultString(getString(R.string.pref_twitter_cache_message, "I found [NAME] ([URL])."));
+ }
+
+ @NonNull
+ public static String getTrackableTwitterMessage() {
+ return StringUtils.defaultString(getString(R.string.pref_twitter_trackable_message, "I touched [NAME] ([URL])."));
+ }
+
+ public static int getLogImageScale() {
+ return getInt(R.string.pref_logImageScale, -1);
+ }
+
+ public static void setLogImageScale(final int scale) {
+ putInt(R.string.pref_logImageScale, scale);
+ }
+
+ public static void setExcludeMine(final boolean exclude) {
+ putBoolean(R.string.pref_excludemine, exclude);
+ }
+
+ static void setLogin(final String username, final String password) {
+ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
+ // erase username and password
+ remove(R.string.pref_username);
+ remove(R.string.pref_password);
+ return;
+ }
+ // save username and password
+ putString(R.string.pref_username, username);
+ putString(R.string.pref_password, password);
+ }
+
+ public static long getFieldnoteExportDate() {
+ return getLong(R.string.pref_fieldNoteExportDate, 0);
+ }
+
+ /**
+ * Remember date of last field note export.
+ */
+ public static void setFieldnoteExportDate(final long date) {
+ putLong(R.string.pref_fieldNoteExportDate, date);
+ }
+
+ public static boolean isUseNavigationApp(final NavigationAppsEnum navApp) {
+ return getBoolean(navApp.preferenceKey, true);
+ }
+
+ /**
+ * Remember the state of the "Upload" checkbox in the field notes export dialog.
+ */
+ public static void setFieldNoteExportUpload(final boolean upload) {
+ putBoolean(R.string.pref_fieldNoteExportUpload, upload);
+ }
+
+ public static boolean getFieldNoteExportUpload() {
+ return getBoolean(R.string.pref_fieldNoteExportUpload, true);
+ }
+
+ /**
+ * Remember the state of the "Only new" checkbox in the field notes export dialog.
+ */
+ public static void setFieldNoteExportOnlyNew(final boolean onlyNew) {
+ putBoolean(R.string.pref_fieldNoteExportOnlyNew, onlyNew);
+ }
+
+ public static boolean getFieldNoteExportOnlyNew() {
+ return getBoolean(R.string.pref_fieldNoteExportOnlyNew, false);
+ }
+
+ public static String getECIconSet() {
+ return getString(R.string.pref_ec_icons, "1");
+ }
+
+ /* Store last checksum of changelog for changelog display */
+ public static long getLastChangelogChecksum() {
+ return getLong(R.string.pref_changelog_last_checksum, 0);
+ }
+
+ public static void setLastChangelogChecksum(final long checksum) {
+ putLong(R.string.pref_changelog_last_checksum, checksum);
+ }
+
+ public static List<String> getLastOpenedCaches() {
+ final List<String> history = Arrays.asList(StringUtils.split(getString(R.string.pref_caches_history, StringUtils.EMPTY), HISTORY_SEPARATOR));
+ return history.subList(0, Math.min(HISTORY_SIZE, history.size()));
+ }
+
+ public static void addCacheToHistory(@NonNull final String geocode) {
+ final List<String> history = new ArrayList<>(getLastOpenedCaches());
+ // bring entry to front, if it already existed
+ history.remove(geocode);
+ history.add(0, geocode);
+ putString(R.string.pref_caches_history, StringUtils.join(history, HISTORY_SEPARATOR));
+ }
+
+ public static boolean useHardwareAcceleration() {
+ return getBoolean(R.string.pref_hardware_acceleration, !HW_ACCEL_DISABLED_BY_DEFAULT);
+ }
+
+ static void setUseHardwareAcceleration(final boolean useHardwareAcceleration) {
+ putBoolean(R.string.pref_hardware_acceleration, useHardwareAcceleration);
+ }
+
+ public static String getLastCacheLog() {
+ return getString(R.string.pref_last_cache_log, StringUtils.EMPTY);
+ }
+
+ public static void setLastCacheLog(final String log) {
+ putString(R.string.pref_last_cache_log, log);
+ }
+
+ public static String getLastTrackableLog() {
+ return getString(R.string.pref_last_trackable_log, StringUtils.EMPTY);
+ }
+
+ public static void setLastTrackableLog(final String log) {
+ putString(R.string.pref_last_trackable_log, log);
+ }
+
+ @Nullable
+ public static String getHomeLocation() {
+ return getString(R.string.pref_home_location, null);
+ }
+
+ public static void setHomeLocation(@NonNull final String homeLocation) {
+ putString(R.string.pref_home_location, homeLocation);
+ }
+
+ public static void setForceOrientationSensor(final boolean forceOrientationSensor) {
+ putBoolean(R.string.pref_force_orientation_sensor, forceOrientationSensor);
+ }
+
+ public static boolean useOrientationSensor(final Context context) {
+ return OrientationProvider.hasOrientationSensor(context) && (getBoolean(R.string.pref_force_orientation_sensor, false) || !RotationProvider.hasRotationSensor(context));
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/CryptUtils.java b/main/src/cgeo/geocaching/utils/CryptUtils.java
index 4aec509..6ef8c25 100644
--- a/main/src/cgeo/geocaching/utils/CryptUtils.java
+++ b/main/src/cgeo/geocaching/utils/CryptUtils.java
@@ -1,153 +1,156 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.lang3.CharEncoding;
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-
-import java.io.UnsupportedEncodingException;
-import java.math.BigInteger;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-public final class CryptUtils {
-
- private CryptUtils() {
- // utility class
- }
-
- private static final byte[] EMPTY = {};
- private static final char[] BASE64MAP1 = new char[64];
- private static final byte[] BASE64MAP2 = new byte[128];
-
- static {
- int i = 0;
- for (char c = 'A'; c <= 'Z'; c++) {
- BASE64MAP1[i++] = c;
- }
- for (char c = 'a'; c <= 'z'; c++) {
- BASE64MAP1[i++] = c;
- }
- for (char c = '0'; c <= '9'; c++) {
- BASE64MAP1[i++] = c;
- }
- BASE64MAP1[i++] = '+';
- BASE64MAP1[i++] = '/';
-
- for (i = 0; i < BASE64MAP2.length; i++) {
- BASE64MAP2[i] = -1;
- }
- for (i = 0; i < 64; i++) {
- BASE64MAP2[BASE64MAP1[i]] = (byte) i;
- }
- }
-
- private static class Rot13Encryption {
- private boolean plaintext = false;
-
- char getNextEncryptedCharacter(final char c) {
- int result = c;
- if (result == '[') {
- plaintext = true;
- } else if (result == ']') {
- plaintext = false;
- } else if (!plaintext) {
- final int capitalized = result & 32;
- result &= ~capitalized;
- result = ((result >= 'A') && (result <= 'Z') ? ((result - 'A' + 13) % 26 + 'A') : result)
- | capitalized;
- }
- return (char) result;
- }
- }
-
- @NonNull
- public static String rot13(final String text) {
- if (text == null) {
- return StringUtils.EMPTY;
- }
- final StringBuilder result = new StringBuilder();
- final Rot13Encryption rot13 = new Rot13Encryption();
-
- final int length = text.length();
- for (int index = 0; index < length; index++) {
- final char c = text.charAt(index);
- result.append(rot13.getNextEncryptedCharacter(c));
- }
- return result.toString();
- }
-
- public static String md5(final String text) {
- try {
- final MessageDigest digest = MessageDigest.getInstance("MD5");
- digest.update(text.getBytes(CharEncoding.UTF_8), 0, text.length());
- return new BigInteger(1, digest.digest()).toString(16);
- } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
- Log.e("CryptUtils.md5", e);
- }
-
- return StringUtils.EMPTY;
- }
-
- public static byte[] hashHmac(final String text, final String salt) {
-
- try {
- final SecretKeySpec secretKeySpec = new SecretKeySpec(salt.getBytes(CharEncoding.UTF_8), "HmacSHA1");
- final Mac mac = Mac.getInstance("HmacSHA1");
- mac.init(secretKeySpec);
- return mac.doFinal(text.getBytes(CharEncoding.UTF_8));
- } catch (GeneralSecurityException | UnsupportedEncodingException e) {
- Log.e("CryptUtils.hashHmac", e);
- return EMPTY;
- }
- }
-
- public static CharSequence rot13(final Spannable span) {
- // I needed to re-implement the rot13(String) encryption here because we must work on
- // a SpannableStringBuilder instead of the pure text and we must replace each character inline.
- // Otherwise we loose all the images, colors and so on...
- final SpannableStringBuilder buffer = new SpannableStringBuilder(span);
- final Rot13Encryption rot13 = new Rot13Encryption();
-
- final int length = span.length();
- for (int index = 0; index < length; index++) {
- final char c = span.charAt(index);
- buffer.replace(index, index + 1, String.valueOf(rot13.getNextEncryptedCharacter(c)));
- }
- return buffer;
- }
-
- public static String base64Encode(final byte[] in) {
- final int iLen = in.length;
- final int oDataLen = (iLen * 4 + 2) / 3; // output length without padding
- final int oLen = ((iLen + 2) / 3) * 4; // output length including padding
- final char[] out = new char[oLen];
- int ip = 0;
- int op = 0;
-
- while (ip < iLen) {
- final int i0 = in[ip++] & 0xff;
- final int i1 = ip < iLen ? in[ip++] & 0xff : 0;
- final int i2 = ip < iLen ? in[ip++] & 0xff : 0;
- final int o0 = i0 >>> 2;
- final int o1 = ((i0 & 3) << 4) | (i1 >>> 4);
- final int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
- final int o3 = i2 & 0x3F;
- out[op++] = BASE64MAP1[o0];
- out[op++] = BASE64MAP1[o1];
- out[op] = op < oDataLen ? BASE64MAP1[o2] : '=';
- op++;
- out[op] = op < oDataLen ? BASE64MAP1[o3] : '=';
- op++;
- }
-
- return new String(out);
- }
-
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.lang3.CharEncoding;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public final class CryptUtils {
+
+ private CryptUtils() {
+ // utility class
+ }
+
+ private static final byte[] EMPTY = {};
+ private static final char[] BASE64MAP1 = new char[64];
+ private static final byte[] BASE64MAP2 = new byte[128];
+
+ static {
+ int i = 0;
+ for (char c = 'A'; c <= 'Z'; c++) {
+ BASE64MAP1[i++] = c;
+ }
+ for (char c = 'a'; c <= 'z'; c++) {
+ BASE64MAP1[i++] = c;
+ }
+ for (char c = '0'; c <= '9'; c++) {
+ BASE64MAP1[i++] = c;
+ }
+ BASE64MAP1[i++] = '+';
+ BASE64MAP1[i++] = '/';
+
+ for (i = 0; i < BASE64MAP2.length; i++) {
+ BASE64MAP2[i] = -1;
+ }
+ for (i = 0; i < 64; i++) {
+ BASE64MAP2[BASE64MAP1[i]] = (byte) i;
+ }
+ }
+
+ private static class Rot13Encryption {
+ private boolean plaintext = false;
+
+ char getNextEncryptedCharacter(final char c) {
+ int result = c;
+ if (result == '[') {
+ plaintext = true;
+ } else if (result == ']') {
+ plaintext = false;
+ } else if (!plaintext) {
+ final int capitalized = result & 32;
+ result &= ~capitalized;
+ result = ((result >= 'A') && (result <= 'Z') ? ((result - 'A' + 13) % 26 + 'A') : result)
+ | capitalized;
+ }
+ return (char) result;
+ }
+ }
+
+ @NonNull
+ public static String rot13(final String text) {
+ if (text == null) {
+ return StringUtils.EMPTY;
+ }
+ final StringBuilder result = new StringBuilder();
+ final Rot13Encryption rot13 = new Rot13Encryption();
+
+ final int length = text.length();
+ for (int index = 0; index < length; index++) {
+ final char c = text.charAt(index);
+ result.append(rot13.getNextEncryptedCharacter(c));
+ }
+ return result.toString();
+ }
+
+ @NonNull
+ public static String md5(final String text) {
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("MD5");
+ digest.update(text.getBytes(CharEncoding.UTF_8), 0, text.length());
+ return new BigInteger(1, digest.digest()).toString(16);
+ } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ Log.e("CryptUtils.md5", e);
+ }
+
+ return StringUtils.EMPTY;
+ }
+
+ @NonNull
+ public static byte[] hashHmac(final String text, final String salt) {
+ try {
+ final SecretKeySpec secretKeySpec = new SecretKeySpec(salt.getBytes(CharEncoding.UTF_8), "HmacSHA1");
+ final Mac mac = Mac.getInstance("HmacSHA1");
+ mac.init(secretKeySpec);
+ return mac.doFinal(text.getBytes(CharEncoding.UTF_8));
+ } catch (GeneralSecurityException | UnsupportedEncodingException e) {
+ Log.e("CryptUtils.hashHmac", e);
+ return EMPTY;
+ }
+ }
+
+ @NonNull
+ public static CharSequence rot13(final Spannable span) {
+ // I needed to re-implement the rot13(String) encryption here because we must work on
+ // a SpannableStringBuilder instead of the pure text and we must replace each character inline.
+ // Otherwise we loose all the images, colors and so on...
+ final SpannableStringBuilder buffer = new SpannableStringBuilder(span);
+ final Rot13Encryption rot13 = new Rot13Encryption();
+
+ final int length = span.length();
+ for (int index = 0; index < length; index++) {
+ final char c = span.charAt(index);
+ buffer.replace(index, index + 1, String.valueOf(rot13.getNextEncryptedCharacter(c)));
+ }
+ return buffer;
+ }
+
+ @NonNull
+ public static String base64Encode(final byte[] in) {
+ final int iLen = in.length;
+ final int oDataLen = (iLen * 4 + 2) / 3; // output length without padding
+ final int oLen = ((iLen + 2) / 3) * 4; // output length including padding
+ final char[] out = new char[oLen];
+ int ip = 0;
+ int op = 0;
+
+ while (ip < iLen) {
+ final int i0 = in[ip++] & 0xff;
+ final int i1 = ip < iLen ? in[ip++] & 0xff : 0;
+ final int i2 = ip < iLen ? in[ip++] & 0xff : 0;
+ final int o0 = i0 >>> 2;
+ final int o1 = ((i0 & 3) << 4) | (i1 >>> 4);
+ final int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
+ final int o3 = i2 & 0x3F;
+ out[op++] = BASE64MAP1[o0];
+ out[op++] = BASE64MAP1[o1];
+ out[op] = op < oDataLen ? BASE64MAP1[o2] : '=';
+ op++;
+ out[op] = op < oDataLen ? BASE64MAP1[o3] : '=';
+ op++;
+ }
+
+ return new String(out);
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/utils/EnvironmentUtils.java b/main/src/cgeo/geocaching/utils/EnvironmentUtils.java
index 90b4c5d..680cc46 100644
--- a/main/src/cgeo/geocaching/utils/EnvironmentUtils.java
+++ b/main/src/cgeo/geocaching/utils/EnvironmentUtils.java
@@ -1,28 +1,30 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.lang3.StringUtils;
-
-import android.os.Environment;
-
-public class EnvironmentUtils {
- private EnvironmentUtils() {
- // utility class
- }
-
- /**
- * Same as {@link Environment#getExternalStorageState()} but more stable. We have seen null pointers here, probably
- * when there are issues in the underlying mount.
- */
- public static String getExternalStorageState() {
- try {
- return Environment.getExternalStorageState();
- } catch (final NullPointerException e) {
- Log.w("Could not get external storage state", e);
- }
- return StringUtils.EMPTY;
- }
-
- public static boolean isExternalStorageAvailable() {
- return EnvironmentUtils.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
- }
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.os.Environment;
+
+public class EnvironmentUtils {
+ private EnvironmentUtils() {
+ // utility class
+ }
+
+ /**
+ * Same as {@link Environment#getExternalStorageState()} but more stable. We have seen null pointers here, probably
+ * when there are issues in the underlying mount.
+ */
+ @NonNull
+ public static String getExternalStorageState() {
+ try {
+ return Environment.getExternalStorageState();
+ } catch (final NullPointerException e) {
+ Log.w("Could not get external storage state", e);
+ }
+ return StringUtils.EMPTY;
+ }
+
+ public static boolean isExternalStorageAvailable() {
+ return EnvironmentUtils.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/FileUtils.java b/main/src/cgeo/geocaching/utils/FileUtils.java
index cae94e4..d26b416 100644
--- a/main/src/cgeo/geocaching/utils/FileUtils.java
+++ b/main/src/cgeo/geocaching/utils/FileUtils.java
@@ -1,212 +1,215 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.CharEncoding;
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.os.Handler;
-import android.os.Message;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.List;
-
-/**
- * Utility class for files
- *
- */
-public final class FileUtils {
-
- private static final int MAX_DIRECTORY_SCAN_DEPTH = 30;
- private static final String FILE_PROTOCOL = "file://";
-
- private FileUtils() {
- // utility class
- }
-
- public static void listDir(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler) {
- listDirInternally(result, directory, chooser, feedBackHandler, 0);
- }
-
- private static void listDirInternally(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler, final int depths) {
- if (directory == null || !directory.isDirectory() || !directory.canRead()
- || result == null
- || chooser == null) {
- return;
- }
-
- final File[] files = directory.listFiles();
-
- if (ArrayUtils.isNotEmpty(files)) {
- for (final File file : files) {
- if (chooser.shouldEnd()) {
- return;
- }
- if (!file.canRead()) {
- continue;
- }
- String name = file.getName();
- if (file.isFile()) {
- if (chooser.isSelected(file)) {
- result.add(file); // add file to list
- }
- } else if (file.isDirectory()) {
- if (name.charAt(0) == '.') {
- continue; // skip hidden directories
- }
- if (name.length() > 16) {
- name = name.substring(0, 14) + '…';
- }
- if (feedBackHandler != null) {
- feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name));
- }
-
- if (depths < MAX_DIRECTORY_SCAN_DEPTH) {
- listDirInternally(result, file, chooser, feedBackHandler, depths + 1); // go deeper
- }
- }
- }
- }
- }
-
- public static boolean deleteDirectory(@NonNull final File dir) {
- final File[] files = dir.listFiles();
-
- // Although we are called on an existing directory, it might have been removed concurrently
- // in the meantime, for example by the user or by another cleanup task.
- if (files != null) {
- for (final File file : files) {
- if (file.isDirectory()) {
- deleteDirectory(file);
- } else {
- delete(file);
- }
- }
- }
-
- return delete(dir);
- }
-
- public interface FileSelector {
- boolean isSelected(File file);
- boolean shouldEnd();
- }
-
- /**
- * Create a unique non existing file named like the given file name. If a file with the given name already exists,
- * add a number as suffix to the file name.<br>
- * Example: For the file name "file.ext" this will return the first file of the list
- * <ul>
- * <li>file.ext</li>
- * <li>file_2.ext</li>
- * <li>file_3.ext</li>
- * </ul>
- * which does not yet exist.
- */
- public static File getUniqueNamedFile(final File file) {
- if (!file.exists()) {
- return file;
- }
- final String baseNameAndPath = file.getPath();
- final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_";
- final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, ".");
- for (int i = 2; i < Integer.MAX_VALUE; i++) {
- final File numbered = new File(prefix + i + extension);
- if (!numbered.exists()) {
- return numbered;
- }
- }
- throw new IllegalStateException("Unable to generate a non-existing file name");
- }
-
- /**
- * This usage of this method indicates that the return value of File.delete() can safely be ignored.
- */
- public static void deleteIgnoringFailure(final File file) {
- final boolean success = file.delete() || !file.exists();
- if (!success) {
- Log.i("Could not delete " + file.getAbsolutePath());
- }
- }
-
- /**
- * Deletes a file and logs deletion failures.
- *
- * @return <code> true</code> if this file was deleted, <code>false</code> otherwise.
- */
- public static boolean delete(final File file) {
- final boolean success = file.delete() || !file.exists();
- if (!success) {
- Log.e("Could not delete " + file.getAbsolutePath());
- }
- return success;
- }
-
- /**
- * Creates the directory named by the given file, creating any missing parent directories in the process.
- *
- * @return <code>true</code> if the directory was created, <code>false</code> on failure or if the directory already
- * existed.
- */
- public static boolean mkdirs(final File file) {
- final boolean success = file.mkdirs() || file.isDirectory(); // mkdirs returns false on existing directories
- if (!success) {
- Log.e("Could not make directories " + file.getAbsolutePath());
- }
- return success;
- }
-
- public static boolean writeFileUTF16(final File file, final String content) {
- // TODO: replace by some apache.commons IOUtils or FileUtils code
- Writer fileWriter = null;
- BufferedOutputStream buffer = null;
- try {
- final OutputStream os = new FileOutputStream(file);
- buffer = new BufferedOutputStream(os);
- fileWriter = new OutputStreamWriter(buffer, CharEncoding.UTF_16);
- fileWriter.write(content);
- } catch (final IOException e) {
- Log.e("FieldnoteExport.ExportTask export", e);
- return false;
- } finally {
- IOUtils.closeQuietly(fileWriter);
- IOUtils.closeQuietly(buffer);
- }
- return true;
- }
-
- /**
- * Check if the URL represents a file on the local file system.
- *
- * @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
- */
- public static boolean isFileUrl(final String url) {
- return StringUtils.startsWith(url, FILE_PROTOCOL);
- }
-
- /**
- * Build an URL from a file name.
- *
- * @param file a local file name
- * @return an URL with the <tt>file</tt> scheme
- */
- public static String fileToUrl(final File file) {
- return FILE_PROTOCOL + file.getAbsolutePath();
- }
-
- /**
- * Local file name when {@link #isFileUrl(String)} is <tt>true</tt>.
- *
- * @return the local file
- */
- public static File urlToFile(final String url) {
- return new File(StringUtils.substring(url, FILE_PROTOCOL.length()));
- }
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.CharEncoding;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.os.Handler;
+import android.os.Message;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * Utility class for files
+ *
+ */
+public final class FileUtils {
+
+ private static final int MAX_DIRECTORY_SCAN_DEPTH = 30;
+ private static final String FILE_PROTOCOL = "file://";
+
+ private FileUtils() {
+ // utility class
+ }
+
+ public static void listDir(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler) {
+ listDirInternally(result, directory, chooser, feedBackHandler, 0);
+ }
+
+ private static void listDirInternally(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler, final int depths) {
+ if (directory == null || !directory.isDirectory() || !directory.canRead()
+ || result == null
+ || chooser == null) {
+ return;
+ }
+
+ final File[] files = directory.listFiles();
+
+ if (ArrayUtils.isNotEmpty(files)) {
+ for (final File file : files) {
+ if (chooser.shouldEnd()) {
+ return;
+ }
+ if (!file.canRead()) {
+ continue;
+ }
+ String name = file.getName();
+ if (file.isFile()) {
+ if (chooser.isSelected(file)) {
+ result.add(file); // add file to list
+ }
+ } else if (file.isDirectory()) {
+ if (name.charAt(0) == '.') {
+ continue; // skip hidden directories
+ }
+ if (name.length() > 16) {
+ name = name.substring(0, 14) + '…';
+ }
+ if (feedBackHandler != null) {
+ feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name));
+ }
+
+ if (depths < MAX_DIRECTORY_SCAN_DEPTH) {
+ listDirInternally(result, file, chooser, feedBackHandler, depths + 1); // go deeper
+ }
+ }
+ }
+ }
+ }
+
+ public static boolean deleteDirectory(@NonNull final File dir) {
+ final File[] files = dir.listFiles();
+
+ // Although we are called on an existing directory, it might have been removed concurrently
+ // in the meantime, for example by the user or by another cleanup task.
+ if (files != null) {
+ for (final File file : files) {
+ if (file.isDirectory()) {
+ deleteDirectory(file);
+ } else {
+ delete(file);
+ }
+ }
+ }
+
+ return delete(dir);
+ }
+
+ public interface FileSelector {
+ boolean isSelected(File file);
+ boolean shouldEnd();
+ }
+
+ /**
+ * Create a unique non existing file named like the given file name. If a file with the given name already exists,
+ * add a number as suffix to the file name.<br>
+ * Example: For the file name "file.ext" this will return the first file of the list
+ * <ul>
+ * <li>file.ext</li>
+ * <li>file_2.ext</li>
+ * <li>file_3.ext</li>
+ * </ul>
+ * which does not yet exist.
+ */
+ @NonNull
+ public static File getUniqueNamedFile(final File file) {
+ if (!file.exists()) {
+ return file;
+ }
+ final String baseNameAndPath = file.getPath();
+ final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_";
+ final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, ".");
+ for (int i = 2; i < Integer.MAX_VALUE; i++) {
+ final File numbered = new File(prefix + i + extension);
+ if (!numbered.exists()) {
+ return numbered;
+ }
+ }
+ throw new IllegalStateException("Unable to generate a non-existing file name");
+ }
+
+ /**
+ * This usage of this method indicates that the return value of File.delete() can safely be ignored.
+ */
+ public static void deleteIgnoringFailure(final File file) {
+ final boolean success = file.delete() || !file.exists();
+ if (!success) {
+ Log.i("Could not delete " + file.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Deletes a file and logs deletion failures.
+ *
+ * @return <code> true</code> if this file was deleted, <code>false</code> otherwise.
+ */
+ public static boolean delete(final File file) {
+ final boolean success = file.delete() || !file.exists();
+ if (!success) {
+ Log.e("Could not delete " + file.getAbsolutePath());
+ }
+ return success;
+ }
+
+ /**
+ * Creates the directory named by the given file, creating any missing parent directories in the process.
+ *
+ * @return <code>true</code> if the directory was created, <code>false</code> on failure or if the directory already
+ * existed.
+ */
+ public static boolean mkdirs(final File file) {
+ final boolean success = file.mkdirs() || file.isDirectory(); // mkdirs returns false on existing directories
+ if (!success) {
+ Log.e("Could not make directories " + file.getAbsolutePath());
+ }
+ return success;
+ }
+
+ public static boolean writeFileUTF16(final File file, final String content) {
+ // TODO: replace by some apache.commons IOUtils or FileUtils code
+ Writer fileWriter = null;
+ BufferedOutputStream buffer = null;
+ try {
+ final OutputStream os = new FileOutputStream(file);
+ buffer = new BufferedOutputStream(os);
+ fileWriter = new OutputStreamWriter(buffer, CharEncoding.UTF_16);
+ fileWriter.write(content);
+ } catch (final IOException e) {
+ Log.e("FieldnoteExport.ExportTask export", e);
+ return false;
+ } finally {
+ IOUtils.closeQuietly(fileWriter);
+ IOUtils.closeQuietly(buffer);
+ }
+ return true;
+ }
+
+ /**
+ * Check if the URL represents a file on the local file system.
+ *
+ * @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
+ */
+ public static boolean isFileUrl(final String url) {
+ return StringUtils.startsWith(url, FILE_PROTOCOL);
+ }
+
+ /**
+ * Build an URL from a file name.
+ *
+ * @param file a local file name
+ * @return an URL with the <tt>file</tt> scheme
+ */
+ @NonNull
+ public static String fileToUrl(final File file) {
+ return FILE_PROTOCOL + file.getAbsolutePath();
+ }
+
+ /**
+ * Local file name when {@link #isFileUrl(String)} is <tt>true</tt>.
+ *
+ * @return the local file
+ */
+ @NonNull
+ public static File urlToFile(final String url) {
+ return new File(StringUtils.substring(url, FILE_PROTOCOL.length()));
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/Formatter.java b/main/src/cgeo/geocaching/utils/Formatter.java
index ed9a72c..afca300 100644
--- a/main/src/cgeo/geocaching/utils/Formatter.java
+++ b/main/src/cgeo/geocaching/utils/Formatter.java
@@ -1,238 +1,254 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.CgeoApplication;
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.R;
-import cgeo.geocaching.Waypoint;
-import cgeo.geocaching.enumerations.CacheSize;
-import cgeo.geocaching.enumerations.WaypointType;
-
-import org.apache.commons.lang3.StringUtils;
-
-import android.content.Context;
-import android.text.format.DateUtils;
-
-import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
-public final class Formatter {
-
- /** Text separator used for formatting texts */
- public static final String SEPARATOR = " · ";
-
- private static final Context context = CgeoApplication.getInstance().getBaseContext();
-
- private Formatter() {
- // Utility class
- }
-
- /**
- * Generate a time string according to system-wide settings (locale, 12/24 hour)
- * such as "13:24".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatTime(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME);
- }
-
- /**
- * Generate a date string according to system-wide settings (locale, date format)
- * such as "20 December" or "20 December 2010". The year will only be included when necessary.
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatDate(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE);
- }
-
- /**
- * Generate a date string according to system-wide settings (locale, date format)
- * such as "20 December 2010". The year will always be included, making it suitable
- * to generate long-lived log entries.
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatFullDate(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE
- | DateUtils.FORMAT_SHOW_YEAR);
- }
-
- /**
- * Generate a numeric date string according to system-wide settings (locale, date format)
- * such as "10/20/2010".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatShortDate(final long date) {
- final DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context);
- return dateFormat.format(date);
- }
-
- private static String formatShortDateIncludingWeekday(final long time) {
- return DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY) + ", " + formatShortDate(time);
- }
-
- /**
- * Generate a numeric date string according to system-wide settings (locale, date format)
- * such as "10/20/2010". Today and yesterday will be presented as strings "today" and "yesterday".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatShortDateVerbally(final long date) {
- final int diff = CalendarUtils.daysSince(date);
- switch (diff) {
- case 0:
- return CgeoApplication.getInstance().getString(R.string.log_today);
- case 1:
- return CgeoApplication.getInstance().getString(R.string.log_yesterday);
- default:
- return formatShortDate(date);
- }
- }
-
- /**
- * Generate a numeric date and time string according to system-wide settings (locale,
- * date format) such as "7 sept. at 12:35".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatShortDateTime(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL);
- }
-
- /**
- * Generate a numeric date and time string according to system-wide settings (locale,
- * date format) such as "7 september at 12:35".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- public static String formatDateTime(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
- }
-
- public static String formatCacheInfoLong(final Geocache cache) {
- final List<String> infos = new ArrayList<>();
- if (StringUtils.isNotBlank(cache.getGeocode())) {
- infos.add(cache.getGeocode());
- }
-
- addShortInfos(cache, infos);
-
- if (cache.isPremiumMembersOnly()) {
- infos.add(CgeoApplication.getInstance().getString(R.string.cache_premium));
- }
- return StringUtils.join(infos, SEPARATOR);
- }
-
- public static String formatCacheInfoShort(final Geocache cache) {
- final List<String> infos = new ArrayList<>();
- addShortInfos(cache, infos);
- return StringUtils.join(infos, SEPARATOR);
- }
-
- private static void addShortInfos(final Geocache cache, final List<String> infos) {
- if (cache.hasDifficulty()) {
- infos.add("D " + formatDT(cache.getDifficulty()));
- }
- if (cache.hasTerrain()) {
- infos.add("T " + formatDT(cache.getTerrain()));
- }
-
- // don't show "not chosen" for events and virtuals, that should be the normal case
- if (cache.getSize() != CacheSize.UNKNOWN && cache.showSize()) {
- infos.add(cache.getSize().getL10n());
- } else if (cache.isEventCache()) {
- final Date hiddenDate = cache.getHiddenDate();
- if (hiddenDate != null) {
- infos.add(formatShortDateIncludingWeekday(hiddenDate.getTime()));
- }
- }
- }
-
- private static String formatDT(final float value) {
- return String.format(Locale.getDefault(), "%.1f", value);
- }
-
- public static String formatCacheInfoHistory(final Geocache cache) {
- final List<String> infos = new ArrayList<>(3);
- infos.add(StringUtils.upperCase(cache.getGeocode()));
- infos.add(formatDate(cache.getVisitedDate()));
- infos.add(formatTime(cache.getVisitedDate()));
- return StringUtils.join(infos, SEPARATOR);
- }
-
- public static String formatWaypointInfo(final Waypoint waypoint) {
- final List<String> infos = new ArrayList<>(3);
- final WaypointType waypointType = waypoint.getWaypointType();
- if (waypointType != WaypointType.OWN && waypointType != null) {
- infos.add(waypointType.getL10n());
- }
- if (Waypoint.PREFIX_OWN.equalsIgnoreCase(waypoint.getPrefix())) {
- infos.add(CgeoApplication.getInstance().getString(R.string.waypoint_custom));
- } else {
- if (StringUtils.isNotBlank(waypoint.getPrefix())) {
- infos.add(waypoint.getPrefix());
- }
- if (StringUtils.isNotBlank(waypoint.getLookup())) {
- infos.add(waypoint.getLookup());
- }
- }
- return StringUtils.join(infos, SEPARATOR);
- }
-
- public static String formatDaysAgo(final long date) {
- final int days = CalendarUtils.daysSince(date);
- switch (days) {
- case 0:
- return CgeoApplication.getInstance().getString(R.string.log_today);
- case 1:
- return CgeoApplication.getInstance().getString(R.string.log_yesterday);
- default:
- return CgeoApplication.getInstance().getResources().getQuantityString(R.plurals.days_ago, days, days);
- }
- }
-
- /**
- * Formatting of the hidden date of a cache
- *
- * @return {@code null} or hidden date of the cache (or event date of the cache) in human readable format
- */
- public static String formatHiddenDate(final Geocache cache) {
- final Date hiddenDate = cache.getHiddenDate();
- if (hiddenDate == null) {
- return null;
- }
- final long time = hiddenDate.getTime();
- if (time <= 0) {
- return null;
- }
- String dateString = formatFullDate(time);
- if (cache.isEventCache()) {
- dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString;
- }
- return dateString;
- }
-
- public static String formatMapSubtitle(final Geocache cache) {
- return "D " + formatDT(cache.getDifficulty()) + SEPARATOR + "T " + formatDT(cache.getTerrain()) + SEPARATOR + cache.getGeocode();
- }
-
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.R;
+import cgeo.geocaching.Waypoint;
+import cgeo.geocaching.enumerations.CacheSize;
+import cgeo.geocaching.enumerations.WaypointType;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public final class Formatter {
+
+ /** Text separator used for formatting texts */
+ public static final String SEPARATOR = " · ";
+
+ private static final Context context = CgeoApplication.getInstance().getBaseContext();
+
+ private Formatter() {
+ // Utility class
+ }
+
+ /**
+ * Generate a time string according to system-wide settings (locale, 12/24 hour)
+ * such as "13:24".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatTime(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME);
+ }
+
+ /**
+ * Generate a date string according to system-wide settings (locale, date format)
+ * such as "20 December" or "20 December 2010". The year will only be included when necessary.
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatDate(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE);
+ }
+
+ /**
+ * Generate a date string according to system-wide settings (locale, date format)
+ * such as "20 December 2010". The year will always be included, making it suitable
+ * to generate long-lived log entries.
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatFullDate(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_YEAR);
+ }
+
+ /**
+ * Generate a numeric date string according to system-wide settings (locale, date format)
+ * such as "10/20/2010".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatShortDate(final long date) {
+ final DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context);
+ return dateFormat.format(date);
+ }
+
+ private static String formatShortDateIncludingWeekday(final long time) {
+ return DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY) + ", " + formatShortDate(time);
+ }
+
+ /**
+ * Generate a numeric date string according to system-wide settings (locale, date format)
+ * such as "10/20/2010". Today and yesterday will be presented as strings "today" and "yesterday".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatShortDateVerbally(final long date) {
+ final int diff = CalendarUtils.daysSince(date);
+ switch (diff) {
+ case 0:
+ return CgeoApplication.getInstance().getString(R.string.log_today);
+ case 1:
+ return CgeoApplication.getInstance().getString(R.string.log_yesterday);
+ default:
+ return formatShortDate(date);
+ }
+ }
+
+ /**
+ * Generate a numeric date and time string according to system-wide settings (locale,
+ * date format) such as "7 sept. at 12:35".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatShortDateTime(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ /**
+ * Generate a numeric date and time string according to system-wide settings (locale,
+ * date format) such as "7 september at 12:35".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ @NonNull
+ public static String formatDateTime(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
+ }
+
+ @NonNull
+ public static String formatCacheInfoLong(final Geocache cache) {
+ final List<String> infos = new ArrayList<>();
+ if (StringUtils.isNotBlank(cache.getGeocode())) {
+ infos.add(cache.getGeocode());
+ }
+
+ addShortInfos(cache, infos);
+
+ if (cache.isPremiumMembersOnly()) {
+ infos.add(CgeoApplication.getInstance().getString(R.string.cache_premium));
+ }
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ @NonNull
+ public static String formatCacheInfoShort(final Geocache cache) {
+ final List<String> infos = new ArrayList<>();
+ addShortInfos(cache, infos);
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ private static void addShortInfos(final Geocache cache, final List<String> infos) {
+ if (cache.hasDifficulty()) {
+ infos.add("D " + formatDT(cache.getDifficulty()));
+ }
+ if (cache.hasTerrain()) {
+ infos.add("T " + formatDT(cache.getTerrain()));
+ }
+
+ // don't show "not chosen" for events and virtuals, that should be the normal case
+ if (cache.getSize() != CacheSize.UNKNOWN && cache.showSize()) {
+ infos.add(cache.getSize().getL10n());
+ } else if (cache.isEventCache()) {
+ final Date hiddenDate = cache.getHiddenDate();
+ if (hiddenDate != null) {
+ infos.add(formatShortDateIncludingWeekday(hiddenDate.getTime()));
+ }
+ }
+ }
+
+ private static String formatDT(final float value) {
+ return String.format(Locale.getDefault(), "%.1f", value);
+ }
+
+ @NonNull
+ public static String formatCacheInfoHistory(final Geocache cache) {
+ final List<String> infos = new ArrayList<>(3);
+ infos.add(StringUtils.upperCase(cache.getGeocode()));
+ infos.add(formatDate(cache.getVisitedDate()));
+ infos.add(formatTime(cache.getVisitedDate()));
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ @NonNull
+ public static String formatWaypointInfo(final Waypoint waypoint) {
+ final List<String> infos = new ArrayList<>(3);
+ final WaypointType waypointType = waypoint.getWaypointType();
+ if (waypointType != WaypointType.OWN && waypointType != null) {
+ infos.add(waypointType.getL10n());
+ }
+ if (Waypoint.PREFIX_OWN.equalsIgnoreCase(waypoint.getPrefix())) {
+ infos.add(CgeoApplication.getInstance().getString(R.string.waypoint_custom));
+ } else {
+ if (StringUtils.isNotBlank(waypoint.getPrefix())) {
+ infos.add(waypoint.getPrefix());
+ }
+ if (StringUtils.isNotBlank(waypoint.getLookup())) {
+ infos.add(waypoint.getLookup());
+ }
+ }
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ @NonNull
+ public static String formatDaysAgo(final long date) {
+ final int days = CalendarUtils.daysSince(date);
+ switch (days) {
+ case 0:
+ return CgeoApplication.getInstance().getString(R.string.log_today);
+ case 1:
+ return CgeoApplication.getInstance().getString(R.string.log_yesterday);
+ default:
+ return CgeoApplication.getInstance().getResources().getQuantityString(R.plurals.days_ago, days, days);
+ }
+ }
+
+ /**
+ * Formatting of the hidden date of a cache
+ *
+ * @return {@code null} or hidden date of the cache (or event date of the cache) in human readable format
+ */
+ @Nullable
+ public static String formatHiddenDate(final Geocache cache) {
+ final Date hiddenDate = cache.getHiddenDate();
+ if (hiddenDate == null) {
+ return null;
+ }
+ final long time = hiddenDate.getTime();
+ if (time <= 0) {
+ return null;
+ }
+ String dateString = formatFullDate(time);
+ if (cache.isEventCache()) {
+ dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString;
+ }
+ return dateString;
+ }
+
+ @NonNull
+ public static String formatMapSubtitle(final Geocache cache) {
+ return "D " + formatDT(cache.getDifficulty()) + SEPARATOR + "T " + formatDT(cache.getTerrain()) + SEPARATOR + cache.getGeocode();
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/utils/HtmlUtils.java b/main/src/cgeo/geocaching/utils/HtmlUtils.java
index 8202cd4..0748e84 100644
--- a/main/src/cgeo/geocaching/utils/HtmlUtils.java
+++ b/main/src/cgeo/geocaching/utils/HtmlUtils.java
@@ -1,73 +1,76 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
-
-import android.text.Html;
-import android.text.Spanned;
-import android.text.style.ImageSpan;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-public final class HtmlUtils {
-
- private HtmlUtils() {
- // utility class
- }
-
- /**
- * Extract the text from a HTML based string. This is similar to what HTML.fromHtml(...) does, but this method also
- * removes the embedded images instead of replacing them by a small rectangular representation character.
- *
- */
- public static String extractText(final CharSequence html) {
- if (StringUtils.isBlank(html)) {
- return StringUtils.EMPTY;
- }
- String result = html.toString();
-
- // recognize images in textview HTML contents
- if (html instanceof Spanned) {
- final Spanned text = (Spanned) html;
- final Object[] styles = text.getSpans(0, text.length(), Object.class);
- final List<Pair<Integer, Integer>> removals = new ArrayList<>();
- for (final Object style : styles) {
- if (style instanceof ImageSpan) {
- final int start = text.getSpanStart(style);
- final int end = text.getSpanEnd(style);
- removals.add(Pair.of(start, end));
- }
- }
-
- // sort reversed and delete image spans
- Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() {
-
- @Override
- public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) {
- return rhs.getRight().compareTo(lhs.getRight());
- }
- });
- result = text.toString();
- for (final Pair<Integer, Integer> removal : removals) {
- result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight());
- }
- }
-
- // now that images are gone, do a normal html to text conversion
- return Html.fromHtml(result).toString().trim();
- }
-
- public static String removeExtraParagraph(final String htmlIn) {
- final String html = StringUtils.trim(htmlIn);
- if (StringUtils.startsWith(html, "<p>") && StringUtils.endsWith(html, "</p>")) {
- final String paragraph = StringUtils.substring(html, "<p>".length(), html.length() - "</p>".length()).trim();
- if (extractText(paragraph).equals(paragraph)) {
- return paragraph;
- }
- }
- return html;
- }
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.ImageSpan;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public final class HtmlUtils {
+
+ private HtmlUtils() {
+ // utility class
+ }
+
+ /**
+ * Extract the text from a HTML based string. This is similar to what HTML.fromHtml(...) does, but this method also
+ * removes the embedded images instead of replacing them by a small rectangular representation character.
+ *
+ */
+ @NonNull
+ public static String extractText(final CharSequence html) {
+ if (StringUtils.isBlank(html)) {
+ return StringUtils.EMPTY;
+ }
+ String result = html.toString();
+
+ // recognize images in textview HTML contents
+ if (html instanceof Spanned) {
+ final Spanned text = (Spanned) html;
+ final Object[] styles = text.getSpans(0, text.length(), Object.class);
+ final List<Pair<Integer, Integer>> removals = new ArrayList<>();
+ for (final Object style : styles) {
+ if (style instanceof ImageSpan) {
+ final int start = text.getSpanStart(style);
+ final int end = text.getSpanEnd(style);
+ removals.add(Pair.of(start, end));
+ }
+ }
+
+ // sort reversed and delete image spans
+ Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() {
+
+ @Override
+ public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) {
+ return rhs.getRight().compareTo(lhs.getRight());
+ }
+ });
+ result = text.toString();
+ for (final Pair<Integer, Integer> removal : removals) {
+ result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight());
+ }
+ }
+
+ // now that images are gone, do a normal html to text conversion
+ return Html.fromHtml(result).toString().trim();
+ }
+
+ @NonNull
+ public static String removeExtraParagraph(final String htmlIn) {
+ final String html = StringUtils.trim(htmlIn);
+ if (StringUtils.startsWith(html, "<p>") && StringUtils.endsWith(html, "</p>")) {
+ final String paragraph = StringUtils.substring(html, "<p>".length(), html.length() - "</p>".length()).trim();
+ if (extractText(paragraph).equals(paragraph)) {
+ return paragraph;
+ }
+ }
+ return html;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/ImageUtils.java b/main/src/cgeo/geocaching/utils/ImageUtils.java
index 269791a..000966a 100644
--- a/main/src/cgeo/geocaching/utils/ImageUtils.java
+++ b/main/src/cgeo/geocaching/utils/ImageUtils.java
@@ -1,486 +1,490 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.CgeoApplication;
-import cgeo.geocaching.Image;
-import cgeo.geocaching.R;
-import cgeo.geocaching.compatibility.Compatibility;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.ImmutablePair;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-
-import rx.Observable;
-import rx.Scheduler.Worker;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.functions.Action0;
-import rx.functions.Action1;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.media.ExifInterface;
-import android.net.Uri;
-import android.os.Environment;
-import android.text.Html;
-import android.text.Html.ImageGetter;
-import android.util.Base64;
-import android.util.Base64InputStream;
-import android.widget.TextView;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.ref.WeakReference;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.LinkedBlockingQueue;
-
-public final class ImageUtils {
- private static final int[] ORIENTATIONS = {
- ExifInterface.ORIENTATION_ROTATE_90,
- ExifInterface.ORIENTATION_ROTATE_180,
- ExifInterface.ORIENTATION_ROTATE_270
- };
-
- private static final int[] ROTATION = { 90, 180, 270 };
- private static final int MAX_DISPLAY_IMAGE_XY = 800;
-
- // Images whose URL contains one of those patterns will not be available on the Images tab
- // for opening into an external application.
- private final static String[] NO_EXTERNAL = { "geocheck.org" };
-
- private ImageUtils() {
- // Do not let this class be instantiated, this is a utility class.
- }
-
- /**
- * Scales a bitmap to the device display size.
- *
- * @param image
- * The image Bitmap representation to scale
- * @return BitmapDrawable The scaled image
- */
- public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) {
- final Point displaySize = Compatibility.getDisplaySize();
- final int maxWidth = displaySize.x - 25;
- final int maxHeight = displaySize.y - 25;
- return scaleBitmapTo(image, maxWidth, maxHeight);
- }
-
- /**
- * Reads and scales an image file to the device display size.
- *
- * @param filename
- * The image file to read and scale
- * @return Bitmap The scaled image or Null if source image can't be read
- */
- @Nullable
- public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) {
- final Point displaySize = Compatibility.getDisplaySize();
- // Restrict image size to 800 x 800 to prevent OOM on tablets
- final int maxWidth = Math.min(displaySize.x - 25, MAX_DISPLAY_IMAGE_XY);
- final int maxHeight = Math.min(displaySize.y - 25, MAX_DISPLAY_IMAGE_XY);
- final Bitmap image = readDownsampledImage(filename, maxWidth, maxHeight);
- if (image == null) {
- return null;
- }
- final BitmapDrawable scaledImage = scaleBitmapTo(image, maxWidth, maxHeight);
- return scaledImage.getBitmap();
- }
-
- /**
- * Scales a bitmap to the given bounds if it is larger, otherwise returns the original bitmap.
- *
- * @param image
- * The bitmap to scale
- * @return BitmapDrawable The scaled image
- */
- @NonNull
- private static BitmapDrawable scaleBitmapTo(@NonNull final Bitmap image, final int maxWidth, final int maxHeight) {
- final CgeoApplication app = CgeoApplication.getInstance();
- Bitmap result = image;
- int width = image.getWidth();
- int height = image.getHeight();
-
- if (width > maxWidth || height > maxHeight) {
- final double ratio = Math.min((double) maxHeight / (double) height, (double) maxWidth / (double) width);
- width = (int) Math.ceil(width * ratio);
- height = (int) Math.ceil(height * ratio);
- result = Bitmap.createScaledBitmap(image, width, height, true);
- }
-
- final BitmapDrawable resultDrawable = new BitmapDrawable(app.getResources(), result);
- resultDrawable.setBounds(new Rect(0, 0, width, height));
- return resultDrawable;
- }
-
- /**
- * Store a bitmap to file.
- *
- * @param bitmap
- * The bitmap to store
- * @param format
- * The image format
- * @param quality
- * The image quality
- * @param pathOfOutputImage
- * Path to store to
- */
- public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) {
- try {
- final FileOutputStream out = new FileOutputStream(pathOfOutputImage);
- final BufferedOutputStream bos = new BufferedOutputStream(out);
- bitmap.compress(format, quality, bos);
- bos.flush();
- bos.close();
- } catch (final IOException e) {
- Log.e("ImageHelper.storeBitmap", e);
- }
- }
-
- /**
- * Scales an image to the desired bounds and encodes to file.
- *
- * @param filePath
- * Image to read
- * @param maxXY
- * bounds
- * @return filename and path, <tt>null</tt> if something fails
- */
- @Nullable
- public static String readScaleAndWriteImage(@NonNull final String filePath, final int maxXY) {
- if (maxXY <= 0) {
- return filePath;
- }
- final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY);
- if (image == null) {
- return null;
- }
- final BitmapDrawable scaledImage = scaleBitmapTo(image, maxXY, maxXY);
- final File tempImageFile = getOutputImageFile();
- if (tempImageFile == null) {
- Log.e("ImageUtils.readScaleAndWriteImage: unable to write scaled image");
- return null;
- }
- final String uploadFilename = tempImageFile.getPath();
- storeBitmap(scaledImage.getBitmap(), Bitmap.CompressFormat.JPEG, 75, uploadFilename);
- return uploadFilename;
- }
-
- /**
- * Reads and scales an image file with downsampling in one step to prevent memory consumption.
- *
- * @param filePath
- * The file to read
- * @param maxX
- * The desired width
- * @param maxY
- * The desired height
- * @return Bitmap the image or null if file can't be read
- */
- @Nullable
- public static Bitmap readDownsampledImage(@NonNull final String filePath, final int maxX, final int maxY) {
- int orientation = ExifInterface.ORIENTATION_NORMAL;
- try {
- final ExifInterface exif = new ExifInterface(filePath);
- orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
- } catch (final IOException e) {
- Log.e("ImageUtils.readDownsampledImage", e);
- }
- final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options();
- sizeOnlyOptions.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(filePath, sizeOnlyOptions);
- final int myMaxXY = Math.max(sizeOnlyOptions.outHeight, sizeOnlyOptions.outWidth);
- final int maxXY = Math.max(maxX, maxY);
- final int sampleSize = myMaxXY / maxXY;
- final BitmapFactory.Options sampleOptions = new BitmapFactory.Options();
- if (sampleSize > 1) {
- sampleOptions.inSampleSize = sampleSize;
- }
- final Bitmap decodedImage = BitmapFactory.decodeFile(filePath, sampleOptions);
- if (decodedImage != null) {
- for (int i = 0; i < ORIENTATIONS.length; i++) {
- if (orientation == ORIENTATIONS[i]) {
- final Matrix matrix = new Matrix();
- matrix.postRotate(ROTATION[i]);
- return Bitmap.createBitmap(decodedImage, 0, 0, decodedImage.getWidth(), decodedImage.getHeight(), matrix, true);
- }
- }
- }
- return decodedImage;
- }
-
- /** Create a File for saving an image or video
- *
- * @return the temporary image file to use, or <tt>null</tt> if the media directory could
- * not be created.
- * */
- @Nullable
- public static File getOutputImageFile() {
- // To be safe, you should check that the SDCard is mounted
- // using Environment.getExternalStorageState() before doing this.
- final File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "cgeo");
-
- // This location works best if you want the created images to be shared
- // between applications and persist after your app has been uninstalled.
-
- // Create the storage directory if it does not exist
- if (!mediaStorageDir.exists()) {
- if (!FileUtils.mkdirs(mediaStorageDir)) {
- Log.e("ImageUtils.getOutputImageFile: cannot create media storage directory");
- return null;
- }
- }
-
- // Create a media file name
- final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
- return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
- }
-
- @Nullable
- public static Uri getOutputImageFileUri() {
- final File file = getOutputImageFile();
- if (file == null) {
- return null;
- }
- return Uri.fromFile(file);
- }
-
- /**
- * Check if the URL contains one of the given substrings.
- *
- * @param url the URL to check
- * @param patterns a list of substrings to check against
- * @return <tt>true</tt> if the URL contains at least one of the patterns, <tt>false</tt> otherwise
- */
- public static boolean containsPattern(final String url, final String[] patterns) {
- for (final String entry : patterns) {
- if (StringUtils.containsIgnoreCase(url, entry)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Decode a base64-encoded string and save the result into a file.
- *
- * @param inString the encoded string
- * @param outFile the file to save the decoded result into
- */
- public static void decodeBase64ToFile(final String inString, final File outFile) {
- FileOutputStream out = null;
- try {
- out = new FileOutputStream(outFile);
- decodeBase64ToStream(inString, out);
- } catch (final IOException e) {
- Log.e("HtmlImage.decodeBase64ToFile: cannot write file for decoded inline image", e);
- } finally {
- IOUtils.closeQuietly(out);
- }
- }
-
- /**
- * Decode a base64-encoded string and save the result into a stream.
- *
- * @param inString
- * the encoded string
- * @param out
- * the stream to save the decoded result into
- */
- public static void decodeBase64ToStream(final String inString, final OutputStream out) throws IOException {
- Base64InputStream in = null;
- try {
- in = new Base64InputStream(new ByteArrayInputStream(inString.getBytes(TextUtils.CHARSET_ASCII)), Base64.DEFAULT);
- IOUtils.copy(in, out);
- } finally {
- IOUtils.closeQuietly(in);
- }
- }
-
- public static BitmapDrawable getTransparent1x1Drawable(final Resources res) {
- return new BitmapDrawable(res, BitmapFactory.decodeResource(res, R.drawable.image_no_placement));
- }
-
- /**
- * Add images present in the HTML description to the existing collection.
- * @param images a collection of images
- * @param geocode the common title for images in the description
- * @param htmlText the HTML description to be parsed, can be repeated
- */
- public static void addImagesFromHtml(final Collection<Image> images, final String geocode, final String... htmlText) {
- final Set<String> urls = new LinkedHashSet<>();
- for (final Image image : images) {
- urls.add(image.getUrl());
- }
- for (final String text: htmlText) {
- Html.fromHtml(StringUtils.defaultString(text), new ImageGetter() {
- @Override
- public Drawable getDrawable(final String source) {
- if (!urls.contains(source) && canBeOpenedExternally(source)) {
- images.add(new Image.Builder()
- .setUrl(source)
- .setTitle(StringUtils.defaultString(geocode))
- .build());
- urls.add(source);
- }
- return null;
- }
- }, null);
- }
- }
-
- /**
- * Container which can hold a drawable (initially an empty one) and get a newer version when it
- * becomes available. It also invalidates the view the container belongs to, so that it is
- * redrawn properly.
- * <p/>
- * When a new version of the drawable is available, it is put into a queue and, if needed (no other elements
- * waiting in the queue), a refresh is launched on the UI thread. This refresh will empty the queue (including
- * elements arrived in the meantime) and ensures that the view is uploaded only once all the queued requests have
- * been handled.
- */
- public static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> {
- final private static Object lock = new Object(); // Used to lock the queue to determine if a refresh needs to be scheduled
- final private static LinkedBlockingQueue<ImmutablePair<ContainerDrawable, Drawable>> REDRAW_QUEUE = new LinkedBlockingQueue<>();
- final private static Set<TextView> VIEWS = new HashSet<>(); // Modified only on the UI thread
- final private static Worker UI_WORKER = AndroidSchedulers.mainThread().createWorker();
- final private static Action0 REDRAW_QUEUED_DRAWABLES = new Action0() {
- @Override
- public void call() {
- redrawQueuedDrawables();
- }
- };
-
- private Drawable drawable;
- final protected WeakReference<TextView> viewRef;
-
- @SuppressWarnings("deprecation")
- public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
- viewRef = new WeakReference<>(view);
- drawable = null;
- setBounds(0, 0, 0, 0);
- drawableObservable.subscribe(this);
- }
-
- @Override
- public final void draw(final Canvas canvas) {
- if (drawable != null) {
- drawable.draw(canvas);
- }
- }
-
- @Override
- public final void call(final Drawable newDrawable) {
- final boolean needsRedraw;
- synchronized (lock) {
- // Check for emptyness inside the call to match the behaviour in redrawQueuedDrawables().
- needsRedraw = REDRAW_QUEUE.isEmpty();
- REDRAW_QUEUE.add(ImmutablePair.of(this, newDrawable));
- }
- if (needsRedraw) {
- UI_WORKER.schedule(REDRAW_QUEUED_DRAWABLES);
- }
- }
-
- /**
- * Update the container with the new drawable. Called on the UI thread.
- *
- * @param newDrawable the new drawable
- * @return the view to update or <tt>null</tt> if the view is not alive anymore
- */
- protected TextView updateDrawable(final Drawable newDrawable) {
- setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
- drawable = newDrawable;
- return viewRef.get();
- }
-
- private static void redrawQueuedDrawables() {
- if (!REDRAW_QUEUE.isEmpty()) {
- // Add a small margin so that drawables arriving between the beginning of the allocation and the draining
- // of the queue might be absorbed without reallocation.
- final List<ImmutablePair<ContainerDrawable, Drawable>> toRedraw = new ArrayList<>(REDRAW_QUEUE.size() + 16);
- synchronized (lock) {
- // Empty the queue inside the lock to match the check done in call().
- REDRAW_QUEUE.drainTo(toRedraw);
- }
- for (final ImmutablePair<ContainerDrawable, Drawable> redrawable : toRedraw) {
- final TextView view = redrawable.left.updateDrawable(redrawable.right);
- if (view != null) {
- VIEWS.add(view);
- }
- }
- for (final TextView view : VIEWS) {
- view.setText(view.getText());
- }
- VIEWS.clear();
- }
- }
-
- }
-
- /**
- * Image that automatically scales to fit a line of text in the containing {@link TextView}.
- */
- public final static class LineHeightContainerDrawable extends ContainerDrawable {
- public LineHeightContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
- super(view, drawableObservable);
- }
-
- @Override
- protected TextView updateDrawable(final Drawable newDrawable) {
- final TextView view = super.updateDrawable(newDrawable);
- if (view != null) {
- setBounds(scaleImageToLineHeight(newDrawable, view));
- }
- return view;
- }
- }
-
- public static boolean canBeOpenedExternally(final String source) {
- return !containsPattern(source, NO_EXTERNAL);
- }
-
- public static Rect scaleImageToLineHeight(final Drawable drawable, final TextView view) {
- final int lineHeight = (int) (view.getLineHeight() * 0.8);
- final int width = drawable.getIntrinsicWidth() * lineHeight / drawable.getIntrinsicHeight();
- return new Rect(0, 0, width, lineHeight);
- }
-
- public static Bitmap convertToBitmap(final Drawable drawable) {
- if (drawable instanceof BitmapDrawable) {
- return ((BitmapDrawable) drawable).getBitmap();
- }
-
- // handle solid colors, which have no width
- int width = drawable.getIntrinsicWidth();
- width = width > 0 ? width : 1;
- int height = drawable.getIntrinsicHeight();
- height = height > 0 ? height : 1;
-
- final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- drawable.draw(canvas);
-
- return bitmap;
- }
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.Image;
+import cgeo.geocaching.R;
+import cgeo.geocaching.compatibility.Compatibility;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.text.Html;
+import android.text.Html.ImageGetter;
+import android.util.Base64;
+import android.util.Base64InputStream;
+import android.widget.TextView;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import rx.Observable;
+import rx.Scheduler.Worker;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action0;
+import rx.functions.Action1;
+
+public final class ImageUtils {
+ private static final int[] ORIENTATIONS = {
+ ExifInterface.ORIENTATION_ROTATE_90,
+ ExifInterface.ORIENTATION_ROTATE_180,
+ ExifInterface.ORIENTATION_ROTATE_270
+ };
+
+ private static final int[] ROTATION = { 90, 180, 270 };
+ private static final int MAX_DISPLAY_IMAGE_XY = 800;
+
+ // Images whose URL contains one of those patterns will not be available on the Images tab
+ // for opening into an external application.
+ private final static String[] NO_EXTERNAL = { "geocheck.org" };
+
+ private ImageUtils() {
+ // Do not let this class be instantiated, this is a utility class.
+ }
+
+ /**
+ * Scales a bitmap to the device display size.
+ *
+ * @param image
+ * The image Bitmap representation to scale
+ * @return BitmapDrawable The scaled image
+ */
+ @NonNull
+ public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) {
+ final Point displaySize = Compatibility.getDisplaySize();
+ final int maxWidth = displaySize.x - 25;
+ final int maxHeight = displaySize.y - 25;
+ return scaleBitmapTo(image, maxWidth, maxHeight);
+ }
+
+ /**
+ * Reads and scales an image file to the device display size.
+ *
+ * @param filename
+ * The image file to read and scale
+ * @return Bitmap The scaled image or Null if source image can't be read
+ */
+ @Nullable
+ public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) {
+ final Point displaySize = Compatibility.getDisplaySize();
+ // Restrict image size to 800 x 800 to prevent OOM on tablets
+ final int maxWidth = Math.min(displaySize.x - 25, MAX_DISPLAY_IMAGE_XY);
+ final int maxHeight = Math.min(displaySize.y - 25, MAX_DISPLAY_IMAGE_XY);
+ final Bitmap image = readDownsampledImage(filename, maxWidth, maxHeight);
+ if (image == null) {
+ return null;
+ }
+ final BitmapDrawable scaledImage = scaleBitmapTo(image, maxWidth, maxHeight);
+ return scaledImage.getBitmap();
+ }
+
+ /**
+ * Scales a bitmap to the given bounds if it is larger, otherwise returns the original bitmap.
+ *
+ * @param image
+ * The bitmap to scale
+ * @return BitmapDrawable The scaled image
+ */
+ @NonNull
+ private static BitmapDrawable scaleBitmapTo(@NonNull final Bitmap image, final int maxWidth, final int maxHeight) {
+ final CgeoApplication app = CgeoApplication.getInstance();
+ Bitmap result = image;
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ if (width > maxWidth || height > maxHeight) {
+ final double ratio = Math.min((double) maxHeight / (double) height, (double) maxWidth / (double) width);
+ width = (int) Math.ceil(width * ratio);
+ height = (int) Math.ceil(height * ratio);
+ result = Bitmap.createScaledBitmap(image, width, height, true);
+ }
+
+ final BitmapDrawable resultDrawable = new BitmapDrawable(app.getResources(), result);
+ resultDrawable.setBounds(new Rect(0, 0, width, height));
+ return resultDrawable;
+ }
+
+ /**
+ * Store a bitmap to file.
+ *
+ * @param bitmap
+ * The bitmap to store
+ * @param format
+ * The image format
+ * @param quality
+ * The image quality
+ * @param pathOfOutputImage
+ * Path to store to
+ */
+ public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) {
+ try {
+ final FileOutputStream out = new FileOutputStream(pathOfOutputImage);
+ final BufferedOutputStream bos = new BufferedOutputStream(out);
+ bitmap.compress(format, quality, bos);
+ bos.flush();
+ bos.close();
+ } catch (final IOException e) {
+ Log.e("ImageHelper.storeBitmap", e);
+ }
+ }
+
+ /**
+ * Scales an image to the desired bounds and encodes to file.
+ *
+ * @param filePath
+ * Image to read
+ * @param maxXY
+ * bounds
+ * @return filename and path, <tt>null</tt> if something fails
+ */
+ @Nullable
+ public static String readScaleAndWriteImage(@NonNull final String filePath, final int maxXY) {
+ if (maxXY <= 0) {
+ return filePath;
+ }
+ final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY);
+ if (image == null) {
+ return null;
+ }
+ final BitmapDrawable scaledImage = scaleBitmapTo(image, maxXY, maxXY);
+ final File tempImageFile = getOutputImageFile();
+ if (tempImageFile == null) {
+ Log.e("ImageUtils.readScaleAndWriteImage: unable to write scaled image");
+ return null;
+ }
+ final String uploadFilename = tempImageFile.getPath();
+ storeBitmap(scaledImage.getBitmap(), Bitmap.CompressFormat.JPEG, 75, uploadFilename);
+ return uploadFilename;
+ }
+
+ /**
+ * Reads and scales an image file with downsampling in one step to prevent memory consumption.
+ *
+ * @param filePath
+ * The file to read
+ * @param maxX
+ * The desired width
+ * @param maxY
+ * The desired height
+ * @return Bitmap the image or null if file can't be read
+ */
+ @Nullable
+ public static Bitmap readDownsampledImage(@NonNull final String filePath, final int maxX, final int maxY) {
+ int orientation = ExifInterface.ORIENTATION_NORMAL;
+ try {
+ final ExifInterface exif = new ExifInterface(filePath);
+ orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ } catch (final IOException e) {
+ Log.e("ImageUtils.readDownsampledImage", e);
+ }
+ final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options();
+ sizeOnlyOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(filePath, sizeOnlyOptions);
+ final int myMaxXY = Math.max(sizeOnlyOptions.outHeight, sizeOnlyOptions.outWidth);
+ final int maxXY = Math.max(maxX, maxY);
+ final int sampleSize = myMaxXY / maxXY;
+ final BitmapFactory.Options sampleOptions = new BitmapFactory.Options();
+ if (sampleSize > 1) {
+ sampleOptions.inSampleSize = sampleSize;
+ }
+ final Bitmap decodedImage = BitmapFactory.decodeFile(filePath, sampleOptions);
+ if (decodedImage != null) {
+ for (int i = 0; i < ORIENTATIONS.length; i++) {
+ if (orientation == ORIENTATIONS[i]) {
+ final Matrix matrix = new Matrix();
+ matrix.postRotate(ROTATION[i]);
+ return Bitmap.createBitmap(decodedImage, 0, 0, decodedImage.getWidth(), decodedImage.getHeight(), matrix, true);
+ }
+ }
+ }
+ return decodedImage;
+ }
+
+ /** Create a File for saving an image or video
+ *
+ * @return the temporary image file to use, or <tt>null</tt> if the media directory could
+ * not be created.
+ * */
+ @Nullable
+ public static File getOutputImageFile() {
+ // To be safe, you should check that the SDCard is mounted
+ // using Environment.getExternalStorageState() before doing this.
+ final File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "cgeo");
+
+ // This location works best if you want the created images to be shared
+ // between applications and persist after your app has been uninstalled.
+
+ // Create the storage directory if it does not exist
+ if (!mediaStorageDir.exists()) {
+ if (!FileUtils.mkdirs(mediaStorageDir)) {
+ Log.e("ImageUtils.getOutputImageFile: cannot create media storage directory");
+ return null;
+ }
+ }
+
+ // Create a media file name
+ final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
+ return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
+ }
+
+ @Nullable
+ public static Uri getOutputImageFileUri() {
+ final File file = getOutputImageFile();
+ if (file == null) {
+ return null;
+ }
+ return Uri.fromFile(file);
+ }
+
+ /**
+ * Check if the URL contains one of the given substrings.
+ *
+ * @param url the URL to check
+ * @param patterns a list of substrings to check against
+ * @return <tt>true</tt> if the URL contains at least one of the patterns, <tt>false</tt> otherwise
+ */
+ public static boolean containsPattern(final String url, final String[] patterns) {
+ for (final String entry : patterns) {
+ if (StringUtils.containsIgnoreCase(url, entry)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Decode a base64-encoded string and save the result into a file.
+ *
+ * @param inString the encoded string
+ * @param outFile the file to save the decoded result into
+ */
+ public static void decodeBase64ToFile(final String inString, final File outFile) {
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(outFile);
+ decodeBase64ToStream(inString, out);
+ } catch (final IOException e) {
+ Log.e("HtmlImage.decodeBase64ToFile: cannot write file for decoded inline image", e);
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /**
+ * Decode a base64-encoded string and save the result into a stream.
+ *
+ * @param inString
+ * the encoded string
+ * @param out
+ * the stream to save the decoded result into
+ */
+ public static void decodeBase64ToStream(final String inString, final OutputStream out) throws IOException {
+ Base64InputStream in = null;
+ try {
+ in = new Base64InputStream(new ByteArrayInputStream(inString.getBytes(TextUtils.CHARSET_ASCII)), Base64.DEFAULT);
+ IOUtils.copy(in, out);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ @NonNull
+ public static BitmapDrawable getTransparent1x1Drawable(final Resources res) {
+ return new BitmapDrawable(res, BitmapFactory.decodeResource(res, R.drawable.image_no_placement));
+ }
+
+ /**
+ * Add images present in the HTML description to the existing collection.
+ * @param images a collection of images
+ * @param geocode the common title for images in the description
+ * @param htmlText the HTML description to be parsed, can be repeated
+ */
+ public static void addImagesFromHtml(final Collection<Image> images, final String geocode, final String... htmlText) {
+ final Set<String> urls = new LinkedHashSet<>();
+ for (final Image image : images) {
+ urls.add(image.getUrl());
+ }
+ for (final String text: htmlText) {
+ Html.fromHtml(StringUtils.defaultString(text), new ImageGetter() {
+ @Override
+ public Drawable getDrawable(final String source) {
+ if (!urls.contains(source) && canBeOpenedExternally(source)) {
+ images.add(new Image.Builder()
+ .setUrl(source)
+ .setTitle(StringUtils.defaultString(geocode))
+ .build());
+ urls.add(source);
+ }
+ return null;
+ }
+ }, null);
+ }
+ }
+
+ /**
+ * Container which can hold a drawable (initially an empty one) and get a newer version when it
+ * becomes available. It also invalidates the view the container belongs to, so that it is
+ * redrawn properly.
+ * <p/>
+ * When a new version of the drawable is available, it is put into a queue and, if needed (no other elements
+ * waiting in the queue), a refresh is launched on the UI thread. This refresh will empty the queue (including
+ * elements arrived in the meantime) and ensures that the view is uploaded only once all the queued requests have
+ * been handled.
+ */
+ public static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> {
+ final private static Object lock = new Object(); // Used to lock the queue to determine if a refresh needs to be scheduled
+ final private static LinkedBlockingQueue<ImmutablePair<ContainerDrawable, Drawable>> REDRAW_QUEUE = new LinkedBlockingQueue<>();
+ final private static Set<TextView> VIEWS = new HashSet<>(); // Modified only on the UI thread
+ final private static Worker UI_WORKER = AndroidSchedulers.mainThread().createWorker();
+ final private static Action0 REDRAW_QUEUED_DRAWABLES = new Action0() {
+ @Override
+ public void call() {
+ redrawQueuedDrawables();
+ }
+ };
+
+ private Drawable drawable;
+ final protected WeakReference<TextView> viewRef;
+
+ @SuppressWarnings("deprecation")
+ public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
+ viewRef = new WeakReference<>(view);
+ drawable = null;
+ setBounds(0, 0, 0, 0);
+ drawableObservable.subscribe(this);
+ }
+
+ @Override
+ public final void draw(final Canvas canvas) {
+ if (drawable != null) {
+ drawable.draw(canvas);
+ }
+ }
+
+ @Override
+ public final void call(final Drawable newDrawable) {
+ final boolean needsRedraw;
+ synchronized (lock) {
+ // Check for emptyness inside the call to match the behaviour in redrawQueuedDrawables().
+ needsRedraw = REDRAW_QUEUE.isEmpty();
+ REDRAW_QUEUE.add(ImmutablePair.of(this, newDrawable));
+ }
+ if (needsRedraw) {
+ UI_WORKER.schedule(REDRAW_QUEUED_DRAWABLES);
+ }
+ }
+
+ /**
+ * Update the container with the new drawable. Called on the UI thread.
+ *
+ * @param newDrawable the new drawable
+ * @return the view to update or <tt>null</tt> if the view is not alive anymore
+ */
+ protected TextView updateDrawable(final Drawable newDrawable) {
+ setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
+ drawable = newDrawable;
+ return viewRef.get();
+ }
+
+ private static void redrawQueuedDrawables() {
+ if (!REDRAW_QUEUE.isEmpty()) {
+ // Add a small margin so that drawables arriving between the beginning of the allocation and the draining
+ // of the queue might be absorbed without reallocation.
+ final List<ImmutablePair<ContainerDrawable, Drawable>> toRedraw = new ArrayList<>(REDRAW_QUEUE.size() + 16);
+ synchronized (lock) {
+ // Empty the queue inside the lock to match the check done in call().
+ REDRAW_QUEUE.drainTo(toRedraw);
+ }
+ for (final ImmutablePair<ContainerDrawable, Drawable> redrawable : toRedraw) {
+ final TextView view = redrawable.left.updateDrawable(redrawable.right);
+ if (view != null) {
+ VIEWS.add(view);
+ }
+ }
+ for (final TextView view : VIEWS) {
+ view.setText(view.getText());
+ }
+ VIEWS.clear();
+ }
+ }
+
+ }
+
+ /**
+ * Image that automatically scales to fit a line of text in the containing {@link TextView}.
+ */
+ public final static class LineHeightContainerDrawable extends ContainerDrawable {
+ public LineHeightContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
+ super(view, drawableObservable);
+ }
+
+ @Override
+ protected TextView updateDrawable(final Drawable newDrawable) {
+ final TextView view = super.updateDrawable(newDrawable);
+ if (view != null) {
+ setBounds(scaleImageToLineHeight(newDrawable, view));
+ }
+ return view;
+ }
+ }
+
+ public static boolean canBeOpenedExternally(final String source) {
+ return !containsPattern(source, NO_EXTERNAL);
+ }
+
+ @NonNull
+ public static Rect scaleImageToLineHeight(final Drawable drawable, final TextView view) {
+ final int lineHeight = (int) (view.getLineHeight() * 0.8);
+ final int width = drawable.getIntrinsicWidth() * lineHeight / drawable.getIntrinsicHeight();
+ return new Rect(0, 0, width, lineHeight);
+ }
+
+ @Nullable
+ public static Bitmap convertToBitmap(final Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ // handle solid colors, which have no width
+ int width = drawable.getIntrinsicWidth();
+ width = width > 0 ? width : 1;
+ int height = drawable.getIntrinsicHeight();
+ height = height > 0 ? height : 1;
+
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
index 97e2187..b54f5d4 100644
--- a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
+++ b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
@@ -1,274 +1,277 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.LogEntry;
-import cgeo.geocaching.R;
-import cgeo.geocaching.Trackable;
-import cgeo.geocaching.connector.ConnectorFactory;
-import cgeo.geocaching.connector.IConnector;
-import cgeo.geocaching.connector.capability.ILogin;
-import cgeo.geocaching.settings.Settings;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Provides all the available templates for logging.
- *
- */
-public final class LogTemplateProvider {
-
- private LogTemplateProvider() {
- // utility class
- }
-
- /**
- * Context aware data container for log templates.
- * <p>
- * Some log templates need additional information. To provide that information, it can be encapsulated in this log
- * context.
- * </p>
- *
- */
- public static class LogContext {
- private Geocache cache;
- private Trackable trackable;
- private boolean offline = false;
- private final LogEntry logEntry;
-
- public LogContext(final Geocache cache, final LogEntry logEntry) {
- this(cache, logEntry, false);
- }
-
- public LogContext(final Trackable trackable, final LogEntry logEntry) {
- this.trackable = trackable;
- this.logEntry = logEntry;
- }
-
- public LogContext(final Geocache cache, final LogEntry logEntry, final boolean offline) {
- this.cache = cache;
- this.offline = offline;
- this.logEntry = logEntry;
- }
-
- public final Geocache getCache() {
- return cache;
- }
-
- public final Trackable getTrackable() {
- return trackable;
- }
-
- public final boolean isOffline() {
- return offline;
- }
-
- public final LogEntry getLogEntry() {
- return logEntry;
- }
- }
-
- public abstract static class LogTemplate {
- private final String template;
- private final int resourceId;
-
- protected LogTemplate(final String template, final int resourceId) {
- this.template = template;
- this.resourceId = resourceId;
- }
-
- public abstract String getValue(LogContext context);
-
- public final int getResourceId() {
- return resourceId;
- }
-
- public final int getItemId() {
- return template.hashCode();
- }
-
- public final String getTemplateString() {
- return template;
- }
-
- private final String apply(final String input, final LogContext context) {
- final String bracketedTemplate = "[" + template + "]";
-
- // check containment first to not unconditionally call the getValue(...) method
- if (input.contains(bracketedTemplate)) {
- return StringUtils.replace(input, bracketedTemplate, getValue(context));
- }
- return input;
- }
- }
-
- /**
- * @return all templates, but not the signature template itself
- */
- public static List<LogTemplate> getTemplatesWithoutSignature() {
- final List<LogTemplate> templates = new ArrayList<>();
- templates.add(new LogTemplate("DATE", R.string.init_signature_template_date) {
-
- @Override
- public String getValue(final LogContext context) {
- return Formatter.formatFullDate(System.currentTimeMillis());
- }
- });
- templates.add(new LogTemplate("TIME", R.string.init_signature_template_time) {
-
- @Override
- public String getValue(final LogContext context) {
- return Formatter.formatTime(System.currentTimeMillis());
- }
- });
- templates.add(new LogTemplate("DATETIME", R.string.init_signature_template_datetime) {
-
- @Override
- public String getValue(final LogContext context) {
- final long currentTime = System.currentTimeMillis();
- return Formatter.formatFullDate(currentTime) + " " + Formatter.formatTime(currentTime);
- }
- });
- templates.add(new LogTemplate("USER", R.string.init_signature_template_user) {
-
- @Override
- public String getValue(final LogContext context) {
- final Geocache cache = context.getCache();
- if (cache != null) {
- final IConnector connector = ConnectorFactory.getConnector(cache);
- if (connector instanceof ILogin) {
- return ((ILogin) connector).getUserName();
- }
- }
- return Settings.getUsername();
- }
- });
- templates.add(new LogTemplate("NUMBER", R.string.init_signature_template_number) {
-
- @Override
- public String getValue(final LogContext context) {
- final Geocache cache = context.getCache();
- if (cache == null) {
- return StringUtils.EMPTY;
- }
-
- int current = 0;
- final IConnector connector = ConnectorFactory.getConnector(cache);
- if (connector instanceof ILogin) {
- current = ((ILogin) connector).getCachesFound();
- }
-
- // try updating the login information, if the counter is zero
- if (current == 0) {
- if (context.isOffline()) {
- return StringUtils.EMPTY;
- }
- if (connector instanceof ILogin) {
- ((ILogin) connector).login(null, null);
- current = ((ILogin) connector).getCachesFound();
- }
- }
-
- if (current >= 0) {
- return String.valueOf(current + 1);
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("OWNER", R.string.init_signature_template_owner) {
-
- @Override
- public String getValue(final LogContext context) {
- final Trackable trackable = context.getTrackable();
- if (trackable != null) {
- return trackable.getOwner();
- }
- final Geocache cache = context.getCache();
- if (cache != null) {
- return cache.getOwnerDisplayName();
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("NAME", R.string.init_signature_template_name) {
- @Override
- public String getValue(final LogContext context) {
- final Trackable trackable = context.getTrackable();
- if (trackable != null) {
- return trackable.getName();
- }
- final Geocache cache = context.getCache();
- if (cache != null) {
- return cache.getName();
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("URL", R.string.init_signature_template_url) {
-
- @Override
- public String getValue(final LogContext context) {
- final Trackable trackable = context.getTrackable();
- if (trackable != null) {
- return trackable.getUrl();
- }
- final Geocache cache = context.getCache();
- if (cache != null) {
- return StringUtils.defaultString(cache.getUrl());
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("LOG", R.string.init_signature_template_log) {
- @Override
- public String getValue(final LogContext context) {
- final LogEntry logEntry = context.getLogEntry();
- if (logEntry != null) {
- return logEntry.getDisplayText();
- }
- return StringUtils.EMPTY;
- }
- });
- return templates;
- }
-
- /**
- * @return all templates, including the signature template
- */
- public static List<LogTemplate> getTemplatesWithSignature() {
- final List<LogTemplate> templates = getTemplatesWithoutSignature();
- templates.add(new LogTemplate("SIGNATURE", R.string.init_signature) {
- @Override
- public String getValue(final LogContext context) {
- final String nestedTemplate = StringUtils.defaultString(Settings.getSignature());
- if (StringUtils.contains(nestedTemplate, "SIGNATURE")) {
- return "invalid signature template";
- }
- return applyTemplates(nestedTemplate, context);
- }
- });
- return templates;
- }
-
- public static LogTemplate getTemplate(final int itemId) {
- for (final LogTemplate template : getTemplatesWithSignature()) {
- if (template.getItemId() == itemId) {
- return template;
- }
- }
- return null;
- }
-
- public static String applyTemplates(final String signature, final LogContext context) {
- if (signature == null) {
- return StringUtils.EMPTY;
- }
- String result = signature;
- for (final LogTemplate template : getTemplatesWithSignature()) {
- result = template.apply(result, context);
- }
- return result;
- }
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.LogEntry;
+import cgeo.geocaching.R;
+import cgeo.geocaching.Trackable;
+import cgeo.geocaching.connector.ConnectorFactory;
+import cgeo.geocaching.connector.IConnector;
+import cgeo.geocaching.connector.capability.ILogin;
+import cgeo.geocaching.settings.Settings;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides all the available templates for logging.
+ *
+ */
+public final class LogTemplateProvider {
+
+ private LogTemplateProvider() {
+ // utility class
+ }
+
+ /**
+ * Context aware data container for log templates.
+ * <p>
+ * Some log templates need additional information. To provide that information, it can be encapsulated in this log
+ * context.
+ * </p>
+ *
+ */
+ public static class LogContext {
+ private Geocache cache;
+ private Trackable trackable;
+ private boolean offline = false;
+ private final LogEntry logEntry;
+
+ public LogContext(final Geocache cache, final LogEntry logEntry) {
+ this(cache, logEntry, false);
+ }
+
+ public LogContext(final Trackable trackable, final LogEntry logEntry) {
+ this.trackable = trackable;
+ this.logEntry = logEntry;
+ }
+
+ public LogContext(final Geocache cache, final LogEntry logEntry, final boolean offline) {
+ this.cache = cache;
+ this.offline = offline;
+ this.logEntry = logEntry;
+ }
+
+ public final Geocache getCache() {
+ return cache;
+ }
+
+ public final Trackable getTrackable() {
+ return trackable;
+ }
+
+ public final boolean isOffline() {
+ return offline;
+ }
+
+ public final LogEntry getLogEntry() {
+ return logEntry;
+ }
+ }
+
+ public abstract static class LogTemplate {
+ private final String template;
+ private final int resourceId;
+
+ protected LogTemplate(final String template, final int resourceId) {
+ this.template = template;
+ this.resourceId = resourceId;
+ }
+
+ public abstract String getValue(LogContext context);
+
+ public final int getResourceId() {
+ return resourceId;
+ }
+
+ public final int getItemId() {
+ return template.hashCode();
+ }
+
+ public final String getTemplateString() {
+ return template;
+ }
+
+ @NonNull
+ private final String apply(@NonNull final String input, final LogContext context) {
+ final String bracketedTemplate = "[" + template + "]";
+
+ // check containment first to not unconditionally call the getValue(...) method
+ if (input.contains(bracketedTemplate)) {
+ return StringUtils.replace(input, bracketedTemplate, getValue(context));
+ }
+ return input;
+ }
+ }
+
+ /**
+ * @return all templates, but not the signature template itself
+ */
+ @NonNull
+ public static List<LogTemplate> getTemplatesWithoutSignature() {
+ final List<LogTemplate> templates = new ArrayList<>();
+ templates.add(new LogTemplate("DATE", R.string.init_signature_template_date) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ return Formatter.formatFullDate(System.currentTimeMillis());
+ }
+ });
+ templates.add(new LogTemplate("TIME", R.string.init_signature_template_time) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ return Formatter.formatTime(System.currentTimeMillis());
+ }
+ });
+ templates.add(new LogTemplate("DATETIME", R.string.init_signature_template_datetime) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final long currentTime = System.currentTimeMillis();
+ return Formatter.formatFullDate(currentTime) + " " + Formatter.formatTime(currentTime);
+ }
+ });
+ templates.add(new LogTemplate("USER", R.string.init_signature_template_user) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ final IConnector connector = ConnectorFactory.getConnector(cache);
+ if (connector instanceof ILogin) {
+ return ((ILogin) connector).getUserName();
+ }
+ }
+ return Settings.getUsername();
+ }
+ });
+ templates.add(new LogTemplate("NUMBER", R.string.init_signature_template_number) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Geocache cache = context.getCache();
+ if (cache == null) {
+ return StringUtils.EMPTY;
+ }
+
+ int current = 0;
+ final IConnector connector = ConnectorFactory.getConnector(cache);
+ if (connector instanceof ILogin) {
+ current = ((ILogin) connector).getCachesFound();
+ }
+
+ // try updating the login information, if the counter is zero
+ if (current == 0) {
+ if (context.isOffline()) {
+ return StringUtils.EMPTY;
+ }
+ if (connector instanceof ILogin) {
+ ((ILogin) connector).login(null, null);
+ current = ((ILogin) connector).getCachesFound();
+ }
+ }
+
+ if (current >= 0) {
+ return String.valueOf(current + 1);
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("OWNER", R.string.init_signature_template_owner) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Trackable trackable = context.getTrackable();
+ if (trackable != null) {
+ return trackable.getOwner();
+ }
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ return cache.getOwnerDisplayName();
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("NAME", R.string.init_signature_template_name) {
+ @Override
+ public String getValue(final LogContext context) {
+ final Trackable trackable = context.getTrackable();
+ if (trackable != null) {
+ return trackable.getName();
+ }
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ return cache.getName();
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("URL", R.string.init_signature_template_url) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Trackable trackable = context.getTrackable();
+ if (trackable != null) {
+ return trackable.getUrl();
+ }
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ return StringUtils.defaultString(cache.getUrl());
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("LOG", R.string.init_signature_template_log) {
+ @Override
+ public String getValue(final LogContext context) {
+ final LogEntry logEntry = context.getLogEntry();
+ if (logEntry != null) {
+ return logEntry.getDisplayText();
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ return templates;
+ }
+
+ /**
+ * @return all templates, including the signature template
+ */
+ @NonNull
+ public static List<LogTemplate> getTemplatesWithSignature() {
+ final List<LogTemplate> templates = getTemplatesWithoutSignature();
+ templates.add(new LogTemplate("SIGNATURE", R.string.init_signature) {
+ @Override
+ public String getValue(final LogContext context) {
+ final String nestedTemplate = Settings.getSignature();
+ if (StringUtils.contains(nestedTemplate, "SIGNATURE")) {
+ return "invalid signature template";
+ }
+ return applyTemplates(nestedTemplate, context);
+ }
+ });
+ return templates;
+ }
+
+ @Nullable
+ public static LogTemplate getTemplate(final int itemId) {
+ for (final LogTemplate template : getTemplatesWithSignature()) {
+ if (template.getItemId() == itemId) {
+ return template;
+ }
+ }
+ return null;
+ }
+
+ public static String applyTemplates(@NonNull final String signature, final LogContext context) {
+ String result = signature;
+ for (final LogTemplate template : getTemplatesWithSignature()) {
+ result = template.apply(result, context);
+ }
+ return result;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/MapUtils.java b/main/src/cgeo/geocaching/utils/MapUtils.java
index 11a705d..9f15bf3 100644
--- a/main/src/cgeo/geocaching/utils/MapUtils.java
+++ b/main/src/cgeo/geocaching/utils/MapUtils.java
@@ -1,287 +1,293 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.R;
-import cgeo.geocaching.Waypoint;
-import cgeo.geocaching.compatibility.Compatibility;
-import cgeo.geocaching.enumerations.CacheListType;
-import cgeo.geocaching.enumerations.LogType;
-
-import org.apache.commons.lang3.builder.HashCodeBuilder;
-
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.support.annotation.Nullable;
-import android.util.SparseArray;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-public final class MapUtils {
-
- // data for overlays
- private static final int[][] INSET_RELIABLE = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }; // center, 33x40 / 45x51 / 60x68 / 90x102 / 120x136
- private static final int[][] INSET_TYPE = { { 5, 8, 6, 10 }, { 4, 4, 4, 11 }, { 6, 6, 6, 14 }, { 9, 9, 9, 21 }, { 12, 12, 12, 28 } }; // center, 22x22 / 36x36
- private static final int[][] INSET_OWN = { { 21, 0, 0, 28 }, { 29, 0, 0, 35 }, { 40, 0, 0, 48 }, { 58, 0, 0, 70 }, { 80, 0, 0, 96 } }; // top right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
- private static final int[][] INSET_FOUND = { { 0, 0, 21, 28 }, { 0, 0, 29, 35 }, { 0, 0, 40, 48 }, { 0, 0, 58, 70 }, { 0, 0, 80, 96 } }; // top left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
- private static final int[][] INSET_USERMODIFIEDCOORDS = { { 21, 28, 0, 0 }, { 29, 35, 0, 0 }, { 40, 48, 0, 0 }, { 58, 70, 0, 0 }, { 80, 96, 0, 0 } }; // bottom right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
- private static final int[][] INSET_PERSONALNOTE = { { 0, 28, 21, 0 }, { 0, 35, 29, 0 }, { 0, 48, 40, 0 }, { 0, 70, 58, 0 }, { 0, 96, 80, 0 } }; // bottom left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
-
- private static final SparseArray<LayerDrawable> overlaysCache = new SparseArray<>();
-
- private MapUtils() {
- // Do not instantiate
- }
-
- /**
- * Obtain the drawable for a given cache, with background circle.
- *
- * @param res
- * the resources to use
- * @param cache
- * the cache to build the drawable for
- * @return
- * a drawable representing the current cache status
- */
- public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache) {
- return getCacheMarker(res, cache, null);
- }
-
- /**
- * Obtain the drawable for a given cache.
- * Return a drawable from the cache, if a similar drawable was already generated.
- *
- * cacheListType should be Null if the requesting activity is Map.
- *
- * @param res
- * the resources to use
- * @param cache
- * the cache to build the drawable for
- * @param cacheListType
- * the current CacheListType or Null
- * @return
- * a drawable representing the current cache status
- */
- public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
- final int hashcode = new HashCodeBuilder()
- .append(cache.isReliableLatLon())
- .append(cache.getType().id)
- .append(cache.isDisabled() || cache.isArchived())
- .append(cache.getMapMarkerId())
- .append(cache.isOwner())
- .append(cache.isFound())
- .append(cache.hasUserModifiedCoords())
- .append(cache.getPersonalNote())
- .append(cache.isLogOffline())
- .append(cache.getListId() > 0)
- .append(cache.getOfflineLogType())
- .append(showBackground(cacheListType))
- .append(showFloppyOverlay(cacheListType))
- .toHashCode();
-
- synchronized (overlaysCache) {
- LayerDrawable drawable = overlaysCache.get(hashcode);
- if (drawable == null) {
- drawable = createCacheMarker(res, cache, cacheListType);
- overlaysCache.put(hashcode, drawable);
- }
- return drawable;
- }
- }
-
- /**
- * Obtain the drawable for a given waypoint.
- * Return a drawable from the cache, if a similar drawable was already generated.
- *
- * @param res
- * the resources to use
- * @param waypoint
- * the waypoint to build the drawable for
- * @return
- * a drawable representing the current waypoint status
- */
- public static LayerDrawable getWaypointMarker(final Resources res, final Waypoint waypoint) {
- final int hashcode = new HashCodeBuilder()
- .append(waypoint.isVisited())
- .append(waypoint.getWaypointType().id)
- .toHashCode();
-
- synchronized (overlaysCache) {
- LayerDrawable drawable = overlaysCache.get(hashcode);
- if (drawable == null) {
- drawable = createWaypointMarker(res, waypoint);
- overlaysCache.put(hashcode, drawable);
- }
- return drawable;
- }
- }
-
- /**
- * Build the drawable for a given waypoint.
- *
- * @param res
- * the resources to use
- * @param waypoint
- * the waypoint to build the drawable for
- * @return
- * a drawable representing the current waypoint status
- */
- private static LayerDrawable createWaypointMarker(final Resources res, final Waypoint waypoint) {
- final Drawable marker = Compatibility.getDrawable(res, !waypoint.isVisited() ? R.drawable.marker : R.drawable.marker_transparent);
- final Drawable[] layers = {
- marker,
- Compatibility.getDrawable(res, waypoint.getWaypointType().markerId)
- };
- final LayerDrawable drawable = new LayerDrawable(layers);
- final int resolution = calculateResolution(marker);
- drawable.setLayerInset(1, INSET_TYPE[resolution][0], INSET_TYPE[resolution][1], INSET_TYPE[resolution][2], INSET_TYPE[resolution][3]);
- return drawable;
- }
-
- /**
- * Clear the cache of drawable items.
- */
- public static void clearCachedItems() {
- synchronized (overlaysCache) {
- overlaysCache.clear();
- }
- }
-
- /**
- * Build the drawable for a given cache.
- *
- * @param res
- * the resources to use
- * @param cache
- * the cache to build the drawable for
- * @param cacheListType
- * the current CacheListType or Null
- * @return
- * a drawable representing the current cache status
- */
- private static LayerDrawable createCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
- // Set initial capacities to the maximum of layers and insets to avoid dynamic reallocation
- final List<Drawable> layers = new ArrayList<>(9);
- final List<int[]> insets = new ArrayList<>(8);
-
- // background: disabled or not
- final Drawable marker = Compatibility.getDrawable(res, cache.getMapMarkerId());
- // Show the background circle only on map
- if (showBackground(cacheListType)) {
- layers.add(marker);
- }
- final int resolution = calculateResolution(marker);
- // reliable or not
- if (!cache.isReliableLatLon()) {
- insets.add(INSET_RELIABLE[resolution]);
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_notreliable));
- }
- // cache type
- layers.add(Compatibility.getDrawable(res, cache.getType().markerId));
- insets.add(INSET_TYPE[resolution]);
- // own
- if (cache.isOwner()) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_own));
- insets.add(INSET_OWN[resolution]);
- // if not, checked if stored
- } else if (cache.getListId() > 0 && showFloppyOverlay(cacheListType)) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_stored));
- insets.add(INSET_OWN[resolution]);
- }
- // found
- if (cache.isFound()) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_found));
- insets.add(INSET_FOUND[resolution]);
- // if not, perhaps logged offline
- } else if (cache.isLogOffline()) {
- final LogType offlineLogType = cache.getOfflineLogType();
- if (offlineLogType == null) {
- // Default, backward compatible
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_found_offline));
- } else {
- layers.add(Compatibility.getDrawable(res, offlineLogType.getOfflineLogOverlay()));
- }
- insets.add(INSET_FOUND[resolution]);
- }
- // user modified coords
- if (cache.hasUserModifiedCoords()) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_usermodifiedcoords));
- insets.add(driftBottomItems(INSET_USERMODIFIEDCOORDS, resolution, cacheListType));
- }
- // personal note
- if (cache.getPersonalNote() != null) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_personalnote));
- insets.add(driftBottomItems(INSET_PERSONALNOTE, resolution, cacheListType));
- }
-
- final LayerDrawable ld = new LayerDrawable(layers.toArray(new Drawable[layers.size()]));
-
- int index = showBackground(cacheListType) ? 1 : 0;
- for (final int[] inset : insets) {
- ld.setLayerInset(index++, inset[0], inset[1], inset[2], inset[3]);
- }
-
- return ld;
- }
-
- /**
- * Get the resolution index used for positionning the overlays elements.
- *
- * @param marker
- * The Drawable reference
- * @return
- * an index for the overlays positions
- */
- private static int calculateResolution(final Drawable marker) {
- return marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0;
- }
-
- /**
- * Calculate a new position for the bottom line overlay items, when there is no background circle.
- *
- * @param inset
- * Original inset position
- * @param resolution
- * The current item resolution
- * @param cacheListType
- * The current CacheListType
- * @return
- * The new drifted inset position
- */
- private static int[] driftBottomItems(final int[][] inset, final int resolution, @Nullable final CacheListType cacheListType) {
- // Do not drift in when background is displayed
- if (showBackground(cacheListType)) {
- return inset[resolution];
- }
- final int[] newPosition = Arrays.copyOf(inset[resolution], 4);
- newPosition[1] -= INSET_TYPE[resolution][3];
- newPosition[3] += INSET_TYPE[resolution][3];
- return newPosition;
- }
-
- /**
- * Conditionnal expression to choose if we need the background circle or not.
- *
- * @param cacheListType
- * The cache list currently used
- * @return
- * True if the background circle should be displayed
- */
- private static boolean showBackground(final CacheListType cacheListType) {
- return cacheListType == null;
- }
-
- /**
- * Conditionnal expression to choose if we need the floppy overlay or not.
- *
- * @param cacheListType
- * The cache list currently used
- * @return
- * True if the floppy overlay should be displayed
- */
- private static boolean showFloppyOverlay(final CacheListType cacheListType) {
- return cacheListType == null || cacheListType != CacheListType.OFFLINE;
- }
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.R;
+import cgeo.geocaching.Waypoint;
+import cgeo.geocaching.compatibility.Compatibility;
+import cgeo.geocaching.enumerations.CacheListType;
+import cgeo.geocaching.enumerations.LogType;
+
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.support.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class MapUtils {
+
+ // data for overlays
+ private static final int[][] INSET_RELIABLE = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }; // center, 33x40 / 45x51 / 60x68 / 90x102 / 120x136
+ private static final int[][] INSET_TYPE = { { 5, 8, 6, 10 }, { 4, 4, 4, 11 }, { 6, 6, 6, 14 }, { 9, 9, 9, 21 }, { 12, 12, 12, 28 } }; // center, 22x22 / 36x36
+ private static final int[][] INSET_OWN = { { 21, 0, 0, 28 }, { 29, 0, 0, 35 }, { 40, 0, 0, 48 }, { 58, 0, 0, 70 }, { 80, 0, 0, 96 } }; // top right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+ private static final int[][] INSET_FOUND = { { 0, 0, 21, 28 }, { 0, 0, 29, 35 }, { 0, 0, 40, 48 }, { 0, 0, 58, 70 }, { 0, 0, 80, 96 } }; // top left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+ private static final int[][] INSET_USERMODIFIEDCOORDS = { { 21, 28, 0, 0 }, { 29, 35, 0, 0 }, { 40, 48, 0, 0 }, { 58, 70, 0, 0 }, { 80, 96, 0, 0 } }; // bottom right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+ private static final int[][] INSET_PERSONALNOTE = { { 0, 28, 21, 0 }, { 0, 35, 29, 0 }, { 0, 48, 40, 0 }, { 0, 70, 58, 0 }, { 0, 96, 80, 0 } }; // bottom left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+
+ private static final SparseArray<LayerDrawable> overlaysCache = new SparseArray<>();
+
+ private MapUtils() {
+ // Do not instantiate
+ }
+
+ /**
+ * Obtain the drawable for a given cache, with background circle.
+ *
+ * @param res
+ * the resources to use
+ * @param cache
+ * the cache to build the drawable for
+ * @return
+ * a drawable representing the current cache status
+ */
+ @NonNull
+ public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache) {
+ return getCacheMarker(res, cache, null);
+ }
+
+ /**
+ * Obtain the drawable for a given cache.
+ * Return a drawable from the cache, if a similar drawable was already generated.
+ *
+ * cacheListType should be Null if the requesting activity is Map.
+ *
+ * @param res
+ * the resources to use
+ * @param cache
+ * the cache to build the drawable for
+ * @param cacheListType
+ * the current CacheListType or Null
+ * @return
+ * a drawable representing the current cache status
+ */
+ @NonNull
+ public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
+ final int hashcode = new HashCodeBuilder()
+ .append(cache.isReliableLatLon())
+ .append(cache.getType().id)
+ .append(cache.isDisabled() || cache.isArchived())
+ .append(cache.getMapMarkerId())
+ .append(cache.isOwner())
+ .append(cache.isFound())
+ .append(cache.hasUserModifiedCoords())
+ .append(cache.getPersonalNote())
+ .append(cache.isLogOffline())
+ .append(cache.getListId() > 0)
+ .append(cache.getOfflineLogType())
+ .append(showBackground(cacheListType))
+ .append(showFloppyOverlay(cacheListType))
+ .toHashCode();
+
+ synchronized (overlaysCache) {
+ LayerDrawable drawable = overlaysCache.get(hashcode);
+ if (drawable == null) {
+ drawable = createCacheMarker(res, cache, cacheListType);
+ overlaysCache.put(hashcode, drawable);
+ }
+ return drawable;
+ }
+ }
+
+ /**
+ * Obtain the drawable for a given waypoint.
+ * Return a drawable from the cache, if a similar drawable was already generated.
+ *
+ * @param res
+ * the resources to use
+ * @param waypoint
+ * the waypoint to build the drawable for
+ * @return
+ * a drawable representing the current waypoint status
+ */
+ @NonNull
+ public static LayerDrawable getWaypointMarker(final Resources res, final Waypoint waypoint) {
+ final int hashcode = new HashCodeBuilder()
+ .append(waypoint.isVisited())
+ .append(waypoint.getWaypointType().id)
+ .toHashCode();
+
+ synchronized (overlaysCache) {
+ LayerDrawable drawable = overlaysCache.get(hashcode);
+ if (drawable == null) {
+ drawable = createWaypointMarker(res, waypoint);
+ overlaysCache.put(hashcode, drawable);
+ }
+ return drawable;
+ }
+ }
+
+ /**
+ * Build the drawable for a given waypoint.
+ *
+ * @param res
+ * the resources to use
+ * @param waypoint
+ * the waypoint to build the drawable for
+ * @return
+ * a drawable representing the current waypoint status
+ */
+ @NonNull
+ private static LayerDrawable createWaypointMarker(final Resources res, final Waypoint waypoint) {
+ final Drawable marker = Compatibility.getDrawable(res, !waypoint.isVisited() ? R.drawable.marker : R.drawable.marker_transparent);
+ final Drawable[] layers = {
+ marker,
+ Compatibility.getDrawable(res, waypoint.getWaypointType().markerId)
+ };
+ final LayerDrawable drawable = new LayerDrawable(layers);
+ final int resolution = calculateResolution(marker);
+ drawable.setLayerInset(1, INSET_TYPE[resolution][0], INSET_TYPE[resolution][1], INSET_TYPE[resolution][2], INSET_TYPE[resolution][3]);
+ return drawable;
+ }
+
+ /**
+ * Clear the cache of drawable items.
+ */
+ public static void clearCachedItems() {
+ synchronized (overlaysCache) {
+ overlaysCache.clear();
+ }
+ }
+
+ /**
+ * Build the drawable for a given cache.
+ *
+ * @param res
+ * the resources to use
+ * @param cache
+ * the cache to build the drawable for
+ * @param cacheListType
+ * the current CacheListType or Null
+ * @return
+ * a drawable representing the current cache status
+ */
+ @NonNull
+ private static LayerDrawable createCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
+ // Set initial capacities to the maximum of layers and insets to avoid dynamic reallocation
+ final List<Drawable> layers = new ArrayList<>(9);
+ final List<int[]> insets = new ArrayList<>(8);
+
+ // background: disabled or not
+ final Drawable marker = Compatibility.getDrawable(res, cache.getMapMarkerId());
+ // Show the background circle only on map
+ if (showBackground(cacheListType)) {
+ layers.add(marker);
+ }
+ final int resolution = calculateResolution(marker);
+ // reliable or not
+ if (!cache.isReliableLatLon()) {
+ insets.add(INSET_RELIABLE[resolution]);
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_notreliable));
+ }
+ // cache type
+ layers.add(Compatibility.getDrawable(res, cache.getType().markerId));
+ insets.add(INSET_TYPE[resolution]);
+ // own
+ if (cache.isOwner()) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_own));
+ insets.add(INSET_OWN[resolution]);
+ // if not, checked if stored
+ } else if (cache.getListId() > 0 && showFloppyOverlay(cacheListType)) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_stored));
+ insets.add(INSET_OWN[resolution]);
+ }
+ // found
+ if (cache.isFound()) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_found));
+ insets.add(INSET_FOUND[resolution]);
+ // if not, perhaps logged offline
+ } else if (cache.isLogOffline()) {
+ final LogType offlineLogType = cache.getOfflineLogType();
+ if (offlineLogType == null) {
+ // Default, backward compatible
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_found_offline));
+ } else {
+ layers.add(Compatibility.getDrawable(res, offlineLogType.getOfflineLogOverlay()));
+ }
+ insets.add(INSET_FOUND[resolution]);
+ }
+ // user modified coords
+ if (cache.hasUserModifiedCoords()) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_usermodifiedcoords));
+ insets.add(driftBottomItems(INSET_USERMODIFIEDCOORDS, resolution, cacheListType));
+ }
+ // personal note
+ if (cache.getPersonalNote() != null) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_personalnote));
+ insets.add(driftBottomItems(INSET_PERSONALNOTE, resolution, cacheListType));
+ }
+
+ final LayerDrawable ld = new LayerDrawable(layers.toArray(new Drawable[layers.size()]));
+
+ int index = showBackground(cacheListType) ? 1 : 0;
+ for (final int[] inset : insets) {
+ ld.setLayerInset(index++, inset[0], inset[1], inset[2], inset[3]);
+ }
+
+ return ld;
+ }
+
+ /**
+ * Get the resolution index used for positioning the overlays elements.
+ *
+ * @param marker
+ * The Drawable reference
+ * @return
+ * an index for the overlays positions
+ */
+ private static int calculateResolution(final Drawable marker) {
+ return marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0;
+ }
+
+ /**
+ * Calculate a new position for the bottom line overlay items, when there is no background circle.
+ *
+ * @param inset
+ * Original inset position
+ * @param resolution
+ * The current item resolution
+ * @param cacheListType
+ * The current CacheListType
+ * @return
+ * The new drifted inset position
+ */
+ private static int[] driftBottomItems(final int[][] inset, final int resolution, @Nullable final CacheListType cacheListType) {
+ // Do not drift in when background is displayed
+ if (showBackground(cacheListType)) {
+ return inset[resolution];
+ }
+ final int[] newPosition = Arrays.copyOf(inset[resolution], 4);
+ newPosition[1] -= INSET_TYPE[resolution][3];
+ newPosition[3] += INSET_TYPE[resolution][3];
+ return newPosition;
+ }
+
+ /**
+ * Conditional expression to choose if we need the background circle or not.
+ *
+ * @param cacheListType
+ * The cache list currently used
+ * @return
+ * True if the background circle should be displayed
+ */
+ private static boolean showBackground(final CacheListType cacheListType) {
+ return cacheListType == null;
+ }
+
+ /**
+ * Conditional expression to choose if we need the floppy overlay or not.
+ *
+ * @param cacheListType
+ * The cache list currently used
+ * @return
+ * True if the floppy overlay should be displayed
+ */
+ private static boolean showFloppyOverlay(final CacheListType cacheListType) {
+ return cacheListType == null || cacheListType != CacheListType.OFFLINE;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/MatcherWrapper.java b/main/src/cgeo/geocaching/utils/MatcherWrapper.java
index 733a18e..b3a361b 100644
--- a/main/src/cgeo/geocaching/utils/MatcherWrapper.java
+++ b/main/src/cgeo/geocaching/utils/MatcherWrapper.java
@@ -1,12 +1,13 @@
package cgeo.geocaching.utils;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* Wrapper around the regex {@link Matcher} class. This implementation optimizes the memory usage of the matched
* Strings.
@@ -46,6 +47,7 @@ public class MatcherWrapper {
* Do not change this method, even if Findbugs and other tools will report a violation for that line!
*
*/
+ @Nullable
@SuppressFBWarnings("DM_STRING_CTOR")
private static String newString(final String input) {
if (input == null) {