aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/utils
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/cgeo/geocaching/utils')
-rw-r--r--main/src/cgeo/geocaching/utils/AngleUtils.java43
-rw-r--r--main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java14
-rw-r--r--main/src/cgeo/geocaching/utils/BundleUtils.java4
-rw-r--r--main/src/cgeo/geocaching/utils/CalendarUtils.java (renamed from main/src/cgeo/geocaching/utils/DateUtils.java)23
-rw-r--r--main/src/cgeo/geocaching/utils/CancellableHandler.java2
-rw-r--r--main/src/cgeo/geocaching/utils/CheckerUtils.java35
-rw-r--r--main/src/cgeo/geocaching/utils/ClipboardUtils.java17
-rw-r--r--main/src/cgeo/geocaching/utils/CryptUtils.java84
-rw-r--r--main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java123
-rw-r--r--main/src/cgeo/geocaching/utils/DebugUtils.java7
-rw-r--r--main/src/cgeo/geocaching/utils/EditUtils.java6
-rw-r--r--main/src/cgeo/geocaching/utils/FileUtils.java61
-rw-r--r--main/src/cgeo/geocaching/utils/Formatter.java83
-rw-r--r--main/src/cgeo/geocaching/utils/HtmlUtils.java31
-rw-r--r--main/src/cgeo/geocaching/utils/ImageUtils.java180
-rw-r--r--main/src/cgeo/geocaching/utils/JsonUtils.java20
-rw-r--r--main/src/cgeo/geocaching/utils/LazyInitializedList.java2
-rw-r--r--main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java23
-rw-r--r--main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java89
-rw-r--r--main/src/cgeo/geocaching/utils/Log.java27
-rw-r--r--main/src/cgeo/geocaching/utils/LogTemplateProvider.java2
-rw-r--r--main/src/cgeo/geocaching/utils/MapUtils.java3
-rw-r--r--main/src/cgeo/geocaching/utils/MatcherWrapper.java16
-rw-r--r--main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java7
-rw-r--r--main/src/cgeo/geocaching/utils/ProcessUtils.java44
-rw-r--r--main/src/cgeo/geocaching/utils/RxUtils.java179
-rw-r--r--main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java30
-rw-r--r--main/src/cgeo/geocaching/utils/StartableHandlerThread.java80
-rw-r--r--main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java2
-rw-r--r--main/src/cgeo/geocaching/utils/TextUtils.java83
-rw-r--r--main/src/cgeo/geocaching/utils/UnknownTagsHandler.java14
-rw-r--r--main/src/cgeo/geocaching/utils/XmlUtils.java2
32 files changed, 876 insertions, 460 deletions
diff --git a/main/src/cgeo/geocaching/utils/AngleUtils.java b/main/src/cgeo/geocaching/utils/AngleUtils.java
index fdd9a9d..5ab2c75 100644
--- a/main/src/cgeo/geocaching/utils/AngleUtils.java
+++ b/main/src/cgeo/geocaching/utils/AngleUtils.java
@@ -1,7 +1,17 @@
package cgeo.geocaching.utils;
+import cgeo.geocaching.CgeoApplication;
+
+import android.content.Context;
+import android.view.Surface;
+import android.view.WindowManager;
+
public final class AngleUtils {
+ private static class WindowManagerHolder {
+ public static final WindowManager WINDOW_MANAGER = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE);
+ }
+
private AngleUtils() {
// Do not instantiate
}
@@ -27,4 +37,37 @@ public final class AngleUtils {
public static float normalize(final float angle) {
return (angle >= 0 ? angle : (360 - ((-angle) % 360))) % 360;
}
+
+ public static int getRotationOffset() {
+ switch (WindowManagerHolder.WINDOW_MANAGER.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Take the phone rotation (through a given activity) in account and adjust the direction.
+ *
+ * @param direction the unadjusted direction in degrees, in the [0, 360[ range
+ * @return the adjusted direction in degrees, in the [0, 360[ range
+ */
+ public static float getDirectionNow(final float direction) {
+ return normalize(direction + getRotationOffset());
+ }
+
+ /**
+ * Reverse the phone rotation (through a given activity) in account and adjust the direction.
+ *
+ * @param direction the unadjusted direction in degrees, in the [0, 360[ range
+ * @return the adjusted direction in degrees, in the [0, 360[ range
+ */
+ public static float reverseDirectionNow(final float direction) {
+ return normalize(direction - getRotationOffset());
+ }
}
diff --git a/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java b/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java
index 3d2b2b1..c2edd24 100644
--- a/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java
+++ b/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java
@@ -14,8 +14,6 @@ import android.os.AsyncTask;
* automatically derived from the number of {@code Params} given to the task in {@link #execute(Object...)}.
* </p>
*
- * @param <Params>
- * @param <Result>
*/
public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Params, Integer, Result> {
@@ -28,9 +26,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa
/**
* Creates an AsyncTask with progress dialog.
*
- * @param activity
- * @param progressTitle
- * @param progressMessage
*/
public AsyncTaskWithProgress(final Activity activity, final String progressTitle, final String progressMessage) {
this(activity, progressTitle, progressMessage, false);
@@ -39,8 +34,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa
/**
* Creates an AsyncTask with progress dialog.
*
- * @param activity
- * @param progressTitle
*/
public AsyncTaskWithProgress(final Activity activity, final String progressTitle) {
this(activity, progressTitle, null);
@@ -49,9 +42,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa
/**
* Creates an AsyncTask with progress dialog.
*
- * @param activity
- * @param progressTitle
- * @param progressMessage
*/
public AsyncTaskWithProgress(final Activity activity, final String progressTitle, final String progressMessage, final boolean indeterminate) {
this.activity = activity;
@@ -63,8 +53,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa
/**
* Creates an AsyncTask with progress dialog.
*
- * @param activity
- * @param progressTitle
*/
public AsyncTaskWithProgress(final Activity activity, final String progressTitle, final boolean indeterminate) {
this(activity, progressTitle, null, indeterminate);
@@ -102,6 +90,8 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa
* This method should typically be overridden by sub classes instead of {@link #onPostExecute(Object)}.
*
* @param result
+ * The result of the operation computed by {@link #doInBackground(Object...)}.
+ *
*/
protected void onPostExecuteInternal(final Result result) {
// empty by default
diff --git a/main/src/cgeo/geocaching/utils/BundleUtils.java b/main/src/cgeo/geocaching/utils/BundleUtils.java
index 9c4255b..e61e45e 100644
--- a/main/src/cgeo/geocaching/utils/BundleUtils.java
+++ b/main/src/cgeo/geocaching/utils/BundleUtils.java
@@ -7,8 +7,8 @@ import android.os.Bundle;
public class BundleUtils {
@NonNull
- public static String getString(Bundle bundle, @NonNull String key, @NonNull String defaultValue) {
- String res = bundle.getString(key);
+ public static String getString(final Bundle bundle, @NonNull final String key, @NonNull final String defaultValue) {
+ final String res = bundle.getString(key);
if (res != null) {
return res;
}
diff --git a/main/src/cgeo/geocaching/utils/DateUtils.java b/main/src/cgeo/geocaching/utils/CalendarUtils.java
index 9aa4222..ed3b18c 100644
--- a/main/src/cgeo/geocaching/utils/DateUtils.java
+++ b/main/src/cgeo/geocaching/utils/CalendarUtils.java
@@ -5,13 +5,13 @@ import cgeo.geocaching.Geocache;
import java.util.Calendar;
import java.util.Date;
-public final class DateUtils {
+public final class CalendarUtils {
- private DateUtils() {
+ private CalendarUtils() {
// utility class
}
- public static int daysSince(long date) {
+ public static int daysSince(final long date) {
final Calendar logDate = Calendar.getInstance();
logDate.setTimeInMillis(date);
logDate.set(Calendar.SECOND, 0);
@@ -24,12 +24,27 @@ public final class DateUtils {
return (int) Math.round((today.getTimeInMillis() - logDate.getTimeInMillis()) / 86400000d);
}
+ public static int daysSince(final Calendar date) {
+ return daysSince(date.getTimeInMillis());
+ }
+
public static boolean isPastEvent(final Geocache cache) {
if (!cache.isEventCache()) {
return false;
}
final Date hiddenDate = cache.getHiddenDate();
- return hiddenDate != null && DateUtils.daysSince(hiddenDate.getTime()) > 0;
+ return hiddenDate != null && CalendarUtils.daysSince(hiddenDate.getTime()) > 0;
+ }
+
+ /**
+ * Return whether the given date is *more* than 1 day away. We allow 1 day to be "present time" to compensate for
+ * potential timezone issues.
+ *
+ * @param date
+ * the date
+ */
+ public static boolean isFuture(final Calendar date) {
+ return daysSince(date) < -1;
}
}
diff --git a/main/src/cgeo/geocaching/utils/CancellableHandler.java b/main/src/cgeo/geocaching/utils/CancellableHandler.java
index 3ed233a..7b7aa6f 100644
--- a/main/src/cgeo/geocaching/utils/CancellableHandler.java
+++ b/main/src/cgeo/geocaching/utils/CancellableHandler.java
@@ -17,7 +17,7 @@ public abstract class CancellableHandler extends Handler {
public static final int DONE = -1000;
protected static final int UPDATE_LOAD_PROGRESS_DETAIL = 42186;
private volatile boolean cancelled = false;
- private static CompositeSubscription subscriptions = new CompositeSubscription();
+ private final CompositeSubscription subscriptions = new CompositeSubscription();
private static class CancelHolder {
final Object payload;
diff --git a/main/src/cgeo/geocaching/utils/CheckerUtils.java b/main/src/cgeo/geocaching/utils/CheckerUtils.java
new file mode 100644
index 0000000..39ef078
--- /dev/null
+++ b/main/src/cgeo/geocaching/utils/CheckerUtils.java
@@ -0,0 +1,35 @@
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.Geocache;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.util.Patterns;
+
+import java.util.regex.Matcher;
+
+public final class CheckerUtils {
+ private static final String[] CHECKERS = new String[] { "geocheck.org", "geochecker.com", "certitudes.org" };
+
+ private CheckerUtils() {
+ // utility class
+ }
+
+ @Nullable
+ public static String getCheckerUrl(@NonNull final Geocache cache) {
+ final String description = cache.getDescription();
+ final Matcher matcher = Patterns.WEB_URL.matcher(description);
+ while (matcher.find()) {
+ final String url = matcher.group();
+ for (final String checker : CHECKERS) {
+ if (StringUtils.containsIgnoreCase(url, checker)) {
+ return StringEscapeUtils.unescapeHtml4(url);
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/ClipboardUtils.java b/main/src/cgeo/geocaching/utils/ClipboardUtils.java
index 77250f3..fb30886 100644
--- a/main/src/cgeo/geocaching/utils/ClipboardUtils.java
+++ b/main/src/cgeo/geocaching/utils/ClipboardUtils.java
@@ -2,6 +2,8 @@ package cgeo.geocaching.utils;
import cgeo.geocaching.CgeoApplication;
+import org.eclipse.jdt.annotation.Nullable;
+
import android.content.Context;
/**
@@ -9,7 +11,6 @@ import android.content.Context;
* This class uses the deprecated function ClipboardManager.setText(CharSequence).
* API 11 introduced setPrimaryClip(ClipData)
*/
-@SuppressWarnings("deprecation")
public final class ClipboardUtils {
private ClipboardUtils() {
@@ -22,10 +23,24 @@ public final class ClipboardUtils {
* @param text
* The text to place in the clipboard.
*/
+ @SuppressWarnings("deprecation")
public static void copyToClipboard(final CharSequence text) {
// fully qualified name used here to avoid buggy deprecation warning (of javac) on the import statement
final android.text.ClipboardManager clipboard = (android.text.ClipboardManager) CgeoApplication.getInstance().getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(text);
}
+ /**
+ * get clipboard content
+ *
+ */
+ @SuppressWarnings("deprecation")
+ @Nullable
+ public static String getText() {
+ // fully qualified name used here to avoid buggy deprecation warning (of javac) on the import statement
+ final android.text.ClipboardManager clipboard = (android.text.ClipboardManager) CgeoApplication.getInstance().getSystemService(Context.CLIPBOARD_SERVICE);
+ final CharSequence text = clipboard.getText();
+ return text != null ? text.toString() : null;
+ }
+
}
diff --git a/main/src/cgeo/geocaching/utils/CryptUtils.java b/main/src/cgeo/geocaching/utils/CryptUtils.java
index 815c2f4..4aec509 100644
--- a/main/src/cgeo/geocaching/utils/CryptUtils.java
+++ b/main/src/cgeo/geocaching/utils/CryptUtils.java
@@ -1,6 +1,5 @@
package cgeo.geocaching.utils;
-
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
@@ -23,28 +22,29 @@ public final class CryptUtils {
// utility class
}
- private static char[] base64map1 = new char[64];
- private static byte[] base64map2 = new byte[128];
+ 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;
+ BASE64MAP1[i++] = c;
}
for (char c = 'a'; c <= 'z'; c++) {
- base64map1[i++] = c;
+ BASE64MAP1[i++] = c;
}
for (char c = '0'; c <= '9'; c++) {
- base64map1[i++] = c;
+ BASE64MAP1[i++] = c;
}
- base64map1[i++] = '+';
- base64map1[i++] = '/';
+ BASE64MAP1[i++] = '+';
+ BASE64MAP1[i++] = '/';
- for (i = 0; i < base64map2.length; i++) {
- base64map2[i] = -1;
+ for (i = 0; i < BASE64MAP2.length; i++) {
+ BASE64MAP2[i] = -1;
}
for (i = 0; i < 64; i++) {
- base64map2[base64map1[i]] = (byte) i;
+ BASE64MAP2[BASE64MAP1[i]] = (byte) i;
}
}
@@ -58,7 +58,7 @@ public final class CryptUtils {
} else if (result == ']') {
plaintext = false;
} else if (!plaintext) {
- int capitalized = result & 32;
+ final int capitalized = result & 32;
result &= ~capitalized;
result = ((result >= 'A') && (result <= 'Z') ? ((result - 'A' + 13) % 26 + 'A') : result)
| capitalized;
@@ -68,50 +68,44 @@ public final class CryptUtils {
}
@NonNull
- public static String rot13(String text) {
+ public static String rot13(final String text) {
if (text == null) {
return StringUtils.EMPTY;
}
final StringBuilder result = new StringBuilder();
- Rot13Encryption rot13 = new Rot13Encryption();
+ final Rot13Encryption rot13 = new Rot13Encryption();
final int length = text.length();
for (int index = 0; index < length; index++) {
- char c = text.charAt(index);
+ final char c = text.charAt(index);
result.append(rot13.getNextEncryptedCharacter(c));
}
return result.toString();
}
- public static String md5(String text) {
+ 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 e) {
- Log.e("CryptUtils.md5", e);
- } catch (UnsupportedEncodingException e) {
+ } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
Log.e("CryptUtils.md5", e);
}
return StringUtils.EMPTY;
}
- public static byte[] hashHmac(String text, String salt) {
- byte[] macBytes = {};
+ 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);
- macBytes = mac.doFinal(text.getBytes(CharEncoding.UTF_8));
- } catch (GeneralSecurityException e) {
- Log.e("CryptUtils.hashHmac", e);
- } catch (UnsupportedEncodingException e) {
+ return mac.doFinal(text.getBytes(CharEncoding.UTF_8));
+ } catch (GeneralSecurityException | UnsupportedEncodingException e) {
Log.e("CryptUtils.hashHmac", e);
+ return EMPTY;
}
-
- return macBytes;
}
public static CharSequence rot13(final Spannable span) {
@@ -119,37 +113,37 @@ public final class CryptUtils {
// 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);
- Rot13Encryption rot13 = new Rot13Encryption();
+ final Rot13Encryption rot13 = new Rot13Encryption();
final int length = span.length();
for (int index = 0; index < length; index++) {
- char c = span.charAt(index);
+ final char c = span.charAt(index);
buffer.replace(index, index + 1, String.valueOf(rot13.getNextEncryptedCharacter(c)));
}
return buffer;
}
- public static String base64Encode(byte[] in) {
- int iLen = in.length;
- int oDataLen = (iLen * 4 + 2) / 3; // output length without padding
- int oLen = ((iLen + 2) / 3) * 4; // output length including padding
- char[] out = new char[oLen];
+ 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) {
- int i0 = in[ip++] & 0xff;
- int i1 = ip < iLen ? in[ip++] & 0xff : 0;
- int i2 = ip < iLen ? in[ip++] & 0xff : 0;
- int o0 = i0 >>> 2;
- int o1 = ((i0 & 3) << 4) | (i1 >>> 4);
- int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
- int o3 = i2 & 0x3F;
- out[op++] = base64map1[o0];
- out[op++] = base64map1[o1];
- out[op] = op < oDataLen ? base64map1[o2] : '=';
+ 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] : '=';
+ out[op] = op < oDataLen ? BASE64MAP1[o3] : '=';
op++;
}
diff --git a/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java b/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java
index d8aff74..a65a9fb 100644
--- a/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java
+++ b/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java
@@ -6,9 +6,18 @@ import cgeo.geocaching.R;
import cgeo.geocaching.ui.dialog.Dialogs;
import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import rx.functions.Action0;
+import rx.functions.Action1;
+import rx.functions.Func0;
+import rx.schedulers.Schedulers;
import android.app.Activity;
import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
import android.content.res.Resources;
import java.io.File;
@@ -21,70 +30,107 @@ public class DatabaseBackupUtils {
}
/**
- * restore the database in a new thread, showing a progress window
+ * After confirming to overwrite the existing caches on the devices, restore the database in a new thread, showing a
+ * progress window
*
* @param activity
* calling activity
*/
public static void restoreDatabase(final Activity activity) {
+ if (!hasBackup()) {
+ return;
+ }
+ final int caches = DataStore.getAllCachesCount();
+ if (caches == 0) {
+ restoreDatabaseInternal(activity);
+ }
+ else {
+ Dialogs.confirm(activity, R.string.init_backup_restore, activity.getString(R.string.restore_confirm_overwrite, activity.getResources().getQuantityString(R.plurals.cache_counts, caches, caches)), new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ restoreDatabaseInternal(activity);
+ }
+ });
+
+ }
+ }
+
+ private static void restoreDatabaseInternal(final Activity activity) {
final Resources res = activity.getResources();
final ProgressDialog dialog = ProgressDialog.show(activity, res.getString(R.string.init_backup_restore), res.getString(R.string.init_restore_running), true, false);
final AtomicBoolean restoreSuccessful = new AtomicBoolean(false);
- new Thread() {
+ RxUtils.andThenOnUi(Schedulers.io(), new Action0() {
@Override
- public void run() {
+ public void call() {
restoreSuccessful.set(DataStore.restoreDatabaseInternal());
- activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- dialog.dismiss();
- boolean restored = restoreSuccessful.get();
- String message = restored ? res.getString(R.string.init_restore_success) : res.getString(R.string.init_restore_failed);
- Dialogs.message(activity, R.string.init_backup_restore, message);
- if (activity instanceof MainActivity) {
- ((MainActivity) activity).updateCacheCounter();
- }
- }
- });
}
- }.start();
+ }, new Action0() {
+ @Override
+ public void call() {
+ dialog.dismiss();
+ final boolean restored = restoreSuccessful.get();
+ final String message = restored ? res.getString(R.string.init_restore_success) : res.getString(R.string.init_restore_failed);
+ Dialogs.message(activity, R.string.init_backup_restore, message);
+ if (activity instanceof MainActivity) {
+ ((MainActivity) activity).updateCacheCounter();
+ }
+ }
+ });
}
- public static boolean createBackup(final Activity activity, final Runnable runAfterwards) {
+ /**
+ * Create a backup after confirming to overwrite the existing backup.
+ *
+ */
+ public static void createBackup(final Activity activity, final Runnable runAfterwards) {
// avoid overwriting an existing backup with an empty database
// (can happen directly after reinstalling the app)
if (DataStore.getAllCachesCount() == 0) {
Dialogs.message(activity, R.string.init_backup, R.string.init_backup_unnecessary);
- return false;
+ return;
}
+ if (hasBackup()) {
+ Dialogs.confirm(activity, R.string.init_backup, activity.getString(R.string.backup_confirm_overwrite, getBackupDateTime()), new OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ createBackupInternal(activity, runAfterwards);
+ }
+ });
+ }
+ else {
+ createBackupInternal(activity, runAfterwards);
+ }
+ }
+
+ private static void createBackupInternal(final Activity activity, final Runnable runAfterwards) {
final ProgressDialog dialog = ProgressDialog.show(activity,
activity.getString(R.string.init_backup),
activity.getString(R.string.init_backup_running), true, false);
- new Thread() {
+ RxUtils.andThenOnUi(Schedulers.io(), new Func0<String>() {
+ @Override
+ public String call() {
+ return DataStore.backupDatabaseInternal();
+ }
+ }, new Action1<String>() {
@Override
- public void run() {
- final String backupFileName = DataStore.backupDatabaseInternal();
- activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- dialog.dismiss();
- Dialogs.message(activity,
- R.string.init_backup_backup,
- backupFileName != null
- ? activity.getString(R.string.init_backup_success)
- + "\n" + backupFileName
- : activity.getString(R.string.init_backup_failed));
- if (runAfterwards != null) {
- runAfterwards.run();
- }
- }
- });
+ public void call(final String backupFileName) {
+ dialog.dismiss();
+ Dialogs.message(activity,
+ R.string.init_backup_backup,
+ backupFileName != null
+ ? activity.getString(R.string.init_backup_success)
+ + "\n" + backupFileName
+ : activity.getString(R.string.init_backup_failed));
+ if (runAfterwards != null) {
+ runAfterwards.run();
+ }
}
- }.start();
- return true;
+ });
}
+ @Nullable
public static File getRestoreFile() {
final File fileSourceFile = DataStore.getBackupFileInternal();
return fileSourceFile.exists() && fileSourceFile.length() > 0 ? fileSourceFile : null;
@@ -94,6 +140,7 @@ public class DatabaseBackupUtils {
return getRestoreFile() != null;
}
+ @NonNull
public static String getBackupDateTime() {
final File restoreFile = getRestoreFile();
if (restoreFile == null) {
diff --git a/main/src/cgeo/geocaching/utils/DebugUtils.java b/main/src/cgeo/geocaching/utils/DebugUtils.java
index 07aac64..1f95e7c 100644
--- a/main/src/cgeo/geocaching/utils/DebugUtils.java
+++ b/main/src/cgeo/geocaching/utils/DebugUtils.java
@@ -22,15 +22,14 @@ public class DebugUtils {
public static void createMemoryDump(final @NonNull Context context) {
try {
- final Date now = new Date();
final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyy-MM-dd_hh-mm", Locale.US);
- File file = FileUtils.getUniqueNamedFile(Environment.getExternalStorageDirectory().getPath()
- + File.separatorChar + "cgeo_dump_" + fileNameDateFormat.format(now) + ".hprof");
+ final File file = FileUtils.getUniqueNamedFile(new File(Environment.getExternalStorageDirectory(),
+ "cgeo_dump_" + fileNameDateFormat.format(new Date()) + ".hprof"));
android.os.Debug.dumpHprofData(file.getPath());
Toast.makeText(context, context.getString(R.string.init_memory_dumped, file.getAbsolutePath()),
Toast.LENGTH_LONG).show();
ShareUtils.share(context, file, R.string.init_memory_dump);
- } catch (IOException e) {
+ } catch (final IOException e) {
Log.e("createMemoryDump", e);
}
}
diff --git a/main/src/cgeo/geocaching/utils/EditUtils.java b/main/src/cgeo/geocaching/utils/EditUtils.java
index 0bfe2ea..455ce4d 100644
--- a/main/src/cgeo/geocaching/utils/EditUtils.java
+++ b/main/src/cgeo/geocaching/utils/EditUtils.java
@@ -17,7 +17,7 @@ public final class EditUtils {
editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ public boolean onEditorAction(final TextView v, final int actionId, final KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
runnable.run();
return true;
@@ -30,7 +30,7 @@ public final class EditUtils {
editText.setOnKeyListener(new View.OnKeyListener() {
@Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
+ public boolean onKey(final View v, final int keyCode, final KeyEvent event) {
// If the event is a key-down event on the "enter" button
if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
runnable.run();
@@ -42,7 +42,7 @@ public final class EditUtils {
}
- public static void disableSuggestions(EditText edit) {
+ public static void disableSuggestions(final EditText edit) {
edit.setInputType(edit.getInputType()
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
| InputType.TYPE_TEXT_VARIATION_FILTER);
diff --git a/main/src/cgeo/geocaching/utils/FileUtils.java b/main/src/cgeo/geocaching/utils/FileUtils.java
index 979820c..778b9c7 100644
--- a/main/src/cgeo/geocaching/utils/FileUtils.java
+++ b/main/src/cgeo/geocaching/utils/FileUtils.java
@@ -4,6 +4,7 @@ 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;
@@ -23,14 +24,18 @@ import java.util.List;
*/
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(List<File> result, File directory, FileSelector chooser, Handler feedBackHandler) {
+ 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) {
@@ -40,7 +45,7 @@ public final class FileUtils {
final File[] files = directory.listFiles();
if (ArrayUtils.isNotEmpty(files)) {
- for (File file : files) {
+ for (final File file : files) {
if (chooser.shouldEnd()) {
return;
}
@@ -63,12 +68,32 @@ public final class FileUtils {
feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name));
}
- listDir(result, file, chooser, feedBackHandler); // go deeper
+ 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 static abstract class FileSelector {
public abstract boolean isSelected(File file);
@@ -86,18 +111,20 @@ public final class FileUtils {
* </ul>
* which does not yet exist.
*/
- public static File getUniqueNamedFile(final String baseNameAndPath) {
- String extension = StringUtils.substringAfterLast(baseNameAndPath, ".");
- String pathName = StringUtils.substringBeforeLast(baseNameAndPath, ".");
- int number = 1;
- while (new File(getNumberedFileName(pathName, extension, number)).exists()) {
- number++;
+ public static File getUniqueNamedFile(final File file) {
+ if (!file.exists()) {
+ return file;
}
- return new File(getNumberedFileName(pathName, extension, number));
- }
-
- private static String getNumberedFileName(String pathName, String extension, int number) {
- return pathName + (number > 1 ? "_" + Integer.toString(number) : "") + "." + extension;
+ final String baseNameAndPath = file.getPath();
+ final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_";
+ final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, ".");
+ for (int i = 1; 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");
}
/**
@@ -129,7 +156,7 @@ public final class FileUtils {
* @return <code>true</code> if the directory was created, <code>false</code> on failure or if the directory already
* existed.
*/
- public static boolean mkdirs(File file) {
+ 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());
@@ -137,7 +164,7 @@ public final class FileUtils {
return success;
}
- public static boolean writeFileUTF16(File file, String content) {
+ 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;
@@ -177,7 +204,7 @@ public final class FileUtils {
/**
* Local file name when {@link #isFileUrl(String)} is <tt>true</tt>.
- *
+ *
* @return the local file
*/
public static File urlToFile(final String url) {
diff --git a/main/src/cgeo/geocaching/utils/Formatter.java b/main/src/cgeo/geocaching/utils/Formatter.java
index 3068cd4..2127d59 100644
--- a/main/src/cgeo/geocaching/utils/Formatter.java
+++ b/main/src/cgeo/geocaching/utils/Formatter.java
@@ -17,6 +17,7 @@ import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.Locale;
public abstract class Formatter {
@@ -33,7 +34,7 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatTime(long date) {
+ public static String formatTime(final long date) {
return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME);
}
@@ -45,7 +46,7 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatDate(long date) {
+ public static String formatDate(final long date) {
return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE);
}
@@ -58,7 +59,7 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatFullDate(long date) {
+ public static String formatFullDate(final long date) {
return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_YEAR);
}
@@ -71,11 +72,15 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatShortDate(long date) {
- DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context);
+ 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".
@@ -84,8 +89,8 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatShortDateVerbally(long date) {
- int diff = cgeo.geocaching.utils.DateUtils.daysSince(date);
+ 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);
@@ -104,7 +109,7 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatShortDateTime(long date) {
+ public static String formatShortDateTime(final long date) {
return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL);
}
@@ -116,11 +121,11 @@ public abstract class Formatter {
* milliseconds since the epoch
* @return the formatted string
*/
- public static String formatDateTime(long date) {
+ public static String formatDateTime(final long date) {
return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
}
- public static String formatCacheInfoLong(Geocache cache, CacheListType cacheListType) {
+ public static String formatCacheInfoLong(final Geocache cache, final CacheListType cacheListType) {
final ArrayList<String> infos = new ArrayList<>();
if (StringUtils.isNotBlank(cache.getGeocode())) {
infos.add(cache.getGeocode());
@@ -137,18 +142,18 @@ public abstract class Formatter {
return StringUtils.join(infos, Formatter.SEPARATOR);
}
- public static String formatCacheInfoShort(Geocache cache) {
+ public static String formatCacheInfoShort(final Geocache cache) {
final ArrayList<String> infos = new ArrayList<>();
addShortInfos(cache, infos);
return StringUtils.join(infos, Formatter.SEPARATOR);
}
- private static void addShortInfos(Geocache cache, final ArrayList<String> infos) {
+ private static void addShortInfos(final Geocache cache, final ArrayList<String> infos) {
if (cache.hasDifficulty()) {
- infos.add("D " + String.format("%.1f", cache.getDifficulty()));
+ infos.add("D " + formatDT(cache.getDifficulty()));
}
if (cache.hasTerrain()) {
- infos.add("T " + String.format("%.1f", cache.getTerrain()));
+ infos.add("T " + formatDT(cache.getTerrain()));
}
// don't show "not chosen" for events and virtuals, that should be the normal case
@@ -157,12 +162,16 @@ public abstract class Formatter {
} else if (cache.isEventCache()) {
final Date hiddenDate = cache.getHiddenDate();
if (hiddenDate != null) {
- infos.add(Formatter.formatShortDate(hiddenDate.getTime()));
+ infos.add(Formatter.formatShortDateIncludingWeekday(hiddenDate.getTime()));
}
}
}
- public static String formatCacheInfoHistory(Geocache cache) {
+ private static String formatDT(final float value) {
+ return String.format(Locale.getDefault(), "%.1f", value);
+ }
+
+ public static String formatCacheInfoHistory(final Geocache cache) {
final ArrayList<String> infos = new ArrayList<>(3);
infos.add(StringUtils.upperCase(cache.getGeocode()));
infos.add(Formatter.formatDate(cache.getVisitedDate()));
@@ -170,9 +179,9 @@ public abstract class Formatter {
return StringUtils.join(infos, Formatter.SEPARATOR);
}
- public static String formatWaypointInfo(Waypoint waypoint) {
+ public static String formatWaypointInfo(final Waypoint waypoint) {
final List<String> infos = new ArrayList<>(3);
- WaypointType waypointType = waypoint.getWaypointType();
+ final WaypointType waypointType = waypoint.getWaypointType();
if (waypointType != WaypointType.OWN && waypointType != null) {
infos.add(waypointType.getL10n());
}
@@ -188,4 +197,42 @@ public abstract class Formatter {
}
return StringUtils.join(infos, Formatter.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 = Formatter.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();
+ }
+
}
diff --git a/main/src/cgeo/geocaching/utils/HtmlUtils.java b/main/src/cgeo/geocaching/utils/HtmlUtils.java
index 51c4d6e..ab6e8fe 100644
--- a/main/src/cgeo/geocaching/utils/HtmlUtils.java
+++ b/main/src/cgeo/geocaching/utils/HtmlUtils.java
@@ -21,10 +21,8 @@ public final class HtmlUtils {
* 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.
*
- * @param html
- * @return
*/
- public static String extractText(CharSequence html) {
+ public static String extractText(final CharSequence html) {
if (StringUtils.isBlank(html)) {
return StringUtils.EMPTY;
}
@@ -32,13 +30,13 @@ public final class HtmlUtils {
// recognize images in textview HTML contents
if (html instanceof Spanned) {
- Spanned text = (Spanned) html;
- Object[] styles = text.getSpans(0, text.length(), Object.class);
- ArrayList<Pair<Integer, Integer>> removals = new ArrayList<>();
- for (Object style : styles) {
+ final Spanned text = (Spanned) html;
+ final Object[] styles = text.getSpans(0, text.length(), Object.class);
+ final ArrayList<Pair<Integer, Integer>> removals = new ArrayList<>();
+ for (final Object style : styles) {
if (style instanceof ImageSpan) {
- int start = text.getSpanStart(style);
- int end = text.getSpanEnd(style);
+ final int start = text.getSpanStart(style);
+ final int end = text.getSpanEnd(style);
removals.add(Pair.of(start, end));
}
}
@@ -47,12 +45,12 @@ public final class HtmlUtils {
Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() {
@Override
- public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) {
+ public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) {
return rhs.getRight().compareTo(lhs.getRight());
}
});
result = text.toString();
- for (Pair<Integer, Integer> removal : removals) {
+ for (final Pair<Integer, Integer> removal : removals) {
result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight());
}
}
@@ -60,4 +58,15 @@ public final class HtmlUtils {
// 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;
+ }
}
diff --git a/main/src/cgeo/geocaching/utils/ImageUtils.java b/main/src/cgeo/geocaching/utils/ImageUtils.java
index 739ecc4..71d5e39 100644
--- a/main/src/cgeo/geocaching/utils/ImageUtils.java
+++ b/main/src/cgeo/geocaching/utils/ImageUtils.java
@@ -1,16 +1,20 @@
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;
@@ -25,6 +29,8 @@ 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;
@@ -36,8 +42,14 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
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.Locale;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
public final class ImageUtils {
private static final int[] ORIENTATIONS = new int[] {
@@ -49,6 +61,10 @@ public final class ImageUtils {
private static final int[] ROTATION = new int[] { 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 = new String[] { "geocheck.org" };
+
private ImageUtils() {
// Do not let this class be instantiated, this is a utility class.
}
@@ -61,7 +77,7 @@ public final class ImageUtils {
* @return BitmapDrawable The scaled image
*/
public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) {
- Point displaySize = Compatibility.getDisplaySize();
+ final Point displaySize = Compatibility.getDisplaySize();
final int maxWidth = displaySize.x - 25;
final int maxHeight = displaySize.y - 25;
return scaleBitmapTo(image, maxWidth, maxHeight);
@@ -76,7 +92,7 @@ public final class ImageUtils {
*/
@Nullable
public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) {
- Point displaySize = Compatibility.getDisplaySize();
+ 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);
@@ -128,12 +144,12 @@ public final class ImageUtils {
*/
public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) {
try {
- FileOutputStream out = new FileOutputStream(pathOfOutputImage);
- BufferedOutputStream bos = new BufferedOutputStream(out);
+ final FileOutputStream out = new FileOutputStream(pathOfOutputImage);
+ final BufferedOutputStream bos = new BufferedOutputStream(out);
bitmap.compress(format, quality, bos);
bos.flush();
bos.close();
- } catch (IOException e) {
+ } catch (final IOException e) {
Log.e("ImageHelper.storeBitmap", e);
}
}
@@ -152,7 +168,7 @@ public final class ImageUtils {
if (maxXY <= 0) {
return filePath;
}
- Bitmap image = readDownsampledImage(filePath, maxXY, maxXY);
+ final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY);
if (image == null) {
return null;
}
@@ -184,7 +200,7 @@ public final class ImageUtils {
try {
final ExifInterface exif = new ExifInterface(filePath);
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
- } catch (IOException e) {
+ } catch (final IOException e) {
Log.e("ImageUtils.readDownsampledImage", e);
}
final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options();
@@ -233,7 +249,7 @@ public final class ImageUtils {
}
// Create a media file name
- String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
+ final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
}
@@ -254,7 +270,7 @@ public final class ImageUtils {
* @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 (String entry : patterns) {
+ for (final String entry : patterns) {
if (StringUtils.containsIgnoreCase(url, entry)) {
return true;
}
@@ -282,7 +298,7 @@ public final class ImageUtils {
/**
* Decode a base64-encoded string and save the result into a stream.
- *
+ *
* @param inString
* the encoded string
* @param out
@@ -303,42 +319,158 @@ public final class ImageUtils {
}
/**
+ * 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(source, StringUtils.defaultString(geocode)));
+ 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.
*/
- @SuppressWarnings("deprecation")
- public final static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> {
+ 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 private TextView view;
+ final protected TextView view;
- public ContainerDrawable(@NonNull final TextView view) {
+ @SuppressWarnings("deprecation")
+ public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
this.view = view;
drawable = null;
setBounds(0, 0, 0, 0);
- }
-
- public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
- this(view);
- updateFrom(drawableObservable);
+ drawableObservable.subscribe(this);
}
@Override
- public void draw(final Canvas canvas) {
+ public final void draw(final Canvas canvas) {
if (drawable != null) {
drawable.draw(canvas);
}
}
@Override
- public void call(final Drawable newDrawable) {
+ 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
+ */
+ protected TextView updateDrawable(final Drawable newDrawable) {
setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
drawable = newDrawable;
- view.setText(view.getText());
+ return view;
+ }
+
+ 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 ArrayList<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) {
+ VIEWS.add(redrawable.left.updateDrawable(redrawable.right));
+ }
+ 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);
}
- public void updateFrom(final Observable<? extends Drawable> drawableObservable) {
- drawableObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(this);
+ @Override
+ protected TextView updateDrawable(final Drawable newDrawable) {
+ super.updateDrawable(newDrawable);
+ setBounds(ImageUtils.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;
+ }
}
diff --git a/main/src/cgeo/geocaching/utils/JsonUtils.java b/main/src/cgeo/geocaching/utils/JsonUtils.java
new file mode 100644
index 0000000..492e137
--- /dev/null
+++ b/main/src/cgeo/geocaching/utils/JsonUtils.java
@@ -0,0 +1,20 @@
+package cgeo.geocaching.utils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+
+public class JsonUtils {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ public static final ObjectReader reader = mapper.reader();
+ public static final ObjectWriter writer = mapper.writer();
+
+ public static final JsonNodeFactory factory = new JsonNodeFactory(true);
+
+ private JsonUtils() {
+ // Do not instantiate
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/utils/LazyInitializedList.java b/main/src/cgeo/geocaching/utils/LazyInitializedList.java
index b0e2e46..866acad 100644
--- a/main/src/cgeo/geocaching/utils/LazyInitializedList.java
+++ b/main/src/cgeo/geocaching/utils/LazyInitializedList.java
@@ -49,7 +49,7 @@ public abstract class LazyInitializedList<ElementType> extends AbstractList<Elem
}
@Override
- public void add(int index, final ElementType element) {
+ public void add(final int index, final ElementType element) {
getUnderlyingList().add(index, element);
}
diff --git a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java
index 6122532..aecfaf1 100644
--- a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java
+++ b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java
@@ -30,7 +30,7 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
final int initialCapacity;
final float loadFactor;
- protected LeastRecentlyUsedMap(int maxEntries, int initialCapacity, float loadFactor, OperationModes opMode) {
+ protected LeastRecentlyUsedMap(final int maxEntries, final int initialCapacity, final float loadFactor, final OperationModes opMode) {
super(initialCapacity, loadFactor, (opMode==OperationModes.LRU_CACHE));
this.initialCapacity = initialCapacity;
this.loadFactor = loadFactor;
@@ -38,12 +38,12 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
this.opMode = opMode;
}
- protected LeastRecentlyUsedMap(int maxEntries, OperationModes opMode) {
+ protected LeastRecentlyUsedMap(final int maxEntries, final OperationModes opMode) {
this(maxEntries, 16, 0.75f, opMode);
}
@Override
- public V put(K key, V value) {
+ public V put(final K key, final V value) {
// in case the underlying Map is not running with accessOrder==true, the map won't notice any changes
// of existing keys, so for the normal BOUNDED mode we remove and put the value to get its order updated.
if (opMode == OperationModes.BOUNDED && containsKey(key)) {
@@ -57,7 +57,7 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
}
@Override
- protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
@@ -66,9 +66,9 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
}
@Override
- public V remove(Object key) {
+ public V remove(final Object key) {
- V removed = super.remove(key);
+ final V removed = super.remove(key);
if (removed != null && removeHandler != null) {
removeHandler.onRemove(removed);
@@ -84,18 +84,18 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
* @param removeHandler
* The new handler to receive notifications or null to remove a handler
*/
- public void setRemoveHandler(RemoveHandler<V> removeHandler) {
+ public void setRemoveHandler(final RemoveHandler<V> removeHandler) {
this.removeHandler = removeHandler;
}
public static class LruCache<K, V> extends LeastRecentlyUsedMap<K, V> {
private static final long serialVersionUID = 9028478916221334454L;
- public LruCache(int maxEntries, int initialCapacity, float loadFactor) {
+ public LruCache(final int maxEntries, final int initialCapacity, final float loadFactor) {
super(maxEntries, initialCapacity, loadFactor, OperationModes.LRU_CACHE);
}
- public LruCache(int maxEntries) {
+ public LruCache(final int maxEntries) {
super(maxEntries, OperationModes.LRU_CACHE);
}
}
@@ -104,11 +104,11 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = -1476389304214398315L;
- public Bounded(int maxEntries, int initialCapacity, float loadFactor) {
+ public Bounded(final int maxEntries, final int initialCapacity, final float loadFactor) {
super(maxEntries, initialCapacity, loadFactor, OperationModes.BOUNDED);
}
- public Bounded(int maxEntries) {
+ public Bounded(final int maxEntries) {
super(maxEntries, OperationModes.BOUNDED);
}
}
@@ -117,7 +117,6 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> {
* Interface for handlers that wish to get notified when items are
* removed from the LRUMap
*
- * @param <V>
*/
public interface RemoveHandler<V> {
diff --git a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java
index a69f427..c139136 100644
--- a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java
+++ b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java
@@ -20,21 +20,18 @@ import java.util.List;
* access has to be guarded externally or the synchronized getAsList method can be used
* to get a clone for iteration.
*/
-public class LeastRecentlyUsedSet<E> extends AbstractSet<E>
- implements Cloneable, java.io.Serializable {
+public class LeastRecentlyUsedSet<E> extends AbstractSet<E> {
- private static final long serialVersionUID = -1942301031191419547L;
-
- private transient LeastRecentlyUsedMap<E, Object> map;
+ private final LeastRecentlyUsedMap<E, Object> map;
private static final Object PRESENT = new Object();
- public LeastRecentlyUsedSet(int maxEntries, int initialCapacity, float loadFactor) {
+ public LeastRecentlyUsedSet(final int maxEntries, final int initialCapacity, final float loadFactor) {
// because we don't use any Map.get() methods from the Set, BOUNDED and LRU_CACHE have the exact same Behaviour
// So we use LRU_CACHE mode because it should perform a bit better (as it doesn't re-add explicitly)
map = new LeastRecentlyUsedMap.LruCache<>(maxEntries, initialCapacity, loadFactor);
}
- public LeastRecentlyUsedSet(int maxEntries) {
+ public LeastRecentlyUsedSet(final int maxEntries) {
map = new LeastRecentlyUsedMap.LruCache<>(maxEntries);
}
@@ -79,7 +76,7 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E>
* @see HashSet
*/
@Override
- public synchronized boolean contains(Object o) {
+ public synchronized boolean contains(final Object o) {
return map.containsKey(o);
}
@@ -90,7 +87,7 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E>
* @see HashSet
*/
@Override
- public synchronized boolean add(E e) {
+ public synchronized boolean add(final E e) {
if (e == null) {
throw new IllegalArgumentException("LeastRecentlyUsedSet cannot take null element");
}
@@ -104,7 +101,7 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E>
* @see HashSet
*/
@Override
- public synchronized boolean remove(Object o) {
+ public synchronized boolean remove(final Object o) {
return map.remove(o) == PRESENT;
}
@@ -132,26 +129,6 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E>
}
/**
- * (synchronized) Clone of the set
- * Copy of the HashSet code if clone()
- *
- * @see HashSet
- */
- @Override
- @SuppressWarnings("unchecked")
- public Object clone() throws CloneNotSupportedException {
- try {
- synchronized (this) {
- final LeastRecentlyUsedSet<E> newSet = (LeastRecentlyUsedSet<E>) super.clone();
- newSet.map = (LeastRecentlyUsedMap<E, Object>) map.clone();
- return newSet;
- }
- } catch (CloneNotSupportedException e) {
- throw new InternalError();
- }
- }
-
- /**
* Creates a clone as a list in a synchronized fashion.
*
* @return List based clone of the set
@@ -160,56 +137,4 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E>
return new ArrayList<>(this);
}
- /**
- * Serialization version of HashSet with the additional parameters for the custom Map
- *
- * @see HashSet
- */
- private void writeObject(java.io.ObjectOutputStream s)
- throws java.io.IOException {
- // Write out any hidden serialization magic
- s.defaultWriteObject();
-
- // Write out HashMap capacity and load factor
- s.writeInt(map.initialCapacity);
- s.writeFloat(map.loadFactor);
- s.writeInt(map.getMaxEntries());
-
- // Write out size
- s.writeInt(map.size());
-
- // Write out all elements in the proper order.
- for (final E e : map.keySet()) {
- s.writeObject(e);
- }
- }
-
- /**
- * Serialization version of HashSet with the additional parameters for the custom Map
- *
- * @see HashSet
- */
- @SuppressWarnings("unchecked")
- private void readObject(java.io.ObjectInputStream s)
- throws java.io.IOException, ClassNotFoundException {
- // Read in any hidden serialization magic
- s.defaultReadObject();
-
- // Read in HashMap capacity and load factor and create backing HashMap
- final int capacity = s.readInt();
- final float loadFactor = s.readFloat();
- final int maxEntries = s.readInt();
-
- map = new LeastRecentlyUsedMap.LruCache<>(maxEntries, capacity, loadFactor);
-
- // Read in size
- final int size = s.readInt();
-
- // Read in all elements in the proper order.
- for (int i = 0; i < size; i++) {
- E e = (E) s.readObject();
- map.put(e, PRESENT);
- }
- }
-
}
diff --git a/main/src/cgeo/geocaching/utils/Log.java b/main/src/cgeo/geocaching/utils/Log.java
index f338a8e..861faaa 100644
--- a/main/src/cgeo/geocaching/utils/Log.java
+++ b/main/src/cgeo/geocaching/utils/Log.java
@@ -37,62 +37,65 @@ public final class Log {
/**
* Save a copy of the debug flag from the settings for performance reasons.
*
- * @param isDebug
*/
public static void setDebug(final boolean isDebug) {
Log.isDebug = isDebug;
}
+ private static String addThreadInfo(final String msg) {
+ return new StringBuilder("[").append(Thread.currentThread().getName()).append("] ").append(msg).toString();
+ }
+
public static void v(final String msg) {
if (isDebug) {
- android.util.Log.v(TAG, msg);
+ android.util.Log.v(TAG, addThreadInfo(msg));
}
}
public static void v(final String msg, final Throwable t) {
if (isDebug) {
- android.util.Log.v(TAG, msg, t);
+ android.util.Log.v(TAG, addThreadInfo(msg), t);
}
}
public static void d(final String msg) {
if (isDebug) {
- android.util.Log.d(TAG, msg);
+ android.util.Log.d(TAG, addThreadInfo(msg));
}
}
public static void d(final String msg, final Throwable t) {
if (isDebug) {
- android.util.Log.d(TAG, msg, t);
+ android.util.Log.d(TAG, addThreadInfo(msg), t);
}
}
public static void i(final String msg) {
if (isDebug) {
- android.util.Log.i(TAG, msg);
+ android.util.Log.i(TAG, addThreadInfo(msg));
}
}
public static void i(final String msg, final Throwable t) {
if (isDebug) {
- android.util.Log.i(TAG, msg, t);
+ android.util.Log.i(TAG, addThreadInfo(msg), t);
}
}
public static void w(final String msg) {
- android.util.Log.w(TAG, msg);
+ android.util.Log.w(TAG, addThreadInfo(msg));
}
public static void w(final String msg, final Throwable t) {
- android.util.Log.w(TAG, msg, t);
+ android.util.Log.w(TAG, addThreadInfo(msg), t);
}
public static void e(final String msg) {
- android.util.Log.e(TAG, msg);
+ android.util.Log.e(TAG, addThreadInfo(msg));
}
public static void e(final String msg, final Throwable t) {
- android.util.Log.e(TAG, msg, t);
+ android.util.Log.e(TAG, addThreadInfo(msg), t);
}
/**
@@ -116,7 +119,7 @@ public final class Log {
Writer writer = null;
try {
writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), CharEncoding.UTF_8));
- writer.write(msg);
+ writer.write(addThreadInfo(msg));
} catch (final IOException e) {
Log.e("logToFile: cannot write to " + file, e);
} finally {
diff --git a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
index ff4013c..1db3d5b 100644
--- a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
+++ b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
@@ -215,7 +215,7 @@ public final class LogTemplateProvider {
}
final Geocache cache = context.getCache();
if (cache != null) {
- return cache.getUrl();
+ return StringUtils.defaultString(cache.getUrl());
}
return StringUtils.EMPTY;
}
diff --git a/main/src/cgeo/geocaching/utils/MapUtils.java b/main/src/cgeo/geocaching/utils/MapUtils.java
index 5120ca5..f41247c 100644
--- a/main/src/cgeo/geocaching/utils/MapUtils.java
+++ b/main/src/cgeo/geocaching/utils/MapUtils.java
@@ -154,7 +154,6 @@ public final class MapUtils {
}
private static int calculateResolution(final Drawable marker) {
- final int resolution = marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0;
- return resolution;
+ return marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0;
}
}
diff --git a/main/src/cgeo/geocaching/utils/MatcherWrapper.java b/main/src/cgeo/geocaching/utils/MatcherWrapper.java
index c99d3c4..733a18e 100644
--- a/main/src/cgeo/geocaching/utils/MatcherWrapper.java
+++ b/main/src/cgeo/geocaching/utils/MatcherWrapper.java
@@ -2,6 +2,8 @@ package cgeo.geocaching.utils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.eclipse.jdt.annotation.NonNull;
+
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -13,7 +15,7 @@ import java.util.regex.Pattern;
public class MatcherWrapper {
private final Matcher matcher;
- public MatcherWrapper(Pattern pattern, String input) {
+ public MatcherWrapper(@NonNull final Pattern pattern, @NonNull final String input) {
this.matcher = pattern.matcher(input);
}
@@ -24,14 +26,14 @@ public class MatcherWrapper {
return matcher.find();
}
- public boolean find(int start) {
+ public boolean find(final int start) {
return matcher.find(start);
}
/**
* see {@link Matcher#group(int)}
*/
- public String group(int index) {
+ public String group(final int index) {
return newString(matcher.group(index));
}
@@ -43,11 +45,9 @@ public class MatcherWrapper {
* <p>
* Do not change this method, even if Findbugs and other tools will report a violation for that line!
*
- * @param input
- * @return
*/
@SuppressFBWarnings("DM_STRING_CTOR")
- private static String newString(String input) {
+ private static String newString(final String input) {
if (input == null) {
return null;
}
@@ -78,7 +78,7 @@ public class MatcherWrapper {
/**
* see {@link Matcher#replaceAll(String)}
*/
- public String replaceAll(String replacement) {
+ public String replaceAll(final String replacement) {
return newString(matcher.replaceAll(replacement));
}
@@ -92,7 +92,7 @@ public class MatcherWrapper {
/**
* see {@link Matcher#replaceFirst(String)}
*/
- public String replaceFirst(String replacement) {
+ public String replaceFirst(final String replacement) {
return newString(matcher.replaceFirst(replacement));
}
}
diff --git a/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java b/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java
index 1401542..0c6365c 100644
--- a/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java
+++ b/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java
@@ -11,14 +11,12 @@ public class OOMDumpingUncaughtExceptionHandler implements UncaughtExceptionHand
private boolean defaultReplaced = false;
public static boolean activateHandler() {
-
final OOMDumpingUncaughtExceptionHandler handler = new OOMDumpingUncaughtExceptionHandler();
return handler.activate();
}
private boolean activate() {
-
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// replace default handler if that has not been done already
@@ -34,10 +32,8 @@ public class OOMDumpingUncaughtExceptionHandler implements UncaughtExceptionHand
}
public static boolean resetToDefault() {
-
- boolean defaultResetted = false;
-
final UncaughtExceptionHandler unspecificHandler = Thread.getDefaultUncaughtExceptionHandler();
+ boolean defaultResetted = unspecificHandler != null;
if (unspecificHandler instanceof OOMDumpingUncaughtExceptionHandler) {
final OOMDumpingUncaughtExceptionHandler handler = (OOMDumpingUncaughtExceptionHandler) unspecificHandler;
@@ -48,7 +44,6 @@ public class OOMDumpingUncaughtExceptionHandler implements UncaughtExceptionHand
}
private boolean reset() {
-
final boolean resetted = defaultReplaced;
if (defaultReplaced) {
diff --git a/main/src/cgeo/geocaching/utils/ProcessUtils.java b/main/src/cgeo/geocaching/utils/ProcessUtils.java
index d80674b..6a57cbf 100644
--- a/main/src/cgeo/geocaching/utils/ProcessUtils.java
+++ b/main/src/cgeo/geocaching/utils/ProcessUtils.java
@@ -3,7 +3,10 @@ package cgeo.geocaching.utils;
import cgeo.geocaching.CgeoApplication;
import org.apache.commons.collections4.CollectionUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -21,10 +24,8 @@ public final class ProcessUtils {
/**
* Preferred method to detect the availability of an external app
*
- * @param packageName
- * @return
*/
- public static boolean isLaunchable(final String packageName) {
+ public static boolean isLaunchable(@Nullable final String packageName) {
return getLaunchIntent(packageName) != null;
}
@@ -33,17 +34,15 @@ public final class ProcessUtils {
* This function is relatively costly, so if you know that the package in question has
* a launch intent, use isLaunchable() instead.
*
- * @param packageName
- * @return
*/
- public static boolean isInstalled(final String packageName) {
+ public static boolean isInstalled(@NonNull final String packageName) {
return isLaunchable(packageName) || hasPackageInstalled(packageName);
}
/**
* This will find installed applications even without launch intent (e.g. the streetview plugin).
*/
- private static boolean hasPackageInstalled(final String packageName) {
+ private static boolean hasPackageInstalled(@NonNull final String packageName) {
final List<PackageInfo> packs = CgeoApplication.getInstance().getPackageManager().getInstalledPackages(0);
for (final PackageInfo packageInfo : packs) {
if (packageName.equals(packageInfo.packageName)) {
@@ -56,7 +55,8 @@ public final class ProcessUtils {
/**
* This will find applications, which can be launched.
*/
- public static Intent getLaunchIntent(final String packageName) {
+ @Nullable
+ public static Intent getLaunchIntent(@Nullable final String packageName) {
if (packageName == null) {
return null;
}
@@ -65,12 +65,12 @@ public final class ProcessUtils {
// This can throw an exception where the exception type is only defined on API Level > 3
// therefore surround with try-catch
return packageManager.getLaunchIntentForPackage(packageName);
- } catch (final Exception e) {
+ } catch (final Exception ignored) {
return null;
}
}
- public static boolean isIntentAvailable(final String intent) {
+ public static boolean isIntentAvailable(@NonNull final String intent) {
return isIntentAvailable(intent, null);
}
@@ -79,16 +79,16 @@ public final class ProcessUtils {
* method queries the package manager for installed packages that can
* respond to an intent with the specified action. If no suitable package is
* found, this method returns false.
- *
+ *
* @param action
* The Intent action to check for availability.
* @param uri
* The Intent URI to check for availability.
- *
+ *
* @return True if an Intent with the specified action can be sent and
* responded to, false otherwise.
*/
- public static boolean isIntentAvailable(final String action, final Uri uri) {
+ public static boolean isIntentAvailable(@NonNull final String action, @Nullable final Uri uri) {
final PackageManager packageManager = CgeoApplication.getInstance().getPackageManager();
final Intent intent;
if (uri == null) {
@@ -98,7 +98,23 @@ public final class ProcessUtils {
}
final List<ResolveInfo> list = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
- return CollectionUtils.isNotEmpty(list);
+ final List<ResolveInfo> servicesList = packageManager.queryIntentServices(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return CollectionUtils.isNotEmpty(list) || CollectionUtils.isNotEmpty(servicesList);
+ }
+
+ @SuppressWarnings("deprecation")
+ public static void openMarket(final Activity activity, @NonNull final String packageName) {
+ try {
+ final String url = "market://details?id=" + packageName;
+ final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ marketIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ activity.startActivity(marketIntent);
+
+ } catch (final RuntimeException ignored) {
+ // market not available, fall back to browser
+ activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=" + packageName)));
+ }
}
}
diff --git a/main/src/cgeo/geocaching/utils/RxUtils.java b/main/src/cgeo/geocaching/utils/RxUtils.java
index 241ba78..08cc3e7 100644
--- a/main/src/cgeo/geocaching/utils/RxUtils.java
+++ b/main/src/cgeo/geocaching/utils/RxUtils.java
@@ -1,22 +1,56 @@
package cgeo.geocaching.utils;
import rx.Observable;
+import rx.Observable.OnSubscribe;
import rx.Scheduler;
+import rx.Scheduler.Worker;
+import rx.Subscriber;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action0;
+import rx.functions.Action1;
+import rx.functions.Func0;
+import rx.functions.Func1;
+import rx.internal.util.RxThreadFactory;
import rx.observables.BlockingObservable;
+import rx.observers.Subscribers;
import rx.schedulers.Schedulers;
+import rx.subjects.PublishSubject;
+import rx.subscriptions.Subscriptions;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
public class RxUtils {
- // Utility class, not to be instanciated
- private RxUtils() {}
+ private RxUtils() {
+ // Utility class, not to be instantiated
+ }
public final static Scheduler computationScheduler = Schedulers.computation();
- public static final Scheduler networkScheduler = Schedulers.from(new ThreadPoolExecutor(10, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()));
+ public static final Scheduler networkScheduler = Schedulers.from(Executors.newFixedThreadPool(10, new RxThreadFactory("network-")));
+
+ public static final Scheduler refreshScheduler = Schedulers.from(Executors.newFixedThreadPool(3, new RxThreadFactory("refresh-")));
+
+ private static final HandlerThread looperCallbacksThread =
+ new HandlerThread("looper callbacks", Process.THREAD_PRIORITY_DEFAULT);
+
+ static {
+ looperCallbacksThread.start();
+ }
+
+ public static final Looper looperCallbacksLooper = looperCallbacksThread.getLooper();
+ public static final Scheduler looperCallbacksScheduler = AndroidSchedulers.handlerThread(new Handler(looperCallbacksLooper));
+ public static final Worker looperCallbacksWorker = looperCallbacksScheduler.createWorker();
public static <T> void waitForCompletion(final BlockingObservable<T> observable) {
observable.lastOrDefault(null);
@@ -25,4 +59,139 @@ public class RxUtils {
public static void waitForCompletion(final Observable<?>... observables) {
waitForCompletion(Observable.merge(observables).toBlocking());
}
+
+ /**
+ * Subscribe function whose subscription and unsubscription take place on a looper thread.
+ *
+ * @param <T>
+ * the type of the observable
+ */
+ public static abstract class LooperCallbacks<T> implements OnSubscribe<T> {
+
+ final AtomicInteger counter = new AtomicInteger(0);
+ final long stopDelay;
+ final TimeUnit stopDelayUnit;
+ final protected PublishSubject<T> subject = PublishSubject.create();
+
+ public LooperCallbacks(final long stopDelay, final TimeUnit stopDelayUnit) {
+ this.stopDelay = stopDelay;
+ this.stopDelayUnit = stopDelayUnit;
+ }
+
+ public LooperCallbacks() {
+ this(0, TimeUnit.SECONDS);
+ }
+
+ @Override
+ final public void call(final Subscriber<? super T> subscriber) {
+ subscriber.add(subject.subscribe(Subscribers.from(subscriber)));
+ looperCallbacksWorker.schedule(new Action0() {
+ @Override
+ public void call() {
+ if (counter.getAndIncrement() == 0) {
+ onStart();
+ }
+ subscriber.add(Subscriptions.create(new Action0() {
+ @Override
+ public void call() {
+ looperCallbacksWorker.schedule(new Action0() {
+ @Override
+ public void call() {
+ if (counter.decrementAndGet() == 0) {
+ onStop();
+ }
+ }
+ }, stopDelay, stopDelayUnit);
+ }
+ }));
+ }
+ });
+ }
+
+ abstract protected void onStart();
+
+ abstract protected void onStop();
+ }
+
+ public static<T> Observable<T> rememberLast(final Observable<T> observable, final T initialValue) {
+ final AtomicReference<T> lastValue = new AtomicReference<>(initialValue);
+ return observable.doOnNext(new Action1<T>() {
+ @Override
+ public void call(final T value) {
+ lastValue.set(value);
+ }
+ }).startWith(Observable.defer(new Func0<Observable<T>>() {
+ @Override
+ public Observable<T> call() {
+ final T last = lastValue.get();
+ return last != null ? Observable.just(last) : Observable.<T>empty();
+ }
+ })).replay(1).refCount();
+ }
+
+ public static <T> void andThenOnUi(final Scheduler scheduler, final Func0<T> background, final Action1<T> foreground) {
+ scheduler.createWorker().schedule(new Action0() {
+ @Override
+ public void call() {
+ final T value = background.call();
+ AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {
+ @Override
+ public void call() {
+ foreground.call(value);
+ }
+ });
+ }
+ });
+ }
+
+ public static void andThenOnUi(final Scheduler scheduler, final Action0 background, final Action0 foreground) {
+ scheduler.createWorker().schedule(new Action0() {
+ @Override
+ public void call() {
+ background.call();
+ AndroidSchedulers.mainThread().createWorker().schedule(foreground);
+ }
+ });
+ }
+
+ /**
+ * Cache the last value of observables so that every key is associated to only one of them.
+ *
+ * @param <K> the type of the key
+ * @param <V> the type of the value
+ */
+ public static class ObservableCache<K, V> {
+
+ final private Func1<K, Observable<V>> func;
+ final private Map<K, Observable<V>> cached = new HashMap<>();
+
+ /**
+ * Create a new observables cache.
+ *
+ * @param func the function transforming a key into an observable
+ */
+ public ObservableCache(final Func1<K, Observable<V>> func) {
+ this.func = func;
+ }
+
+ /**
+ * Get the observable corresponding to a key. If the key has not already been
+ * seen, the function passed to the constructor will be called to build the observable
+ * <p/>
+ * If the observable has already emitted values, only the last one will be remembered.
+ *
+ * @param key the key
+ * @return the observable corresponding to the key
+ */
+ public synchronized Observable<V> get(final K key) {
+ if (cached.containsKey(key)) {
+ return cached.get(key);
+ }
+ final Observable<V> value = func.call(key).replay(1).refCount();
+ cached.put(key, value);
+ return value;
+ }
+
+ }
+
}
diff --git a/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java b/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java
index eee71ba..0743692 100644
--- a/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java
+++ b/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java
@@ -5,6 +5,7 @@ import cgeo.geocaching.activity.AbstractActivity;
import cgeo.geocaching.activity.Progress;
import android.content.res.Resources;
+import android.os.Bundle;
import android.os.Message;
import java.lang.ref.WeakReference;
@@ -21,7 +22,7 @@ public class SimpleCancellableHandler extends CancellableHandler {
@Override
protected void handleRegularMessage(final Message msg) {
- AbstractActivity activity = activityRef.get();
+ final AbstractActivity activity = activityRef.get();
if (activity != null && msg.getData() != null && msg.getData().getString(MESSAGE_TEXT) != null) {
activity.showToast(msg.getData().getString(MESSAGE_TEXT));
}
@@ -30,37 +31,37 @@ public class SimpleCancellableHandler extends CancellableHandler {
@Override
protected void handleCancel(final Object extra) {
- AbstractActivity activity = activityRef.get();
+ final AbstractActivity activity = activityRef.get();
if (activity != null) {
activity.showToast((String) extra);
}
dismissProgress();
}
- protected final void showToast(int resId) {
- AbstractActivity activity = activityRef.get();
+ protected final void showToast(final int resId) {
+ final AbstractActivity activity = activityRef.get();
if (activity != null) {
- Resources res = activity.getResources();
+ final Resources res = activity.getResources();
activity.showToast(res.getText(resId).toString());
}
}
protected final void dismissProgress() {
- Progress progressDialog = progressDialogRef.get();
+ final Progress progressDialog = progressDialogRef.get();
if (progressDialog != null) {
progressDialog.dismiss();
}
}
protected final void setProgressMessage(final String txt) {
- Progress progressDialog = progressDialogRef.get();
+ final Progress progressDialog = progressDialogRef.get();
if (progressDialog != null) {
progressDialog.setMessage(txt);
}
}
protected final void finishActivity() {
- AbstractActivity activity = activityRef.get();
+ final AbstractActivity activity = activityRef.get();
if (activity != null) {
activity.finish();
}
@@ -68,7 +69,7 @@ public class SimpleCancellableHandler extends CancellableHandler {
}
protected void updateStatusMsg(final int resId, final String msg) {
- CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get());
+ final CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get());
if (activity != null) {
setProgressMessage(activity.getResources().getString(resId)
+ "\n\n"
@@ -76,4 +77,15 @@ public class SimpleCancellableHandler extends CancellableHandler {
}
}
+ public void sendTextMessage(final int what, final int resId) {
+ final CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get());
+ if (activity != null) {
+ final Message msg = obtainMessage(what);
+ final Bundle bundle = new Bundle();
+ bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, activity.getResources().getString(resId));
+ msg.setData(bundle);
+ msg.sendToTarget();
+ }
+ }
+
}
diff --git a/main/src/cgeo/geocaching/utils/StartableHandlerThread.java b/main/src/cgeo/geocaching/utils/StartableHandlerThread.java
deleted file mode 100644
index 91ab1d0..0000000
--- a/main/src/cgeo/geocaching/utils/StartableHandlerThread.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package cgeo.geocaching.utils;
-
-import org.eclipse.jdt.annotation.NonNull;
-import rx.Subscriber;
-import rx.functions.Action0;
-import rx.subscriptions.Subscriptions;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-
-/**
- * Derivated class of {@link android.os.HandlerThread} with an exposed handler and a start/stop mechanism
- * based on subscriptions.
- */
-
-public class StartableHandlerThread extends HandlerThread {
-
- private final static int START = 1;
- private final static int STOP = 2;
-
- static public interface Callback {
- public void start(final Context context, final Handler handler);
- public void stop();
- }
-
- // The handler and the thread are intimely linked, there will be no leak.
- @SuppressLint("HandlerLeak")
- private class StartableHandler extends Handler {
- public StartableHandler() {
- super(StartableHandlerThread.this.getLooper());
- }
-
- @Override
- public void handleMessage(final Message message) {
- if (callback != null) {
- switch (message.what) {
- case START:
- callback.start((Context) message.obj, this);
- break;
- case STOP:
- callback.stop();
- break;
- }
- }
- }
- }
-
- private Handler handler;
- private Callback callback;
-
- public StartableHandlerThread(@NonNull final String name, final int priority, final Callback callback) {
- super(name, priority);
- this.callback = callback;
- }
-
- public StartableHandlerThread(@NonNull final String name, final int priority) {
- this(name, priority, null);
- }
-
- public synchronized Handler getHandler() {
- if (handler == null) {
- handler = new StartableHandler();
- }
- return handler;
- }
-
- public void start(final Subscriber<?> subscriber, final Context context) {
- getHandler().obtainMessage(START, context).sendToTarget();
- subscriber.add(Subscriptions.create(new Action0() {
- @Override
- public void call() {
- getHandler().sendEmptyMessage(STOP);
- }
- }));
- }
-
-}
diff --git a/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java b/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java
index 7848d1a..5963e2e 100644
--- a/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java
+++ b/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java
@@ -13,7 +13,7 @@ public class SynchronizedDateFormat {
format = new SimpleDateFormat(pattern, locale);
}
- public SynchronizedDateFormat(String pattern, TimeZone timeZone, Locale locale) {
+ public SynchronizedDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {
format = new SimpleDateFormat(pattern, locale);
format.setTimeZone(timeZone);
}
diff --git a/main/src/cgeo/geocaching/utils/TextUtils.java b/main/src/cgeo/geocaching/utils/TextUtils.java
index 77aa167..1f14f8d 100644
--- a/main/src/cgeo/geocaching/utils/TextUtils.java
+++ b/main/src/cgeo/geocaching/utils/TextUtils.java
@@ -8,6 +8,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.eclipse.jdt.annotation.Nullable;
import java.nio.charset.Charset;
+import java.text.Collator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
@@ -27,11 +28,11 @@ public final class TextUtils {
}
/**
- * Searches for the pattern p in the data. If the pattern is not found defaultValue is returned
+ * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned
*
* @param data
* Data to search in
- * @param p
+ * @param pattern
* Pattern to search for
* @param trim
* Set to true if the group found should be trim'ed
@@ -44,37 +45,38 @@ public final class TextUtils {
* @return defaultValue or the n-th group if the pattern matches (trimmed if wanted)
*/
@SuppressFBWarnings("DM_STRING_CTOR")
- public static String getMatch(@Nullable final String data, final Pattern p, final boolean trim, final int group, final String defaultValue, final boolean last) {
+ public static String getMatch(@Nullable final String data, final Pattern pattern, final boolean trim, final int group, final String defaultValue, final boolean last) {
if (data != null) {
-
- String result = null;
- final Matcher matcher = p.matcher(data);
-
+ final Matcher matcher = pattern.matcher(data);
if (matcher.find()) {
- result = matcher.group(group);
- }
- if (null != result) {
- final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(result);
- result = remover.replaceAll(" ");
+ String result = matcher.group(group);
+ while (last && matcher.find()) {
+ result = matcher.group(group);
+ }
- return trim ? new String(result).trim() : new String(result);
- // Java copies the whole page String, when matching with regular expressions
- // later this would block the garbage collector, as we only need tiny parts of the page
- // see http://developer.android.com/reference/java/lang/String.html#backing_array
- // Thus the creating of a new String via String constructor is necessary here!!
+ if (result != null) {
+ final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(result);
+ result = remover.replaceAll(" ");
- // And BTW: You cannot even see that effect in the debugger, but must use a separate memory profiler!
+ // Some versions of Java copy the whole page String, when matching with regular expressions
+ // later this would block the garbage collector, as we only need tiny parts of the page
+ // see http://developer.android.com/reference/java/lang/String.html#backing_array
+ // Thus the creating of a new String via String constructor is voluntary here!!
+ // And BTW: You cannot even see that effect in the debugger, but must use a separate memory profiler!
+ return trim ? new String(result).trim() : new String(result);
+ }
}
}
+
return defaultValue;
}
/**
- * Searches for the pattern p in the data. If the pattern is not found defaultValue is returned
+ * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned
*
* @param data
* Data to search in
- * @param p
+ * @param pattern
* Pattern to search for
* @param trim
* Set to true if the group found should be trim'ed
@@ -82,38 +84,33 @@ public final class TextUtils {
* Value to return if the pattern is not found
* @return defaultValue or the first group if the pattern matches (trimmed if wanted)
*/
- public static String getMatch(final String data, final Pattern p, final boolean trim, final String defaultValue) {
- return TextUtils.getMatch(data, p, trim, 1, defaultValue, false);
+ public static String getMatch(final String data, final Pattern pattern, final boolean trim, final String defaultValue) {
+ return TextUtils.getMatch(data, pattern, trim, 1, defaultValue, false);
}
/**
- * Searches for the pattern p in the data. If the pattern is not found defaultValue is returned
+ * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned
*
* @param data
* Data to search in
- * @param p
+ * @param pattern
* Pattern to search for
* @param defaultValue
* Value to return if the pattern is not found
* @return defaultValue or the first group if the pattern matches (trimmed)
*/
- public static String getMatch(@Nullable final String data, final Pattern p, final String defaultValue) {
- return TextUtils.getMatch(data, p, true, 1, defaultValue, false);
+ public static String getMatch(@Nullable final String data, final Pattern pattern, final String defaultValue) {
+ return TextUtils.getMatch(data, pattern, true, 1, defaultValue, false);
}
/**
- * Searches for the pattern p in the data.
+ * Searches for the pattern pattern in the data.
*
- * @param data
- * @param p
- * @return true if data contains the pattern p
+ * @return true if data contains the pattern pattern
*/
- public static boolean matches(final String data, final Pattern p) {
- if (data == null) {
- return false;
- }
+ public static boolean matches(final String data, final Pattern pattern) {
// matcher is faster than String.contains() and more flexible - it takes patterns instead of fixed texts
- return p.matcher(data).find();
+ return data != null && pattern.matcher(data).find();
}
@@ -165,8 +162,6 @@ public final class TextUtils {
* Remove all control characters (which are not valid in XML or HTML), as those should not appear in cache texts
* anyway
*
- * @param input
- * @return
*/
public static String removeControlCharacters(final String input) {
final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(input);
@@ -182,7 +177,19 @@ public final class TextUtils {
*/
public static long checksum(final String input) {
final CRC32 checksum = new CRC32();
- checksum.update(input.getBytes());
+ checksum.update(input.getBytes(CHARSET_UTF8));
return checksum.getValue();
}
+
+ /**
+ * Build a Collator instance appropriate for comparing strings using the default locale while ignoring the casing.
+ *
+ * @return a collator
+ */
+ public static Collator getCollator() {
+ final Collator collator = Collator.getInstance();
+ collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+ collator.setStrength(Collator.TERTIARY);
+ return collator;
+ }
}
diff --git a/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java b/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java
index 3cb4f16..d518ac8 100644
--- a/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java
+++ b/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java
@@ -22,8 +22,8 @@ public class UnknownTagsHandler implements TagHandler {
private ListType listType = ListType.Unordered;
@Override
- public void handleTag(boolean opening, String tag, Editable output,
- XMLReader xmlReader) {
+ public void handleTag(final boolean opening, final String tag, final Editable output,
+ final XMLReader xmlReader) {
if (tag.equalsIgnoreCase("strike") || tag.equals("s")) {
handleStrike(opening, output);
} else if (tag.equalsIgnoreCase("table")) {
@@ -41,7 +41,7 @@ public class UnknownTagsHandler implements TagHandler {
}
}
- private void handleStrike(boolean opening, Editable output) {
+ private void handleStrike(final boolean opening, final Editable output) {
final int length = output.length();
if (opening) {
strikePos = length;
@@ -61,7 +61,7 @@ public class UnknownTagsHandler implements TagHandler {
problematicDetected = true;
}
- private void handleTd(boolean opening, Editable output) {
+ private void handleTd(final boolean opening, final Editable output) {
// insert bar for each table column, see https://en.wikipedia.org/wiki/Box-drawing_characters
if (opening) {
if (countCells++ > 0) {
@@ -70,7 +70,7 @@ public class UnknownTagsHandler implements TagHandler {
}
}
- private void handleTr(boolean opening, Editable output) {
+ private void handleTr(final boolean opening, final Editable output) {
// insert new line for each table row
if (opening) {
output.append('\n');
@@ -80,7 +80,7 @@ public class UnknownTagsHandler implements TagHandler {
// Ordered lists are handled in a simple manner. They are rendered as Arabic numbers starting at 1
// with no handling for alpha or Roman numbers or arbitrary numbering.
- private void handleOl(boolean opening) {
+ private void handleOl(final boolean opening) {
if (opening) {
listIndex = 1;
listType = ListType.Ordered;
@@ -89,7 +89,7 @@ public class UnknownTagsHandler implements TagHandler {
}
}
- private void handleLi(boolean opening, Editable output) {
+ private void handleLi(final boolean opening, final Editable output) {
if (opening) {
if (listType == ListType.Ordered) {
output.append("\n ").append(String.valueOf(listIndex++)).append(". ");
diff --git a/main/src/cgeo/geocaching/utils/XmlUtils.java b/main/src/cgeo/geocaching/utils/XmlUtils.java
index c36fb53..004fd1b 100644
--- a/main/src/cgeo/geocaching/utils/XmlUtils.java
+++ b/main/src/cgeo/geocaching/utils/XmlUtils.java
@@ -17,7 +17,6 @@ public final class XmlUtils {
* @param prefix an XML prefix, see {@link XmlSerializer#startTag(String, String)}
* @param tag an XML tag
* @param text some text to insert, or <tt>null</tt> to omit completely this tag
- * @throws IOException
*/
public static void simpleText(final XmlSerializer serializer, final String prefix, final String tag, final String text) throws IOException {
if (text != null) {
@@ -34,7 +33,6 @@ public final class XmlUtils {
* @param prefix an XML prefix, see {@link XmlSerializer#startTag(String, String)} shared by all tags
* @param tagAndText an XML tag, the corresponding text, another XML tag, the corresponding text. <tt>null</tt> texts
* will be omitted along with their respective tag.
- * @throws IOException
*/
public static void multipleTexts(final XmlSerializer serializer, final String prefix, final String... tagAndText) throws IOException {
for (int i = 0; i < tagAndText.length; i += 2) {