diff options
Diffstat (limited to 'main/src/cgeo/geocaching/utils')
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) { |