diff options
Diffstat (limited to 'main/src')
175 files changed, 3085 insertions, 2742 deletions
diff --git a/main/src/cgeo/calendar/CalendarAddon.java b/main/src/cgeo/calendar/CalendarAddon.java new file mode 100644 index 0000000..117fb9a --- /dev/null +++ b/main/src/cgeo/calendar/CalendarAddon.java @@ -0,0 +1,58 @@ +package cgeo.calendar; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; +import cgeo.geocaching.geopoint.GeopointFormatter; +import cgeo.geocaching.network.Parameters; +import cgeo.geocaching.ui.dialog.Dialogs; +import cgeo.geocaching.utils.ProcessUtils; + +import org.apache.commons.lang3.StringUtils; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; + +import java.util.Date; + +public class CalendarAddon { + public static boolean isAvailable() { + return ProcessUtils.isIntentAvailable(ICalendar.INTENT, Uri.parse(ICalendar.URI_SCHEME + "://" + ICalendar.URI_HOST)); + } + + public static void addToCalendarWithIntent(final Activity activity, final Geocache cache) { + final Resources res = activity.getResources(); + if (CalendarAddon.isAvailable()) { + final Date hiddenDate = cache.getHiddenDate(); + final Parameters params = new Parameters( + ICalendar.PARAM_NAME, cache.getName(), + ICalendar.PARAM_NOTE, StringUtils.defaultString(cache.getPersonalNote()), + ICalendar.PARAM_HIDDEN_DATE, hiddenDate != null ? String.valueOf(hiddenDate.getTime()) : StringUtils.EMPTY, + ICalendar.PARAM_URL, StringUtils.defaultString(cache.getUrl()), + ICalendar.PARAM_COORDS, cache.getCoords() == null ? "" : cache.getCoords().format(GeopointFormatter.Format.LAT_LON_DECMINUTE_RAW), + ICalendar.PARAM_LOCATION, StringUtils.defaultString(cache.getLocation()), + ICalendar.PARAM_SHORT_DESC, StringUtils.defaultString(cache.getShortDescription()), + ICalendar.PARAM_START_TIME_MINUTES, StringUtils.defaultString(cache.guessEventTimeMinutes()) + ); + + activity.startActivity(new Intent(ICalendar.INTENT, + Uri.parse(ICalendar.URI_SCHEME + "://" + ICalendar.URI_HOST + "?" + params.toString()))); + } else { + // Inform user the calendar add-on is not installed and let them get it from Google Play + Dialogs.confirmYesNo(activity, R.string.addon_missing_title, new StringBuilder(res.getString(R.string.helper_calendar_missing)) + .append(' ') + .append(res.getString(R.string.addon_download_prompt)) + .toString(), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(ICalendar.CALENDAR_ADDON_URI)); + activity.startActivity(intent); + } + }); + } + } + +} diff --git a/main/src/cgeo/calendar/ICalendar.java b/main/src/cgeo/calendar/ICalendar.java index 933d248..6ecb6d5 100644 --- a/main/src/cgeo/calendar/ICalendar.java +++ b/main/src/cgeo/calendar/ICalendar.java @@ -14,6 +14,6 @@ public interface ICalendar { static final String PARAM_NOTE = "note"; // personal note static final String PARAM_NAME = "name"; // cache name static final String PARAM_LOCATION = "location"; // cache location, or empty string - static final String PARAM_COORDS = "coords"; // cache coords, or empty string + static final String PARAM_COORDS = "coords"; // cache coordinates, or empty string static final String PARAM_START_TIME_MINUTES = "time"; // time of start } diff --git a/main/src/cgeo/contacts/IContacts.java b/main/src/cgeo/contacts/IContacts.java index d46a5a4..d68b78a 100644 --- a/main/src/cgeo/contacts/IContacts.java +++ b/main/src/cgeo/contacts/IContacts.java @@ -1,8 +1,6 @@ package cgeo.contacts; public interface IContacts { - static final String CALENDAR_ADDON_URI = "market://details?id=cgeo.contacts"; - static final String INTENT = "cgeo.contacts.FIND"; static final String URI_SCHEME = "find"; diff --git a/main/src/cgeo/geocaching/AbstractPopupActivity.java b/main/src/cgeo/geocaching/AbstractPopupActivity.java index 5f24030..38e37da 100644 --- a/main/src/cgeo/geocaching/AbstractPopupActivity.java +++ b/main/src/cgeo/geocaching/AbstractPopupActivity.java @@ -40,7 +40,7 @@ public abstract class AbstractPopupActivity extends AbstractActivity implements private final GeoDirHandler geoUpdate = new GeoDirHandler() { @Override - protected void updateGeoData(final IGeoData geo) { + public void updateGeoData(final IGeoData geo) { try { if (geo.getCoords() != null && cache != null && cache.getCoords() != null) { cacheDistance.setText(Units.getDistanceFromKilometers(geo.getCoords().distanceTo(cache.getCoords()))); diff --git a/main/src/cgeo/geocaching/CacheDetailActivity.java b/main/src/cgeo/geocaching/CacheDetailActivity.java index 4068e38..7e8ab28 100644 --- a/main/src/cgeo/geocaching/CacheDetailActivity.java +++ b/main/src/cgeo/geocaching/CacheDetailActivity.java @@ -3,6 +3,7 @@ package cgeo.geocaching; import butterknife.ButterKnife; import butterknife.InjectView; +import cgeo.calendar.CalendarAddon; import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.activity.AbstractViewPagerActivity; import cgeo.geocaching.activity.Progress; @@ -38,18 +39,14 @@ import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.ui.logs.CacheLogsViewCreator; import cgeo.geocaching.utils.CancellableHandler; -import cgeo.geocaching.utils.ClipboardUtils; import cgeo.geocaching.utils.CryptUtils; import cgeo.geocaching.utils.GeoDirHandler; -import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; -import cgeo.geocaching.utils.RunnableWithArgument; import cgeo.geocaching.utils.SimpleCancellableHandler; import cgeo.geocaching.utils.SimpleHandler; import cgeo.geocaching.utils.TextUtils; -import cgeo.geocaching.utils.TranslationUtils; import cgeo.geocaching.utils.UnknownTagsHandler; import org.apache.commons.collections4.CollectionUtils; @@ -57,6 +54,14 @@ import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; +import rx.android.observables.AndroidObservable; +import rx.functions.Action1; +import rx.schedulers.Schedulers; import android.R.color; import android.app.AlertDialog; @@ -71,7 +76,6 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -80,7 +84,6 @@ import android.text.Editable; import android.text.Html; import android.text.Spannable; import android.text.Spanned; -import android.text.format.DateUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; @@ -109,7 +112,6 @@ import android.widget.TextView.BufferType; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Locale; @@ -164,6 +166,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc private TextView cacheDistanceView; protected ImagesList imagesList; + private Subscription imagesSubscription; /** * waypoint selected in context menu. This variable will be gone when the waypoint context menu is a fragment. */ @@ -176,24 +179,15 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // set title in code, as the activity needs a hard coded title due to the intent filters setTitle(res.getString(R.string.cache)); - String geocode = null; - - // TODO Why can it happen that search is not null? onCreate should be called only once and it is not set before. - if (search != null) { - cache = search.getFirstCacheFromResult(LoadFlags.LOAD_ALL_DB_ONLY); - if (cache != null && cache.getGeocode() != null) { - geocode = cache.getGeocode(); - } - } - // get parameters final Bundle extras = getIntent().getExtras(); final Uri uri = getIntent().getData(); // try to get data from extras String name = null; + String geocode = null; String guid = null; - if (geocode == null && extras != null) { + if (extras != null) { geocode = extras.getString(Intents.EXTRA_GEOCODE); name = extras.getString(Intents.EXTRA_NAME); guid = extras.getString(Intents.EXTRA_GUID); @@ -327,6 +321,14 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } @Override + public void onDestroy() { + if (imagesList != null) { + imagesSubscription.unsubscribe(); + } + super.onDestroy(); + } + + @Override public void onStop() { if (cache != null) { cache.setChangeNotificationHandler(null); @@ -349,12 +351,12 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc assert view instanceof TextView; clickedItemText = ((TextView) view).getText(); final String itemTitle = (String) ((TextView) ((View) view.getParent()).findViewById(R.id.name)).getText(); - buildDetailsContextMenu(menu, itemTitle, true); + buildDetailsContextMenu(menu, clickedItemText, itemTitle, true); break; case R.id.shortdesc: assert view instanceof TextView; clickedItemText = ((TextView) view).getText(); - buildDetailsContextMenu(menu, res.getString(R.string.cache_description), false); + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_description), false); break; case R.id.longdesc: assert view instanceof TextView; @@ -365,22 +367,28 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } else { clickedItemText = shortDesc + "\n\n" + ((TextView) view).getText(); } - buildDetailsContextMenu(menu, res.getString(R.string.cache_description), false); + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_description), false); break; case R.id.personalnote: assert view instanceof TextView; clickedItemText = ((TextView) view).getText(); - buildDetailsContextMenu(menu, res.getString(R.string.cache_personal_note), true); + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_personal_note), true); break; case R.id.hint: assert view instanceof TextView; clickedItemText = ((TextView) view).getText(); - buildDetailsContextMenu(menu, res.getString(R.string.cache_hint), false); + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_hint), false); break; case R.id.log: assert view instanceof TextView; clickedItemText = ((TextView) view).getText(); - buildDetailsContextMenu(menu, res.getString(R.string.cache_logs), false); + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_logs), false); + break; + case R.id.date: // event date + assert view instanceof TextView; + clickedItemText = ((TextView) view).getText(); + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_event), true); + menu.findItem(R.id.menu_calendar).setVisible(cache.canBeAddedToCalendar()); break; case R.id.waypoint: menu.setHeaderTitle(selectedWaypoint.getName() + " (" + res.getString(R.string.waypoint) + ")"); @@ -406,41 +414,13 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } } - private void buildDetailsContextMenu(ContextMenu menu, String fieldTitle, boolean copyOnly) { - menu.setHeaderTitle(fieldTitle); - getMenuInflater().inflate(R.menu.details_context, menu); - menu.findItem(R.id.menu_translate_to_sys_lang).setVisible(!copyOnly); - if (!copyOnly) { - if (clickedItemText.length() > TranslationUtils.TRANSLATION_TEXT_LENGTH_WARN) { - showToast(res.getString(R.string.translate_length_warning)); - } - menu.findItem(R.id.menu_translate_to_sys_lang).setTitle(res.getString(R.string.translate_to_sys_lang, Locale.getDefault().getDisplayLanguage())); - } - final boolean localeIsEnglish = StringUtils.equals(Locale.getDefault().getLanguage(), Locale.ENGLISH.getLanguage()); - menu.findItem(R.id.menu_translate_to_english).setVisible(!copyOnly && !localeIsEnglish); - } - @Override public boolean onContextItemSelected(MenuItem item) { + if (onClipboardItemSelected(item, clickedItemText)) { + return true; + } switch (item.getItemId()) { - // detail fields - case R.id.menu_copy: - ClipboardUtils.copyToClipboard(clickedItemText); - showToast(res.getString(R.string.clipboard_copy_ok)); - return true; - case R.id.menu_translate_to_sys_lang: - TranslationUtils.startActivityTranslate(this, Locale.getDefault().getLanguage(), HtmlUtils.extractText(clickedItemText)); - return true; - case R.id.menu_translate_to_english: - TranslationUtils.startActivityTranslate(this, Locale.ENGLISH.getLanguage(), HtmlUtils.extractText(clickedItemText)); - return true; - case R.id.menu_cache_share_field: - final Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, clickedItemText.toString()); - startActivity(Intent.createChooser(intent, res.getText(R.string.cache_share_field))); - return true; - // waypoints + // waypoints case R.id.menu_waypoint_edit: if (selectedWaypoint != null) { EditWaypointActivity.startActivityEditWaypoint(this, cache, selectedWaypoint.getId()); @@ -484,6 +464,9 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc new ResetCoordsThread(cache, handler, selectedWaypoint, true, false, progressDialog).start(); } return true; + case R.id.menu_calendar: + CalendarAddon.addToCalendarWithIntent(this, cache); + return true; default: break; } @@ -542,20 +525,19 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (UPDATE_LOAD_PROGRESS_DETAIL == msg.what && msg.obj instanceof String) { updateStatusMsg((String) msg.obj); } else { - CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get()); + final CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get()); if (activity == null) { return; } - SearchResult search = activity.getSearch(); - if (search == null) { + if (activity.search == null) { showToast(R.string.err_dwld_details_failed); dismissProgress(); finishActivity(); return; } - if (search.getError() != null) { - activity.showToast(activity.getResources().getString(R.string.err_dwld_details_failed) + " " + search.getError().getErrorString(activity.getResources()) + "."); + if (activity.search.getError() != null) { + activity.showToast(activity.getResources().getString(R.string.err_dwld_details_failed) + " " + activity.search.getError().getErrorString(activity.getResources()) + "."); dismissProgress(); finishActivity(); return; @@ -691,7 +673,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return; } imagesList = new ImagesList(this, cache.getGeocode()); - imagesList.loadImages(imageView, cache.getImages(), false); + imagesSubscription = imagesList.loadImages(imageView, cache.getImages(), false); } public static void startActivity(final Context context, final String geocode) { @@ -922,9 +904,17 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc view = (ScrollView) getLayoutInflater().inflate(R.layout.cachedetail_details_page, null); // Start loading preview map - if (Settings.isStoreOfflineMaps()) { - new PreviewMapTask().execute((Void) null); - } + AndroidObservable.fromActivity(CacheDetailActivity.this, previewMap.subscribeOn(Schedulers.io())).subscribe(new Action1<BitmapDrawable>() { + @Override + public void call(final BitmapDrawable image) { + final Bitmap bitmap = image.getBitmap(); + if (bitmap != null && bitmap.getWidth() > 10) { + final ImageView imageView = (ImageView) view.findViewById(R.id.map_preview); + imageView.setImageDrawable(image); + view.findViewById(R.id.map_preview_box).setVisibility(View.VISIBLE); + } + } + }); detailsList = (LinearLayout) view.findViewById(R.id.details_list); final CacheDetailsCreator details = new CacheDetailsCreator(CacheDetailActivity.this, detailsList); @@ -972,17 +962,10 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc ownerView.setOnClickListener(new OwnerActionsClickListener(cache)); } - // cache hidden - final Date hiddenDate = cache.getHiddenDate(); - if (hiddenDate != null) { - final long time = hiddenDate.getTime(); - if (time > 0) { - String dateString = Formatter.formatFullDate(time); - if (cache.isEventCache()) { - dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString; - } - details.add(cache.isEventCache() ? R.string.cache_event : R.string.cache_hidden, dateString); - } + // hidden or event date + final TextView hiddenView = details.addHiddenDate(cache); + if (hiddenView != null) { + registerForContextMenu(hiddenView); } // cache location @@ -1051,9 +1034,9 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (Settings.getChooseList()) { // let user select list to store cache in new StoredList.UserInterface(CacheDetailActivity.this).promptForListSelection(R.string.list_title, - new RunnableWithArgument<Integer>() { + new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { storeCache(selectedListId, new StoreCacheHandler(CacheDetailActivity.this, progress)); } }, true, StoredList.TEMPORARY_LIST_ID); @@ -1302,9 +1285,9 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public void onClick(View view) { new StoredList.UserInterface(CacheDetailActivity.this).promptForListSelection(R.string.list_title, - new RunnableWithArgument<Integer>() { + new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { switchListById(selectedListId); } }, true, cache.getListId()); @@ -1368,7 +1351,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc final LinearLayout layout = (LinearLayout) view.findViewById(R.id.favpoint_box); final boolean supportsFavoritePoints = cache.supportsFavoritePoints(); layout.setVisibility(supportsFavoritePoints ? View.VISIBLE : View.GONE); - if (!supportsFavoritePoints || cache.isOwner() || !Settings.isPremiumMember()) { + if (!supportsFavoritePoints || cache.isOwner() || !Settings.isGCPremiumMember()) { return; } final Button buttonAdd = (Button) view.findViewById(R.id.add_to_favpoint); @@ -1418,58 +1401,41 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc box.setVisibility(View.GONE); } } + } - private class PreviewMapTask extends AsyncTask<Void, Void, BitmapDrawable> { - @Override - protected BitmapDrawable doInBackground(Void... parameters) { - try { - // persistent preview from storage - Bitmap image = decode(cache); + private Observable<BitmapDrawable> previewMap = Observable.create(new OnSubscribe<BitmapDrawable>() { + @Override + public void call(final Subscriber<? super BitmapDrawable> subscriber) { + try { + // persistent preview from storage + Bitmap image = StaticMapsProvider.getPreviewMap(cache); - if (image == null) { + if (image == null) { + if (Settings.isStoreOfflineMaps()) { StaticMapsProvider.storeCachePreviewMap(cache); - image = decode(cache); - if (image == null) { - return null; - } + image = StaticMapsProvider.getPreviewMap(cache); } - - return ImageUtils.scaleBitmapToFitDisplay(image); - } catch (final Exception e) { - Log.w("CacheDetailActivity.PreviewMapTask", e); - return null; } - } - private Bitmap decode(final Geocache cache) { - return StaticMapsProvider.getPreviewMap(cache.getGeocode()); - } - - @Override - protected void onPostExecute(BitmapDrawable image) { - if (image == null) { - return; - } - - try { - final Bitmap bitmap = image.getBitmap(); - if (bitmap == null || bitmap.getWidth() <= 10) { - return; - } - - final ImageView imageView = (ImageView) view.findViewById(R.id.map_preview); - imageView.setImageDrawable(image); - view.findViewById(R.id.map_preview_box).setVisibility(View.VISIBLE); - } catch (final Exception e) { - Log.e("CacheDetailActivity.PreviewMapTask", e); + if (image != null) { + subscriber.onNext(ImageUtils.scaleBitmapToFitDisplay(image)); } + subscriber.onCompleted(); + } catch (final Exception e) { + Log.w("CacheDetailActivity.previewMap", e); + subscriber.onError(e); } } - } + + }); protected class DescriptionViewCreator extends AbstractCachingPageViewCreator<ScrollView> { @InjectView(R.id.personalnote) protected TextView personalNoteView; + @InjectView(R.id.shortdesc) protected IndexOutOfBoundsAvoidingTextView shortDescView; + @InjectView(R.id.longdesc) protected IndexOutOfBoundsAvoidingTextView longDescView; + @InjectView(R.id.show_description) protected Button showDesc; + @InjectView(R.id.loading) protected View loadingView; @Override public ScrollView getDispatchedView() { @@ -1483,7 +1449,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // cache short description if (StringUtils.isNotBlank(cache.getShortDescription())) { - new LoadDescriptionTask(cache.getShortDescription(), view.findViewById(R.id.shortdesc), null, null).execute(); + loadDescription(cache.getShortDescription(), shortDescView, null); } // long description @@ -1491,7 +1457,6 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (Settings.isAutoLoadDescription()) { loadLongDescription(); } else { - final Button showDesc = (Button) view.findViewById(R.id.show_description); showDesc.setVisibility(View.VISIBLE); showDesc.setOnClickListener(new View.OnClickListener() { @Override @@ -1605,12 +1570,23 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } private void loadLongDescription() { - final Button showDesc = (Button) view.findViewById(R.id.show_description); showDesc.setVisibility(View.GONE); showDesc.setOnClickListener(null); view.findViewById(R.id.loading).setVisibility(View.VISIBLE); - new LoadDescriptionTask(cache.getDescription(), view.findViewById(R.id.longdesc), view.findViewById(R.id.loading), view.findViewById(R.id.shortdesc)).execute(); + final String longDescription = cache.getDescription(); + loadDescription(longDescription, longDescView, loadingView); + + // Hide the short description, if it is contained somewhere at the start of the long description. + if (shortDescView != null) { + final String shortDescription = cache.getShortDescription(); + if (StringUtils.isNotBlank(shortDescription)) { + final int index = longDescription.indexOf(shortDescription); + if (index >= 0 && index < 200) { + shortDescView.setVisibility(View.GONE); + } + } + } } private void warnPersonalNoteNeedsStoring() { @@ -1641,148 +1617,118 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } - /** - * Loads the description in background. <br /> - * <br /> - * Params: - * <ol> - * <li>description string (String)</li> - * <li>target description view (TextView)</li> - * <li>loading indicator view (View, may be null)</li> - * </ol> - */ - private class LoadDescriptionTask extends AsyncTask<Object, Void, Void> { - private final View loadingIndicatorView; - private final IndexOutOfBoundsAvoidingTextView descriptionView; - private final String descriptionString; - private Spanned description; - private final View shortDescView; + /** + * Load the description in the background. + * @param descriptionString the HTML description as retrieved from the connector + * @param descriptionView the view to fill + * @param loadingIndicatorView the loading indicator view, will be hidden when completed + */ + private void loadDescription(final String descriptionString, final IndexOutOfBoundsAvoidingTextView descriptionView, final View loadingIndicatorView) { + // The producer produces successives (without then with images) versions of the description. + final Observable<Spanned> producer = Observable.create(new OnSubscribe<Spanned>() { + @Override + public void call(final Subscriber<? super Spanned> subscriber) { + try { + // Fast preview: parse only HTML without loading any images + final HtmlImageCounter imageCounter = new HtmlImageCounter(); + final UnknownTagsHandler unknownTagsHandler = new UnknownTagsHandler(); + Spanned description = Html.fromHtml(descriptionString, imageCounter, unknownTagsHandler); + addWarning(unknownTagsHandler, description); + subscriber.onNext(description); + + if (imageCounter.getImageCount() > 0) { + // Complete view: parse again with loading images - if necessary ! If there are any images causing problems the user can see at least the preview + description = Html.fromHtml(descriptionString, new HtmlImage(cache.getGeocode(), true, cache.getListId(), false), unknownTagsHandler); + addWarning(unknownTagsHandler, description); + subscriber.onNext(description); + } - public LoadDescriptionTask(final String description, final View descriptionView, final View loadingIndicatorView, final View shortDescView) { - assert descriptionView instanceof IndexOutOfBoundsAvoidingTextView; - this.descriptionString = description; - this.descriptionView = (IndexOutOfBoundsAvoidingTextView) descriptionView; - this.loadingIndicatorView = loadingIndicatorView; - this.shortDescView = shortDescView; - } + subscriber.onCompleted(); + } catch (final Exception e) { + Log.e("loadDescription", e); + subscriber.onError(e); + } + } - @Override - protected Void doInBackground(Object... params) { - try { - // Fast preview: parse only HTML without loading any images - final HtmlImageCounter imageCounter = new HtmlImageCounter(); - final UnknownTagsHandler unknownTagsHandler = new UnknownTagsHandler(); - description = Html.fromHtml(descriptionString, imageCounter, unknownTagsHandler); - publishProgress(); - - boolean needsRefresh = false; - if (imageCounter.getImageCount() > 0) { - // Complete view: parse again with loading images - if necessary ! If there are any images causing problems the user can see at least the preview - description = Html.fromHtml(descriptionString, new HtmlImage(cache.getGeocode(), true, cache.getListId(), false), unknownTagsHandler); - needsRefresh = true; - } - - // If description has an HTML construct which may be problematic to render, add a note at the end of the long description. - // Technically, it may not be a table, but a pre, which has the same problems as a table, so the message is ok even though - // sometimes technically incorrect. - if (unknownTagsHandler.isProblematicDetected() && descriptionView != null) { + // If description has an HTML construct which may be problematic to render, add a note at the end of the long description. + // Technically, it may not be a table, but a pre, which has the same problems as a table, so the message is ok even though + // sometimes technically incorrect. + private void addWarning(final UnknownTagsHandler unknownTagsHandler, final Spanned description) { + if (unknownTagsHandler.isProblematicDetected()) { final int startPos = description.length(); final IConnector connector = ConnectorFactory.getConnector(cache); final Spanned tableNote = Html.fromHtml(res.getString(R.string.cache_description_table_note, "<a href=\"" + cache.getUrl() + "\">" + connector.getName() + "</a>")); ((Editable) description).append("\n\n").append(tableNote); ((Editable) description).setSpan(new StyleSpan(Typeface.ITALIC), startPos, description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - needsRefresh = true; - } - - if (needsRefresh) { - publishProgress(); - } - } catch (final Exception e) { - Log.e("LoadDescriptionTask: ", e); - } - return null; - } - - @Override - protected void onProgressUpdate(Void... values) { - if (description == null) { - showToast(res.getString(R.string.err_load_descr_failed)); - return; - } - if (StringUtils.isNotBlank(descriptionString)) { - try { - descriptionView.setText(description, TextView.BufferType.SPANNABLE); - } catch (final Exception e) { - // On 4.1, there is sometimes a crash on measuring the layout: https://code.google.com/p/android/issues/detail?id=35412 - Log.e("Android bug setting text: ", e); - // remove the formatting by converting to a simple string - descriptionView.setText(description.toString()); } - descriptionView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); - fixTextColor(descriptionView, descriptionString); - descriptionView.setVisibility(View.VISIBLE); - registerForContextMenu(descriptionView); - - hideDuplicatedShortDescription(); } - } + }); - /** - * Hide the short description, if it is contained somewhere at the start of the long description. - */ - private void hideDuplicatedShortDescription() { - if (shortDescView != null) { - final String shortDescription = cache.getShortDescription(); - if (StringUtils.isNotBlank(shortDescription)) { - final int index = descriptionString.indexOf(shortDescription); - if (index >= 0 && index < 200) { - shortDescView.setVisibility(View.GONE); + AndroidObservable.fromActivity(this, producer.subscribeOn(Schedulers.io())) + .subscribe(new Observer<Spanned>() { + @Override + public void onCompleted() { + if (null != loadingIndicatorView) { + loadingIndicatorView.setVisibility(View.GONE); + } } - } - } - } - @Override - protected void onPostExecute(Void result) { - if (null != loadingIndicatorView) { - loadingIndicatorView.setVisibility(View.GONE); - } - } + @Override + public void onError(final Throwable throwable) { + showToast(res.getString(R.string.err_load_descr_failed)); + } - /** - * Handle caches with black font color in dark skin and white font color in light skin - * by changing background color of the view - * - * @param view - * containing the text - * @param text - * to be checked - */ - private void fixTextColor(final TextView view, final String text) { - int backcolor; - if (Settings.isLightSkin()) { - backcolor = color.white; - - for (final Pattern pattern : LIGHT_COLOR_PATTERNS) { - final MatcherWrapper matcher = new MatcherWrapper(pattern, text); - if (matcher.find()) { - view.setBackgroundResource(color.darker_gray); - return; + @Override + public void onNext(final Spanned description) { + if (StringUtils.isNotBlank(descriptionString)) { + try { + descriptionView.setText(description, TextView.BufferType.SPANNABLE); + } catch (final Exception e) { + // On 4.1, there is sometimes a crash on measuring the layout: https://code.google.com/p/android/issues/detail?id=35412 + Log.e("Android bug setting text: ", e); + // remove the formatting by converting to a simple string + descriptionView.setText(description.toString()); + } + descriptionView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); + fixTextColor(descriptionString); + descriptionView.setVisibility(View.VISIBLE); + registerForContextMenu(descriptionView); + } } - } - } else { - backcolor = color.black; - for (final Pattern pattern : DARK_COLOR_PATTERNS) { - final MatcherWrapper matcher = new MatcherWrapper(pattern, text); - if (matcher.find()) { - view.setBackgroundResource(color.darker_gray); - return; + /** + * Handle caches with black font color in dark skin and white font color in light skin + * by changing background color of the view + * + * @param text + * to be checked + */ + private void fixTextColor(final String text) { + int backcolor; + if (Settings.isLightSkin()) { + backcolor = color.white; + + for (final Pattern pattern : LIGHT_COLOR_PATTERNS) { + final MatcherWrapper matcher = new MatcherWrapper(pattern, text); + if (matcher.find()) { + descriptionView.setBackgroundResource(color.darker_gray); + return; + } + } + } else { + backcolor = color.black; + + for (final Pattern pattern : DARK_COLOR_PATTERNS) { + final MatcherWrapper matcher = new MatcherWrapper(pattern, text); + if (matcher.find()) { + descriptionView.setBackgroundResource(color.darker_gray); + return; + } + } + } + descriptionView.setBackgroundResource(backcolor); } - } - } - view.setBackgroundResource(backcolor); - } + }); } private class WaypointsViewCreator extends AbstractCachingPageViewCreator<ListView> { @@ -2255,10 +2201,6 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return cache; } - public SearchResult getSearch() { - return search; - } - private static class StoreCacheHandler extends SimpleCancellableHandler { public StoreCacheHandler(CacheDetailActivity activity, Progress progress) { diff --git a/main/src/cgeo/geocaching/CacheListActivity.java b/main/src/cgeo/geocaching/CacheListActivity.java index cc8b178..fa84ac6 100644 --- a/main/src/cgeo/geocaching/CacheListActivity.java +++ b/main/src/cgeo/geocaching/CacheListActivity.java @@ -8,7 +8,7 @@ import cgeo.geocaching.activity.Progress; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; import cgeo.geocaching.apps.cachelist.CacheListAppFactory; import cgeo.geocaching.compatibility.Compatibility; -import cgeo.geocaching.connector.gc.SearchHandler; +import cgeo.geocaching.connector.gc.RecaptchaHandler; import cgeo.geocaching.enumerations.CacheListType; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; @@ -18,11 +18,13 @@ import cgeo.geocaching.files.GPXImporter; import cgeo.geocaching.filter.FilterUserInterface; import cgeo.geocaching.filter.IFilter; import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.loaders.AbstractSearchLoader; import cgeo.geocaching.loaders.AbstractSearchLoader.CacheListLoaderType; import cgeo.geocaching.loaders.AddressGeocacheListLoader; import cgeo.geocaching.loaders.CoordsGeocacheListLoader; +import cgeo.geocaching.loaders.FinderGeocacheListLoader; import cgeo.geocaching.loaders.HistoryGeocacheListLoader; import cgeo.geocaching.loaders.KeywordGeocacheListLoader; import cgeo.geocaching.loaders.NextPageGeocacheListLoader; @@ -30,7 +32,6 @@ import cgeo.geocaching.loaders.OfflineGeocacheListLoader; import cgeo.geocaching.loaders.OwnerGeocacheListLoader; import cgeo.geocaching.loaders.PocketGeocacheListLoader; import cgeo.geocaching.loaders.RemoveFromHistoryLoader; -import cgeo.geocaching.loaders.FinderGeocacheListLoader; import cgeo.geocaching.maps.CGeoMap; import cgeo.geocaching.network.Cookies; import cgeo.geocaching.network.Network; @@ -43,10 +44,10 @@ import cgeo.geocaching.ui.LoggingUI; import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.AsyncTaskWithProgress; +import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.DateUtils; import cgeo.geocaching.utils.GeoDirHandler; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; import ch.boye.httpclientandroidlib.HttpResponse; @@ -55,6 +56,8 @@ import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Action1; + import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; @@ -93,7 +96,11 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA private static final int MAX_LIST_ITEMS = 1000; private static final int MSG_DONE = -1; - private static final int MSG_CANCEL = -99; + private static final int MSG_SERVER_FAIL = -2; + private static final int MSG_NO_REGISTRATION = -3; + private static final int MSG_WAITING = 0; + private static final int MSG_LOADING = 1; + private static final int MSG_LOADED = 2; private static final int REQUEST_CODE_IMPORT_GPX = 1; @@ -111,8 +118,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA private int detailTotal = 0; private int detailProgress = 0; private long detailProgressTime = 0L; - private LoadDetailsThread threadDetails = null; - private LoadFromWebThread threadWeb = null; private int listId = StoredList.TEMPORARY_LIST_ID; // Only meaningful for the OFFLINE type private final GeoDirHandler geoDirHandler = new GeoDirHandler() { @@ -267,10 +272,10 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } } - private final Handler loadDetailsHandler = new Handler() { + private final CancellableHandler loadDetailsHandler = new CancellableHandler() { @Override - public void handleMessage(Message msg) { + public void handleRegularMessage(Message msg) { setAdapter(); if (msg.what > -1) { @@ -287,10 +292,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } else { progress.setMessage(res.getString(R.string.caches_downloading) + " " + minutesRemaining + " " + res.getQuantityString(R.plurals.caches_eta_mins, minutesRemaining)); } - } else if (msg.what == MSG_CANCEL) { - if (threadDetails != null) { - threadDetails.kill(); - } } else { if (search != null) { final Set<Geocache> cacheListTmp = search.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB); @@ -304,13 +305,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA showProgress(false); progress.dismiss(); - - if (!isPaused()) { - // If the current activity has been paused, then we do not want to fiddle with the - // GPS and direction states. If the activity later gets resumed, its onResume() - // function will take care of turning the GPS back on. - startGeoAndDir(); - } } } }; @@ -318,54 +312,53 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA /** * TODO Possibly parts should be a Thread not a Handler */ - private final Handler downloadFromWebHandler = new Handler() { + private final CancellableHandler downloadFromWebHandler = new CancellableHandler() { @Override - public void handleMessage(Message msg) { + public void handleRegularMessage(Message msg) { setAdapter(); adapter.notifyDataSetChanged(); - if (msg.what == 0) { //no caches - progress.setMessage(res.getString(R.string.web_import_waiting)); - } else if (msg.what == 1) { //cache downloading - progress.setMessage(res.getString(R.string.web_downloading) + " " + msg.obj + '…'); - } else if (msg.what == 2) { //Cache downloaded - progress.setMessage(res.getString(R.string.web_downloaded) + " " + msg.obj + '…'); - refreshCurrentList(); - } else if (msg.what == -2) { - progress.dismiss(); - showToast(res.getString(R.string.sendToCgeo_download_fail)); - finish(); - } else if (msg.what == -3) { - progress.dismiss(); - showToast(res.getString(R.string.sendToCgeo_no_registration)); - finish(); - } else if (msg.what == MSG_CANCEL) { - if (threadWeb != null) { - threadWeb.kill(); - } - } else { - adapter.setSelectMode(false); - - replaceCacheListFromSearch(); - - progress.dismiss(); + switch (msg.what) { + case MSG_WAITING: //no caches + progress.setMessage(res.getString(R.string.web_import_waiting)); + break; + case MSG_LOADING: //cache downloading + progress.setMessage(res.getString(R.string.web_downloading) + " " + msg.obj + '…'); + break; + case MSG_LOADED: //Cache downloaded + progress.setMessage(res.getString(R.string.web_downloaded) + " " + msg.obj + '…'); + refreshCurrentList(); + break; + case MSG_SERVER_FAIL: + progress.dismiss(); + showToast(res.getString(R.string.sendToCgeo_download_fail)); + finish(); + break; + case MSG_NO_REGISTRATION: + progress.dismiss(); + showToast(res.getString(R.string.sendToCgeo_no_registration)); + finish(); + break; + default: // MSG_DONE + adapter.setSelectMode(false); + replaceCacheListFromSearch(); + progress.dismiss(); + break; } } }; - private final Handler clearOfflineLogsHandler = new Handler() { + private final CancellableHandler clearOfflineLogsHandler = new CancellableHandler() { @Override - public void handleMessage(Message msg) { - if (msg.what != MSG_CANCEL) { - adapter.setSelectMode(false); + public void handleRegularMessage(Message msg) { + adapter.setSelectMode(false); - refreshCurrentList(); + refreshCurrentList(); - replaceCacheListFromSearch(); + replaceCacheListFromSearch(); - progress.dismiss(); - } + progress.dismiss(); } }; @@ -402,7 +395,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA if (isInvokedFromAttachment()) { type = CacheListType.OFFLINE; if (coords == null) { - coords = new Geopoint(0.0, 0.0); + coords = Geopoint.ZERO; } } @@ -422,7 +415,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA prepareFilterBar(); - currentLoader = (AbstractSearchLoader) getSupportLoaderManager().initLoader(type.ordinal(), extras, this); + currentLoader = (AbstractSearchLoader) getSupportLoaderManager().initLoader(type.getLoaderId(), extras, this); // init if (CollectionUtils.isNotEmpty(cacheList)) { @@ -458,10 +451,10 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } private void importGpxAttachement() { - new StoredList.UserInterface(this).promptForListSelection(R.string.gpx_import_select_list_title, new RunnableWithArgument<Integer>() { + new StoredList.UserInterface(this).promptForListSelection(R.string.gpx_import_select_list_title, new Action1<Integer>() { @Override - public void run(Integer listId) { + public void call(Integer listId) { new GPXImporter(CacheListActivity.this, listId, importGpxAttachementFinishedHandler).importGPX(); switchListById(listId); } @@ -472,12 +465,15 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA public void onResume() { super.onResume(); - startGeoAndDir(); + geoDirHandler.startGeo(); + if (Settings.isLiveMap()) { + geoDirHandler.startDir(); + } adapter.setSelectMode(false); setAdapterCurrentCoordinates(true); - if (loadCachesHandler != null && search != null) { + if (search != null) { replaceCacheListFromSearch(); loadCachesHandler.sendEmptyMessage(0); } @@ -503,8 +499,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA @Override public void onPause() { - removeGeoAndDir(); - + geoDirHandler.stopGeoAndDir(); super.onPause(); } @@ -675,9 +670,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return true; case R.id.menu_sort: final CacheComparator oldComparator = adapter.getCacheComparator(); - new ComparatorUserInterface(this).selectComparator(oldComparator, new RunnableWithArgument<CacheComparator>() { + new ComparatorUserInterface(this).selectComparator(oldComparator, new Action1<CacheComparator>() { @Override - public void run(CacheComparator selectedComparator) { + public void call(CacheComparator selectedComparator) { // selecting the same sorting twice will toggle the order if (selectedComparator != null && oldComparator != null && selectedComparator.getClass().equals(oldComparator.getClass())) { adapter.toggleInverseSort(); @@ -728,7 +723,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } public void clearOfflineLogs() { - progress.show(this, null, res.getString(R.string.caches_clear_offlinelogs_progress), true, clearOfflineLogsHandler.obtainMessage(MSG_CANCEL)); + progress.show(this, null, res.getString(R.string.caches_clear_offlinelogs_progress), true, clearOfflineLogsHandler.cancelMessage()); new ClearOfflineLogsThread(clearOfflineLogsHandler).start(); } @@ -737,13 +732,12 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA */ @Override public void showFilterMenu(final View view) { - new FilterUserInterface(this).selectFilter(new RunnableWithArgument<IFilter>() { + new FilterUserInterface(this).selectFilter(new Action1<IFilter>() { @Override - public void run(IFilter selectedFilter) { + public void call(IFilter selectedFilter) { if (selectedFilter != null) { setFilter(selectedFilter); - } - else { + } else { // clear filter setFilter(null); } @@ -793,10 +787,10 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } private void moveCachesToOtherList() { - new StoredList.UserInterface(this).promptForListSelection(R.string.cache_menu_move_list, new RunnableWithArgument<Integer>() { + new StoredList.UserInterface(this).promptForListSelection(R.string.cache_menu_move_list, new Action1<Integer>() { @Override - public void run(Integer newListId) { + public void call(Integer newListId) { DataStore.moveToList(adapter.getCheckedOrAllCaches(), newListId); adapter.setSelectMode(false); @@ -850,10 +844,10 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA }); break; case R.id.menu_move_to_list: - new StoredList.UserInterface(this).promptForListSelection(R.string.cache_menu_move_list, new RunnableWithArgument<Integer>() { + new StoredList.UserInterface(this).promptForListSelection(R.string.cache_menu_move_list, new Action1<Integer>() { @Override - public void run(Integer newListId) { + public void call(Integer newListId) { DataStore.moveToList(Collections.singletonList(cache), newListId); adapter.setSelectMode(false); refreshCurrentList(); @@ -960,7 +954,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA boolean enableMore = (type != CacheListType.OFFLINE && cacheList.size() < MAX_LIST_ITEMS); if (enableMore && search != null) { final int count = search.getTotalCountGC(); - enableMore = enableMore && count > 0 && cacheList.size() < count; + enableMore = count > 0 && cacheList.size() < count; } if (enableMore) { @@ -973,17 +967,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA listFooter.setClickable(enableMore); } - private void startGeoAndDir() { - geoDirHandler.startGeo(); - if (Settings.isLiveMap()) { - geoDirHandler.startDir(); - } - } - - private void removeGeoAndDir() { - geoDirHandler.stopGeoAndDir(); - } - private void importGpx() { GpxFileListActivity.startSubActivity(this, listId); } @@ -1002,7 +985,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA // provided to this method as a parameter. Pull that uri using "resultData.getData()" if (data != null) { final Uri uri = data.getData(); - new GPXImporter(CacheListActivity.this, listId, importGpxAttachementFinishedHandler).importGPX(uri, null, getDisplayName(uri)); + new GPXImporter(this, listId, importGpxAttachementFinishedHandler).importGPX(uri, null, getDisplayName(uri)); } } @@ -1038,9 +1021,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA if (Settings.getChooseList() && type != CacheListType.OFFLINE) { // let user select list to store cache in new StoredList.UserInterface(this).promptForListSelection(R.string.list_title, - new RunnableWithArgument<Integer>() { + new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { refreshStored(caches, selectedListId); } }, true, StoredList.TEMPORARY_LIST_ID, newListName); @@ -1062,12 +1045,12 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA message = res.getString(R.string.caches_downloading) + " " + etaTime + " " + res.getQuantityString(R.plurals.caches_eta_mins, etaTime); } - progress.show(this, null, message, ProgressDialog.STYLE_HORIZONTAL, loadDetailsHandler.obtainMessage(MSG_CANCEL)); + progress.show(this, null, message, ProgressDialog.STYLE_HORIZONTAL, loadDetailsHandler.cancelMessage()); progress.setMaxProgressAndReset(detailTotal); detailProgressTime = System.currentTimeMillis(); - threadDetails = new LoadDetailsThread(loadDetailsHandler, caches, storeListId); + final LoadDetailsThread threadDetails = new LoadDetailsThread(loadDetailsHandler, caches, storeListId); threadDetails.start(); } @@ -1091,16 +1074,16 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } final Bundle b = new Bundle(); b.putStringArray(Intents.EXTRA_CACHELIST, geocodes); - getSupportLoaderManager().initLoader(CacheListLoaderType.REMOVE_FROM_HISTORY.ordinal(), b, this); + getSupportLoaderManager().initLoader(CacheListLoaderType.REMOVE_FROM_HISTORY.getLoaderId(), b, this); } public void importWeb() { detailProgress = 0; showProgress(false); - progress.show(this, null, res.getString(R.string.web_import_waiting), true, downloadFromWebHandler.obtainMessage(MSG_CANCEL)); + progress.show(this, null, res.getString(R.string.web_import_waiting), true, downloadFromWebHandler.cancelMessage()); - threadWeb = new LoadFromWebThread(downloadFromWebHandler, listId); + final LoadFromWebThread threadWeb = new LoadFromWebThread(downloadFromWebHandler, listId); threadWeb.start(); } @@ -1127,26 +1110,20 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA private class LoadDetailsThread extends Thread { - final private Handler handler; + final private CancellableHandler handler; final private int listIdLD; - private volatile boolean needToStop = false; final private List<Geocache> caches; - public LoadDetailsThread(Handler handlerIn, List<Geocache> caches, int listId) { - handler = handlerIn; + public LoadDetailsThread(CancellableHandler handler, List<Geocache> caches, int listId) { + this.handler = handler; this.caches = caches; // in case of online lists, set the list id to the standard list this.listIdLD = Math.max(listId, StoredList.STANDARD_LIST_ID); } - public void kill() { - needToStop = true; - } - @Override public void run() { - removeGeoAndDir(); // First refresh caches that do not yet have static maps to get them a chance to get a copy // before the limit expires, unless we do not want to store offline maps. final List<Geocache> allCaches = Settings.isStoreOfflineMaps() ? @@ -1173,7 +1150,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA */ private boolean refreshCache(Geocache cache) { try { - if (needToStop) { + if (handler.isCancelled()) { throw new InterruptedException("Stopped storing process."); } detailProgress++; @@ -1190,85 +1167,55 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } } - private class LoadFromWebThread extends Thread { + private static class LoadFromWebThread extends Thread { - final private Handler handler; + final private CancellableHandler handler; final private int listIdLFW; - private volatile boolean needToStop = false; - public LoadFromWebThread(Handler handlerIn, int listId) { - handler = handlerIn; + public LoadFromWebThread(CancellableHandler handler, int listId) { + this.handler = handler; listIdLFW = StoredList.getConcreteList(listId); } - public void kill() { - needToStop = true; - } - @Override public void run() { + final long startTime = System.currentTimeMillis(); - removeGeoAndDir(); - - int delay = -1; - int times = 0; - - int ret = MSG_DONE; - while (!needToStop && times < 3 * 60 / 5) { // maximum: 3 minutes, every 5 seconds - //download new code - String deviceCode = Settings.getWebDeviceCode(); - if (deviceCode == null) { - deviceCode = ""; - } - final Parameters params = new Parameters("code", deviceCode); + final String deviceCode = StringUtils.defaultString(Settings.getWebDeviceCode()); + final Parameters params = new Parameters("code", deviceCode); + while (!handler.isCancelled() && System.currentTimeMillis() - startTime < 3 * 60000) { // maximum: 3 minutes + // Download new code final HttpResponse responseFromWeb = Network.getRequest("http://send2.cgeo.org/read.html", params); if (responseFromWeb != null && responseFromWeb.getStatusLine().getStatusCode() == 200) { final String response = Network.getResponseData(responseFromWeb); if (response != null && response.length() > 2) { - delay = 1; - handler.sendMessage(handler.obtainMessage(1, response)); - yield(); + handler.sendMessage(handler.obtainMessage(MSG_LOADING, response)); Geocache.storeCache(null, response, listIdLFW, false, null); - handler.sendMessage(handler.obtainMessage(2, response)); - yield(); + handler.sendMessage(handler.obtainMessage(MSG_LOADED, response)); } else if ("RG".equals(response)) { //Server returned RG (registration) and this device no longer registered. Settings.setWebNameCode(null, null); - ret = -3; - needToStop = true; + handler.sendEmptyMessage(MSG_NO_REGISTRATION); + handler.cancel(); break; } else { - delay = 0; - handler.sendEmptyMessage(0); - yield(); + try { + sleep(5000); // Wait for 5s if no cache found + } catch (final InterruptedException e) { + } + handler.sendEmptyMessage(MSG_WAITING); } - } - if (responseFromWeb == null || responseFromWeb.getStatusLine().getStatusCode() != 200) { - ret = -2; - needToStop = true; + } else { + handler.sendEmptyMessage(MSG_SERVER_FAIL); + handler.cancel(); break; } - - try { - yield(); - if (delay == 0) { - sleep(5000); //No caches 5s - times++; - } else { - sleep(500); //Cache was loaded 0.5s - times = 0; - } - } catch (final InterruptedException e) { - Log.e("CacheListActivity.LoadFromWebThread.sleep", e); - } } - handler.sendEmptyMessage(ret); - - startGeoAndDir(); + handler.sendEmptyMessage(MSG_DONE); } } @@ -1283,9 +1230,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA @Override protected Void doInBackgroundInternal(Geocache[] caches) { - removeGeoAndDir(); DataStore.markDropped(Arrays.asList(caches)); - startGeoAndDir(); return null; } @@ -1328,7 +1273,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA showFooterLoadingCaches(); listFooter.setOnClickListener(null); - getSupportLoaderManager().restartLoader(CacheListLoaderType.NEXT_PAGE.ordinal(), null, CacheListActivity.this); + getSupportLoaderManager().restartLoader(CacheListLoaderType.NEXT_PAGE.getLoaderId(), null, CacheListActivity.this); } } @@ -1349,11 +1294,11 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } @NonNull - private RunnableWithArgument<Integer> getListSwitchingRunnable() { - return new RunnableWithArgument<Integer>() { + private Action1<Integer> getListSwitchingRunnable() { + return new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { switchListById(selectedListId); } }; @@ -1364,6 +1309,12 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } + if (id == PseudoList.HISTORY_LIST.id) { + CacheListActivity.startActivityHistory(this); + finish(); + return; + } + final StoredList list = DataStore.getList(id); if (list == null) { return; @@ -1378,7 +1329,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA showFooterLoadingCaches(); DataStore.moveToList(adapter.getCheckedCaches(), listId); - currentLoader = (OfflineGeocacheListLoader) getSupportLoaderManager().initLoader(CacheListType.OFFLINE.ordinal(), new Bundle(), this); + currentLoader = (OfflineGeocacheListLoader) getSupportLoaderManager().initLoader(CacheListType.OFFLINE.getLoaderId(), new Bundle(), this); currentLoader.reset(); ((OfflineGeocacheListLoader) currentLoader).setListId(listId); ((OfflineGeocacheListLoader) currentLoader).setSearchCenter(coords); @@ -1433,7 +1384,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA * @param view * unused here but needed since this method is referenced from XML layout */ - public void goMap(View view) { + public void goMap(@SuppressWarnings("unused") View view) { if (search == null || CollectionUtils.isEmpty(cacheList)) { showToast(res.getString(R.string.warn_no_cache_coord)); @@ -1483,12 +1434,12 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return true; } - public static void startActivityUserName(final Activity context, final String userName) { + public static void startActivityFinder(final Activity context, final String userName) { if (!isValidUsername(context, userName)) { return; } final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.USERNAME); + cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.FINDER); cachesIntent.putExtra(Intents.EXTRA_USERNAME, userName); context.startActivity(cachesIntent); } @@ -1606,6 +1557,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } else { final StoredList list = DataStore.getList(listId); // list.id may be different if listId was not valid + if (list.id != listId) { + showToast(getString(R.string.list_not_available)); + } listId = list.id; title = list.title; } @@ -1670,8 +1624,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA break; case POCKET: final String guid = extras.getString(Intents.EXTRA_POCKET_GUID); - final String pocket_name = extras.getString(Intents.EXTRA_NAME); - title = pocket_name; + title = extras.getString(Intents.EXTRA_NAME); loader = new PocketGeocacheListLoader(app, guid); break; } @@ -1680,7 +1633,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA showFooterLoadingCaches(); if (loader != null) { - loader.setRecaptchaHandler(new SearchHandler(this, res, loader)); + loader.setRecaptchaHandler(new RecaptchaHandler(this, loader)); } return loader; } diff --git a/main/src/cgeo/geocaching/CacheMenuHandler.java b/main/src/cgeo/geocaching/CacheMenuHandler.java index d0f1005..cfe9eeb 100644 --- a/main/src/cgeo/geocaching/CacheMenuHandler.java +++ b/main/src/cgeo/geocaching/CacheMenuHandler.java @@ -1,27 +1,17 @@ package cgeo.geocaching; -import cgeo.calendar.ICalendar; +import cgeo.calendar.CalendarAddon; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; -import cgeo.geocaching.geopoint.GeopointFormatter; -import cgeo.geocaching.network.Parameters; import cgeo.geocaching.ui.AbstractUIFactory; -import cgeo.geocaching.ui.dialog.Dialogs; -import cgeo.geocaching.utils.ProcessUtils; - -import org.apache.commons.lang3.StringUtils; import android.app.Activity; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; import android.view.Menu; import android.view.MenuItem; -import java.util.Date; - /** - * Shared menu handling for all activities having menu items related to a cache. - * + * Shared menu handling for all activities having menu items related to a cache. <br> + * TODO: replace by a fragment + * */ public class CacheMenuHandler extends AbstractUIFactory { @@ -58,7 +48,7 @@ public class CacheMenuHandler extends AbstractUIFactory { cache.shareCache(activity, res); return true; case R.id.menu_calendar: - addToCalendarWithIntent(activity, cache); + CalendarAddon.addToCalendarWithIntent(activity, cache); return true; default: return false; @@ -84,39 +74,4 @@ public class CacheMenuHandler extends AbstractUIFactory { activity.getMenuInflater().inflate(R.menu.cache_options, menu); onPrepareOptionsMenu(menu, cache); } - - private static void addToCalendarWithIntent(final Activity activity, final Geocache cache) { - final boolean calendarAddOnAvailable = ProcessUtils.isIntentAvailable(ICalendar.INTENT, Uri.parse(ICalendar.URI_SCHEME + "://" + ICalendar.URI_HOST)); - - if (calendarAddOnAvailable) { - final Date hiddenDate = cache.getHiddenDate(); - final Parameters params = new Parameters( - ICalendar.PARAM_NAME, cache.getName(), - ICalendar.PARAM_NOTE, StringUtils.defaultString(cache.getPersonalNote()), - ICalendar.PARAM_HIDDEN_DATE, hiddenDate != null ? String.valueOf(hiddenDate.getTime()) : StringUtils.EMPTY, - ICalendar.PARAM_URL, StringUtils.defaultString(cache.getUrl()), - ICalendar.PARAM_COORDS, cache.getCoords() == null ? "" : cache.getCoords().format(GeopointFormatter.Format.LAT_LON_DECMINUTE_RAW), - ICalendar.PARAM_LOCATION, StringUtils.defaultString(cache.getLocation()), - ICalendar.PARAM_SHORT_DESC, StringUtils.defaultString(cache.getShortDescription()), - ICalendar.PARAM_START_TIME_MINUTES, StringUtils.defaultString(cache.guessEventTimeMinutes()) - ); - - activity.startActivity(new Intent(ICalendar.INTENT, - Uri.parse(ICalendar.URI_SCHEME + "://" + ICalendar.URI_HOST + "?" + params.toString()))); - } else { - // Inform user the calendar add-on is not installed and let them get it from Google Play - Dialogs.confirmYesNo(activity, R.string.addon_missing_title, new StringBuilder(res.getString(R.string.helper_calendar_missing)) - .append(' ') - .append(res.getString(R.string.addon_download_prompt)) - .toString(), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(ICalendar.CALENDAR_ADDON_URI)); - activity.startActivity(intent); - } - }); - } - } - } diff --git a/main/src/cgeo/geocaching/CachePopup.java b/main/src/cgeo/geocaching/CachePopup.java index 9186497..c6c7c1c 100644 --- a/main/src/cgeo/geocaching/CachePopup.java +++ b/main/src/cgeo/geocaching/CachePopup.java @@ -9,9 +9,9 @@ import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.CacheDetailsCreator; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; import org.apache.commons.lang3.StringUtils; +import rx.functions.Action1; import android.content.Context; import android.content.Intent; @@ -110,9 +110,9 @@ public class CachePopup extends AbstractPopupActivity { if (Settings.getChooseList()) { // let user select list to store cache in new StoredList.UserInterface(CachePopup.this).promptForListSelection(R.string.list_title, - new RunnableWithArgument<Integer>() { + new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { storeCache(selectedListId); } }, true, StoredList.TEMPORARY_LIST_ID); diff --git a/main/src/cgeo/geocaching/CgeoApplication.java b/main/src/cgeo/geocaching/CgeoApplication.java index 2500d10..7bee97e 100644 --- a/main/src/cgeo/geocaching/CgeoApplication.java +++ b/main/src/cgeo/geocaching/CgeoApplication.java @@ -1,10 +1,10 @@ package cgeo.geocaching; -import cgeo.geocaching.network.StatusUpdater; import cgeo.geocaching.ui.dialog.Dialogs; -import cgeo.geocaching.utils.IObserver; import cgeo.geocaching.utils.Log; +import rx.Observable; + import android.app.Activity; import android.app.Application; import android.app.ProgressDialog; @@ -14,12 +14,11 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CgeoApplication extends Application { - private volatile GeoDataProvider geo; - private volatile DirectionProvider dir; + private volatile Observable<IGeoData> geo; + private volatile Observable<Float> dir; private boolean forceRelog = false; // c:geo needs to log into cache providers public boolean showLoginToast = true; //login toast shown just once. - private boolean liveMapHintShown = false; // livemap hint has been shown - final private StatusUpdater statusUpdater = new StatusUpdater(); + private boolean liveMapHintShownInThisSession = false; // livemap hint has been shown private static CgeoApplication instance; public CgeoApplication() { @@ -35,26 +34,11 @@ public class CgeoApplication extends Application { } @Override - public void onCreate() { - new Thread(statusUpdater).start(); - } - - @Override public void onLowMemory() { Log.i("Cleaning applications cache."); DataStore.removeAllFromCache(); } - @Override - public void onTerminate() { - Log.d("Terminating c:geo…"); - - DataStore.clean(); - DataStore.closeDb(); - - super.onTerminate(); - } - /** * Move the database to/from external cgdata in a new thread, * showing a progress window @@ -82,29 +66,11 @@ public class CgeoApplication extends Application { }.start(); } - /** - * Register an observer to receive GeoData information. - * <br/> - * If there is a chance that no observers are registered before this - * method is called, it is necessary to call it from a task implementing - * a looper interface as the data provider will use listeners that - * require a looper thread to run. - * - * @param observer a geodata observer - */ - public void addGeoObserver(final IObserver<? super IGeoData> observer) { - currentGeoObject().addObserver(observer); - } - - public void deleteGeoObserver(final IObserver<? super IGeoData> observer) { - currentGeoObject().deleteObserver(observer); - } - - private GeoDataProvider currentGeoObject() { + public Observable<IGeoData> currentGeoObject() { if (geo == null) { synchronized(this) { if (geo == null) { - geo = new GeoDataProvider(this); + geo = GeoDataProvider.create(this); } } } @@ -112,22 +78,14 @@ public class CgeoApplication extends Application { } public IGeoData currentGeo() { - return currentGeoObject().getMemory(); + return currentGeoObject().first().toBlockingObservable().single(); } - public void addDirectionObserver(final IObserver<? super Float> observer) { - currentDirObject().addObserver(observer); - } - - public void deleteDirectionObserver(final IObserver<? super Float> observer) { - currentDirObject().deleteObserver(observer); - } - - private DirectionProvider currentDirObject() { + public Observable<Float> currentDirObject() { if (dir == null) { synchronized(this) { if (dir == null) { - dir = new DirectionProvider(this); + dir = DirectionProvider.create(this); } } } @@ -135,19 +93,15 @@ public class CgeoApplication extends Application { } public Float currentDirection() { - return currentDirObject().getMemory(); - } - - public StatusUpdater getStatusUpdater() { - return statusUpdater; + return currentDirObject().first().toBlockingObservable().single(); } - public boolean isLiveMapHintShown() { - return liveMapHintShown; + public boolean isLiveMapHintShownInThisSession() { + return liveMapHintShownInThisSession; } - public void setLiveMapHintShown() { - liveMapHintShown = true; + public void setLiveMapHintShownInThisSession() { + liveMapHintShownInThisSession = true; } /** diff --git a/main/src/cgeo/geocaching/CompassActivity.java b/main/src/cgeo/geocaching/CompassActivity.java index 8955afd..6fc2de1 100644 --- a/main/src/cgeo/geocaching/CompassActivity.java +++ b/main/src/cgeo/geocaching/CompassActivity.java @@ -150,12 +150,13 @@ public class CompassActivity extends AbstractActivity { final CgeoApplication app = CgeoApplication.getInstance(); final IGeoData geo = app.currentGeo(); if (geo != null) { - geoDirHandler.update(geo); + geoDirHandler.updateGeoData(geo); } final Float dir = app.currentDirection(); if (dir != null) { - geoDirHandler.update(dir); + geoDirHandler.updateDirection(dir); } + } @Override diff --git a/main/src/cgeo/geocaching/CreateShortcutActivity.java b/main/src/cgeo/geocaching/CreateShortcutActivity.java index 7b91ba1..b6ea4f6 100644 --- a/main/src/cgeo/geocaching/CreateShortcutActivity.java +++ b/main/src/cgeo/geocaching/CreateShortcutActivity.java @@ -1,8 +1,10 @@ package cgeo.geocaching; import cgeo.geocaching.activity.AbstractActivity; +import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; -import cgeo.geocaching.utils.RunnableWithArgument; + +import rx.functions.Action1; import android.content.Intent; import android.content.Intent.ShortcutIconResource; @@ -21,17 +23,17 @@ public class CreateShortcutActivity extends AbstractActivity { } private void promptForShortcut() { - new StoredList.UserInterface(this).promptForListSelection(R.string.create_shortcut, new RunnableWithArgument<Integer>() { + new StoredList.UserInterface(this).promptForListSelection(R.string.create_shortcut, new Action1<Integer>() { @Override - public void run(final Integer listId) { + public void call(final Integer listId) { final Intent shortcut = createShortcut(listId.intValue()); setResult(RESULT_OK, shortcut); // finish activity to return the shortcut finish(); } - }); + }, false, PseudoList.HISTORY_LIST.id); } protected Intent createShortcut(int listId) { diff --git a/main/src/cgeo/geocaching/DataStore.java b/main/src/cgeo/geocaching/DataStore.java index 6da1af8..5cc77dc 100644 --- a/main/src/cgeo/geocaching/DataStore.java +++ b/main/src/cgeo/geocaching/DataStore.java @@ -25,17 +25,20 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import android.app.SearchManager; import android.content.ContentValues; import android.content.Context; import android.content.ContextWrapper; import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseUtils; +import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; +import android.provider.BaseColumns; import java.io.File; import java.io.FilenameFilter; @@ -811,12 +814,19 @@ public class DataStore { /** * Remove obsolete cache directories in c:geo private storage. + */ + public static void removeObsoleteCacheDirectories() { + removeObsoleteCacheDirectories(database); + } + + /** + * Remove obsolete cache directories in c:geo private storage. * * @param db * the read-write database to use */ private static void removeObsoleteCacheDirectories(final SQLiteDatabase db) { - final Pattern oldFilePattern = Pattern.compile("^[GC|TB|O][A-Z0-9]{4,7}$"); + final Pattern oldFilePattern = Pattern.compile("^[GC|TB|EC|GK|O][A-Z0-9]{4,7}$"); final SQLiteStatement select = db.compileStatement("select count(*) from " + dbTableCaches + " where geocode = ?"); final File[] files = LocalStorage.getStorage().listFiles(); final ArrayList<File> toRemove = new ArrayList<File>(files.length); @@ -868,28 +878,6 @@ public class DataStore { db.execSQL("drop table if exists " + dbTableTrackables); } - public static String[] getRecentGeocodesForSearch() { - init(); - - try { - long timestamp = System.currentTimeMillis() - DAYS_AFTER_CACHE_IS_DELETED; - final Cursor cursor = database.query( - dbTableCaches, - new String[]{"geocode"}, - "(detailed = 1 and detailedupdate > ?) or reason > 0", - new String[]{Long.toString(timestamp)}, - null, - null, - "detailedupdate desc", - "100"); - - return getFirstColumn(cursor); - } catch (final Exception e) { - Log.e("DataStore.allDetailedThere", e); - return new String[0]; - } - } - public static boolean isThere(String geocode, String guid, boolean detailed, boolean checkTime) { init(); @@ -2302,11 +2290,6 @@ public class DataStore { return new SearchResult(geocodes); } - /** delete caches from the DB store 3 days or more before */ - public static void clean() { - clean(false); - } - /** * Remove caches with listId = 0 * @@ -2355,6 +2338,8 @@ public class DataStore { cursor.close(); + geocodes = exceptCachesWithOfflineLog(geocodes); + if (!geocodes.isEmpty()) { Log.d("Database clean: removing " + geocodes.size() + " geocaches from listId=0"); removeCaches(geocodes, LoadFlags.REMOVE_ALL); @@ -2367,6 +2352,33 @@ public class DataStore { databaseCleaned = true; } + /** + * remove all geocodes from the given list of geocodes where an offline log exists + * + * @param geocodes + * @return + */ + private static Set<String> exceptCachesWithOfflineLog(Set<String> geocodes) { + if (geocodes.isEmpty()) { + return geocodes; + } + + init(); + final Cursor cursor = database.query( + dbTableLogsOffline, + new String[] { "geocode" }, + null, + null, + null, + null, + null); + + final List<String> geocodesWithOfflineLog = Arrays.asList(getFirstColumn(cursor)); + + geocodes.removeAll(geocodesWithOfflineLog); + return geocodes; + } + public static void removeAllFromCache() { // clean up CacheCache cacheCache.removeAllFromCache(); @@ -2897,21 +2909,6 @@ public class DataStore { return waypoints; } - public static String[] getTrackableCodes() { - init(); - - final Cursor cursor = database.query( - dbTableTrackables, - new String[] { "tbcode" }, - null, - null, - null, - null, - "updated DESC", - "100"); - return getFirstColumn(cursor); - } - /** * Extract the first column of the cursor rows and close the cursor. * @@ -3098,4 +3095,103 @@ public class DataStore { return missingFromSearch; } + public static Cursor findSuggestions(final String searchTerm) { + // require 3 characters, otherwise there are to many results + if (StringUtils.length(searchTerm) < 3) { + return null; + } + init(); + final MatrixCursor resultCursor = new MatrixCursor(new String[] { + BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_TEXT_1, + SearchManager.SUGGEST_COLUMN_TEXT_2, + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, + SearchManager.SUGGEST_COLUMN_QUERY + }); + try { + final String selectionArg = getSuggestionArgument(searchTerm); + findCaches(resultCursor, selectionArg); + findTrackables(resultCursor, selectionArg); + } catch (final Exception e) { + Log.e("DataStore.loadBatchOfStoredGeocodes", e); + } + return resultCursor; + } + + private static void findCaches(final MatrixCursor resultCursor, final String selectionArg) { + Cursor cursor = database.query( + dbTableCaches, + new String[] { "geocode", "name" }, + "geocode IS NOT NULL AND geocode != '' AND (geocode LIKE ? OR name LIKE ? OR owner LIKE ?)", + new String[] { selectionArg, selectionArg, selectionArg }, + null, + null, + "name"); + while (cursor.moveToNext()) { + final String geocode = cursor.getString(0); + resultCursor.addRow(new String[] { + String.valueOf(resultCursor.getCount()), + cursor.getString(1), + geocode, + Intents.ACTION_GEOCACHE, + geocode + }); + } + cursor.close(); + } + + private static String getSuggestionArgument(String input) { + return "%" + StringUtils.trim(input) + "%"; + } + + private static void findTrackables(final MatrixCursor resultCursor, final String selectionArg) { + Cursor cursor = database.query( + dbTableTrackables, + new String[] { "tbcode", "title" }, + "tbcode IS NOT NULL AND tbcode != '' AND (tbcode LIKE ? OR title LIKE ?)", + new String[] { selectionArg, selectionArg }, + null, + null, + "title"); + while (cursor.moveToNext()) { + final String tbcode = cursor.getString(0); + resultCursor.addRow(new String[] { + String.valueOf(resultCursor.getCount()), + cursor.getString(1), + tbcode, + Intents.ACTION_TRACKABLE, + tbcode + }); + } + cursor.close(); + } + + public static String[] getSuggestions(final String table, final String column, final String input) { + Cursor cursor = database.rawQuery("SELECT DISTINCT " + column + + " FROM " + table + + " WHERE " + column + " LIKE ?" + + " ORDER BY " + column + " COLLATE NOCASE ASC;", new String[] { getSuggestionArgument(input) }); + return getFirstColumn(cursor); + } + + public static String[] getSuggestionsOwnerName(String input) { + return getSuggestions(dbTableCaches, "owner", input); + } + + public static String[] getSuggestionsTrackableCode(String input) { + return getSuggestions(dbTableTrackables, "tbcode", input); + } + + public static String[] getSuggestionsFinderName(String input) { + return getSuggestions(dbTableLogs, "author", input); + } + + public static String[] getSuggestionsGeocode(String input) { + return getSuggestions(dbTableCaches, "geocode", input); + } + + public static String[] getSuggestionsKeyword(String input) { + return getSuggestions(dbTableCaches, "name", input); + } + } diff --git a/main/src/cgeo/geocaching/DirectionProvider.java b/main/src/cgeo/geocaching/DirectionProvider.java index ae58fed..49a433e 100644 --- a/main/src/cgeo/geocaching/DirectionProvider.java +++ b/main/src/cgeo/geocaching/DirectionProvider.java @@ -1,9 +1,15 @@ package cgeo.geocaching; import cgeo.geocaching.compatibility.Compatibility; -import cgeo.geocaching.utils.MemorySubject; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.Subscription; +import rx.observables.ConnectableObservable; +import rx.subjects.BehaviorSubject; +import rx.subscriptions.Subscriptions; +import rx.functions.Action0; import android.app.Activity; import android.content.Context; @@ -12,58 +18,61 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; -public class DirectionProvider extends MemorySubject<Float> implements SensorEventListener { +public class DirectionProvider implements OnSubscribe<Float> { private final SensorManager sensorManager; + private final BehaviorSubject<Float> subject = BehaviorSubject.create(0.0f); - // Previous values signaled to observers to avoid re-sending the same value when the - // device doesn't change orientation. The orientation is usually given with a 1 degree - // precision by Android, so it is not uncommon to obtain exactly the same value several - // times. - private float previous = -1; - - public DirectionProvider(final Context context) { - sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - + static public Observable<Float> create(final Context context) { + return new DirectionProvider((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)).worker.refCount(); } - @Override - protected void onFirstObserver() { - @SuppressWarnings("deprecation") - // This will be removed when using a new location service. Until then, it is okay to be used. - final Sensor defaultSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); - sensorManager.registerListener(this, defaultSensor, SensorManager.SENSOR_DELAY_NORMAL); + private DirectionProvider(final SensorManager sensorManager) { + this.sensorManager = sensorManager; } @Override - protected void onLastObserver() { - sensorManager.unregisterListener(this); + public void call(final Subscriber<? super Float> subscriber) { + subject.distinctUntilChanged().subscribe(subscriber); } - @Override - public void onAccuracyChanged(final Sensor sensor, int accuracy) { - /* - * There is a bug in Android, which apparently causes this method to be called every - * time the sensor _value_ changed, even if the _accuracy_ did not change. So logging - * this event leads to the log being flooded with multiple entries _per second_, - * which I experienced when running cgeo in a building (with GPS and network being - * unreliable). - * - * See for example https://code.google.com/p/android/issues/detail?id=14792 - */ + private final ConnectableObservable<Float> worker = new ConnectableObservable<Float>(this) { + @Override + public Subscription connect() { + @SuppressWarnings("deprecation") + // This will be removed when using a new location service. Until then, it is okay to be used. + final Sensor defaultSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + final SensorEventListener listener = new SensorEventListener() { + @Override + public void onSensorChanged(final SensorEvent event) { + subject.onNext(event.values[0]); + } - //Log.i(Settings.tag, "Compass' accuracy is low (" + accuracy + ")"); - } + @Override + public void onAccuracyChanged(final Sensor sensor, final int accuracy) { + /* + * There is a bug in Android, which apparently causes this method to be called every + * time the sensor _value_ changed, even if the _accuracy_ did not change. So logging + * this event leads to the log being flooded with multiple entries _per second_, + * which I experienced when running cgeo in a building (with GPS and network being + * unreliable). + * + * See for example https://code.google.com/p/android/issues/detail?id=14792 + */ - @Override - @SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY") - public void onSensorChanged(final SensorEvent event) { - final float direction = event.values[0]; - if (direction != previous) { - notifyObservers(direction); - previous = direction; + //Log.i(Settings.tag, "Compass' accuracy is low (" + accuracy + ")"); + } + }; + + sensorManager.registerListener(listener, defaultSensor, SensorManager.SENSOR_DELAY_NORMAL); + return Subscriptions.create(new Action0() { + @Override + public void call() { + sensorManager.unregisterListener(listener); + } + }); } - } + }; /** * Take the phone rotation (through a given activity) in account and adjust the direction. @@ -72,6 +81,7 @@ public class DirectionProvider extends MemorySubject<Float> implements SensorEve * @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 Activity activity, final float direction) { return Compatibility.getDirectionNow(direction, activity); } diff --git a/main/src/cgeo/geocaching/EditWaypointActivity.java b/main/src/cgeo/geocaching/EditWaypointActivity.java index 6d0f822..9010d3a 100644 --- a/main/src/cgeo/geocaching/EditWaypointActivity.java +++ b/main/src/cgeo/geocaching/EditWaypointActivity.java @@ -17,10 +17,10 @@ import cgeo.geocaching.utils.GeoDirHandler; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.TextUtils; -import com.googlecode.androidannotations.annotations.EActivity; -import com.googlecode.androidannotations.annotations.Extra; -import com.googlecode.androidannotations.annotations.InstanceState; -import com.googlecode.androidannotations.annotations.ViewById; +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.Extra; +import org.androidannotations.annotations.InstanceState; +import org.androidannotations.annotations.ViewById; import org.apache.commons.lang3.StringUtils; @@ -110,12 +110,14 @@ public class EditWaypointActivity extends AbstractActivity { buttonLon.setText(waypoint.getCoords().format(GeopointFormatter.Format.LON_DECMINUTE)); } waypointName.setText(Html.fromHtml(StringUtils.trimToEmpty(waypoint.getName())).toString()); + Dialogs.moveCursorToEnd(waypointName); if (TextUtils.containsHtml(waypoint.getNote())) { note.setText(Html.fromHtml(StringUtils.trimToEmpty(waypoint.getNote())).toString()); } else { note.setText(StringUtils.trimToEmpty(waypoint.getNote())); } + Dialogs.moveCursorToEnd(note); } final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_ONLY); setCoordsModificationVisibility(ConnectorFactory.getConnector(geocode), cache); diff --git a/main/src/cgeo/geocaching/GeoDataProvider.java b/main/src/cgeo/geocaching/GeoDataProvider.java index 73aefce..e18f735 100644 --- a/main/src/cgeo/geocaching/GeoDataProvider.java +++ b/main/src/cgeo/geocaching/GeoDataProvider.java @@ -3,9 +3,16 @@ package cgeo.geocaching; import cgeo.geocaching.enumerations.LocationProviderType; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.MemorySubject; import org.apache.commons.lang3.StringUtils; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.Subscription; +import rx.observables.ConnectableObservable; +import rx.subjects.BehaviorSubject; +import rx.subscriptions.Subscriptions; +import rx.functions.Action0; import android.content.Context; import android.location.GpsSatellite; @@ -15,22 +22,14 @@ import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * Provide information about the user location. This class should be instantiated only once per application. - */ -class GeoDataProvider extends MemorySubject<IGeoData> { +class GeoDataProvider implements OnSubscribe<IGeoData> { private static final String LAST_LOCATION_PSEUDO_PROVIDER = "last"; private final LocationManager geoManager; - private final GpsStatus.Listener gpsStatusListener = new GpsStatusListener(); private final LocationData gpsLocation = new LocationData(); private final LocationData netLocation = new LocationData(); - private final Listener networkListener = new Listener(LocationManager.NETWORK_PROVIDER, netLocation); - private final Listener gpsListener = new Listener(LocationManager.GPS_PROVIDER, gpsLocation); - private final Unregisterer unregisterer = new Unregisterer(); + private final BehaviorSubject<IGeoData> subject; + public boolean gpsEnabled = false; public int satellitesVisible = 0; public int satellitesFixed = 0; @@ -111,51 +110,6 @@ class GeoDataProvider extends MemorySubject<IGeoData> { } } - private class Unregisterer extends Thread { - - private boolean unregisterRequested = false; - private final ArrayBlockingQueue<Boolean> queue = new ArrayBlockingQueue<Boolean>(1); - - public void cancelUnregister() { - try { - queue.put(false); - } catch (final InterruptedException e) { - // Do nothing - } - } - - public void lateUnregister() { - try { - queue.put(true); - } catch (final InterruptedException e) { - // Do nothing - } - } - - @Override - public void run() { - try { - while (true) { - if (unregisterRequested) { - final Boolean element = queue.poll(2500, TimeUnit.MILLISECONDS); - if (element == null) { - // Timeout - unregisterListeners(); - unregisterRequested = false; - } else { - unregisterRequested = element; - } - } else { - unregisterRequested = queue.take(); - } - } - } catch (final InterruptedException e) { - // Do nothing - } - } - - } - /** * Build a new geo data provider object. * <p/> @@ -164,10 +118,50 @@ class GeoDataProvider extends MemorySubject<IGeoData> { * * @param context the context used to retrieve the system services */ - GeoDataProvider(final Context context) { + protected GeoDataProvider(final Context context) { geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - unregisterer.start(); + subject = BehaviorSubject.create(findInitialLocation()); + } + + public static Observable<IGeoData> create(final Context context) { + final GeoDataProvider provider = new GeoDataProvider(context); + return provider.worker.refCount(); + } + @Override + public void call(final Subscriber<? super IGeoData> subscriber) { + subject.subscribe(subscriber); + } + + final ConnectableObservable<IGeoData> worker = new ConnectableObservable<IGeoData>(this) { + @Override + public Subscription connect() { + final GpsStatus.Listener gpsStatusListener = new GpsStatusListener(); + geoManager.addGpsStatusListener(gpsStatusListener); + + final Listener networkListener = new Listener(LocationManager.NETWORK_PROVIDER, netLocation); + final Listener gpsListener = new Listener(LocationManager.GPS_PROVIDER, gpsLocation); + + for (final Listener listener : new Listener[] { networkListener, gpsListener }) { + try { + geoManager.requestLocationUpdates(listener.locationProvider, 0, 0, listener); + } catch (final Exception e) { + Log.w("There is no location provider " + listener.locationProvider); + } + } + + return Subscriptions.create(new Action0() { + @Override + public void call() { + geoManager.removeUpdates(networkListener); + geoManager.removeUpdates(gpsListener); + geoManager.removeGpsStatusListener(gpsStatusListener); + } + }); + } + }; + + private IGeoData findInitialLocation() { final Location initialLocation = new Location(LAST_LOCATION_PSEUDO_PROVIDER); try { // Try to find a sensible initial location from the last locations known to Android. @@ -195,45 +189,13 @@ class GeoDataProvider extends MemorySubject<IGeoData> { } // Start with an historical GeoData just in case someone queries it before we get // a chance to get any information. - notifyObservers(new GeoData(initialLocation, false, 0, 0)); + return new GeoData(initialLocation, false, 0, 0); } private static void copyCoords(final Location target, final Location source) { target.setLatitude(source.getLatitude()); target.setLongitude(source.getLongitude()); } - private void registerListeners() { - geoManager.addGpsStatusListener(gpsStatusListener); - - for (final Listener listener : new Listener[] { networkListener, gpsListener }) { - try { - geoManager.requestLocationUpdates(listener.locationProvider, 0, 0, listener); - } catch (final Exception e) { - Log.w("There is no location provider " + listener.locationProvider); - } - } - } - - private synchronized void unregisterListeners() { - // This method must be synchronized because it will be called asynchronously from the Unregisterer thread. - // We check that no observers have been re-added to prevent a race condition. - if (sizeObservers() == 0) { - geoManager.removeUpdates(networkListener); - geoManager.removeUpdates(gpsListener); - geoManager.removeGpsStatusListener(gpsStatusListener); - } - } - - @Override - protected void onFirstObserver() { - unregisterer.cancelUnregister(); - registerListeners(); - } - - @Override - protected void onLastObserver() { - unregisterer.lateUnregister(); - } private class Listener implements LocationListener { private final String locationProvider; @@ -336,7 +298,7 @@ class GeoDataProvider extends MemorySubject<IGeoData> { // We do not necessarily get signalled when satellites go to 0/0. final int visible = gpsLocation.isRecent() ? satellitesVisible : 0; final IGeoData current = new GeoData(locationData.location, gpsEnabled, visible, satellitesFixed); - notifyObservers(current); + subject.onNext(current); } } diff --git a/main/src/cgeo/geocaching/Geocache.java b/main/src/cgeo/geocaching/Geocache.java index 5d299d4..79b4425 100644 --- a/main/src/cgeo/geocaching/Geocache.java +++ b/main/src/cgeo/geocaching/Geocache.java @@ -164,7 +164,7 @@ public class Geocache implements ICache, IWaypoint { * * @param gpxParser */ - public Geocache(GPXParser gpxParser) { + public Geocache(@SuppressWarnings("unused") GPXParser gpxParser) { setReliableLatLon(true); setAttributes(Collections.<String> emptyList()); setWaypoints(Collections.<Waypoint> emptyList(), false); @@ -369,6 +369,7 @@ public class Geocache implements ICache, IWaypoint { * the other cache to compare this one to * @return true if both caches have the same content */ + @SuppressWarnings("deprecation") @SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY") private boolean isEqualTo(final Geocache other) { return detailed == other.detailed && @@ -437,22 +438,6 @@ public class Geocache implements ICache, IWaypoint { return hidden.compareTo(cal.getTime()) >= 0; } - /** - * Checks if a page contains the guid of a cache - * - * @param page - * the page to search in, may be null - * @return true if the page contains the guid of the cache, false otherwise - */ - public boolean isGuidContainedInPage(final String page) { - if (StringUtils.isBlank(page) || StringUtils.isBlank(guid)) { - return false; - } - final Boolean found = Pattern.compile(guid, Pattern.CASE_INSENSITIVE).matcher(page).find(); - Log.i("Geocache.isGuidContainedInPage: guid '" + guid + "' " + (found ? "" : "not ") + "found"); - return found; - } - public boolean isEventCache() { return cacheType.getValue().isEvent(); } @@ -816,13 +801,7 @@ public class Geocache implements ICache, IWaypoint { } public boolean showSize() { - if (size == CacheSize.NOT_CHOSEN) { - return false; - } - if (isEventCache() || isVirtual()) { - return false; - } - return true; + return !(size == CacheSize.NOT_CHOSEN || isEventCache() || isVirtual()); } public long getUpdated() { @@ -1372,17 +1351,6 @@ public class Geocache implements ICache, IWaypoint { } /** - * Retrieve a given waypoint. - * - * @param index - * the index of the waypoint - * @return waypoint or <code>null</code> if index is out of range - */ - public Waypoint getWaypoint(final int index) { - return index >= 0 && index < waypoints.size() ? waypoints.get(index) : null; - } - - /** * Lookup a waypoint by its id. * * @param id @@ -1612,6 +1580,8 @@ public class Geocache implements ICache, IWaypoint { StaticMapsProvider.downloadMaps(cache); + imgGetter.waitForBackgroundLoading(handler); + if (handler != null) { handler.sendMessage(Message.obtain()); } @@ -1671,8 +1641,9 @@ public class Geocache implements ICache, IWaypoint { patterns.add(Pattern.compile("\\b(\\d{1,2})(?:\\.00)?\\s+" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); } + final String searchText = getShortDescription() + ' ' + getDescription(); for (Pattern pattern : patterns) { - final MatcherWrapper matcher = new MatcherWrapper(pattern, getDescription()); + final MatcherWrapper matcher = new MatcherWrapper(pattern, searchText); while (matcher.find()) { try { final int hours = Integer.parseInt(matcher.group(1)); @@ -1783,4 +1754,8 @@ public class Geocache implements ICache, IWaypoint { return 0; } + public boolean applyDistanceRule() { + return (getType().applyDistanceRule() || hasUserModifiedCoords()) && getConnector() == GCConnector.getInstance(); + } + } diff --git a/main/src/cgeo/geocaching/ImageSelectActivity.java b/main/src/cgeo/geocaching/ImageSelectActivity.java index 12d1e84..766149c 100644 --- a/main/src/cgeo/geocaching/ImageSelectActivity.java +++ b/main/src/cgeo/geocaching/ImageSelectActivity.java @@ -6,6 +6,7 @@ import butterknife.InjectView; import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.files.LocalStorage; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.Log; @@ -111,10 +112,12 @@ public class ImageSelectActivity extends AbstractActivity { if (StringUtils.isNotBlank(imageCaption)) { captionView.setText(imageCaption); + Dialogs.moveCursorToEnd(captionView); } if (StringUtils.isNotBlank(imageDescription)) { descriptionView.setText(imageDescription); + Dialogs.moveCursorToEnd(captionView); } scaleView.setSelection(scaleChoiceIndex); diff --git a/main/src/cgeo/geocaching/ImagesActivity.java b/main/src/cgeo/geocaching/ImagesActivity.java index 29bc8c7..3da1ade 100644 --- a/main/src/cgeo/geocaching/ImagesActivity.java +++ b/main/src/cgeo/geocaching/ImagesActivity.java @@ -6,6 +6,7 @@ import cgeo.geocaching.ui.ImagesList; import cgeo.geocaching.ui.ImagesList.ImageType; import org.apache.commons.collections4.CollectionUtils; +import rx.Subscription; import android.content.Context; import android.content.Intent; @@ -22,8 +23,9 @@ public class ImagesActivity extends AbstractActivity { private boolean offline; private ArrayList<Image> imageNames; - private ImagesList imagesList; private ImageType imgType = ImageType.SpoilerImages; + private ImagesList imagesList; + private Subscription subscription; @Override public void onCreate(Bundle savedInstanceState) { @@ -60,18 +62,19 @@ public class ImagesActivity extends AbstractActivity { offline = DataStore.isOffline(geocode, null) && (imgType == ImageType.SpoilerImages || Settings.isStoreLogImages()); + } @Override public void onStart() { super.onStart(); - imagesList.loadImages(findViewById(R.id.spoiler_list), imageNames, offline); + subscription = imagesList.loadImages(findViewById(R.id.spoiler_list), imageNames, offline); } @Override public void onStop() { // Reclaim native memory faster than the finalizers would - imagesList.removeAllViews(); + subscription.unsubscribe(); super.onStop(); } diff --git a/main/src/cgeo/geocaching/Intents.java b/main/src/cgeo/geocaching/Intents.java index d9d9829..5c969a1 100644 --- a/main/src/cgeo/geocaching/Intents.java +++ b/main/src/cgeo/geocaching/Intents.java @@ -29,4 +29,8 @@ public class Intents { public static final String EXTRA_WAYPOINT_ID = PREFIX + "waypoint_id"; public static final String EXTRA_CACHELIST = PREFIX + "cache_list"; public static final String EXTRA_POCKET_GUID = PREFIX + "pocket_guid"; + + private static final String PREFIX_ACTION = "cgeo.geocaching.intent.action."; + public static final String ACTION_GEOCACHE = PREFIX_ACTION + "GEOCACHE"; + public static final String ACTION_TRACKABLE = PREFIX_ACTION + "TRACKABLE"; } diff --git a/main/src/cgeo/geocaching/LogCacheActivity.java b/main/src/cgeo/geocaching/LogCacheActivity.java index 301de01..4d2815b 100644 --- a/main/src/cgeo/geocaching/LogCacheActivity.java +++ b/main/src/cgeo/geocaching/LogCacheActivity.java @@ -13,6 +13,7 @@ import cgeo.geocaching.settings.Settings; import cgeo.geocaching.twitter.Twitter; import cgeo.geocaching.ui.Formatter; import cgeo.geocaching.ui.dialog.DateDialog; +import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.AsyncTaskWithProgress; import cgeo.geocaching.utils.DateUtils; import cgeo.geocaching.utils.Log; @@ -151,7 +152,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia final TextView actionButton = (TextView) inventoryItem.findViewById(R.id.action); actionButton.setId(tb.id); actionButtons.put(actionButton.getId(), tb); - actionButton.setText(res.getString(tb.action.resourceId) + " ▼"); + actionButton.setText(tb.action.getLabel() + " ▼"); actionButton.setOnClickListener(new View.OnClickListener() { @Override @@ -301,6 +302,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia final EditText logView = (EditText) findViewById(R.id.log); if (StringUtils.isBlank(currentLogText()) && StringUtils.isNotBlank(text)) { logView.setText(text); + Dialogs.moveCursorToEnd(logView); } tweetCheck.setChecked(true); @@ -633,11 +635,11 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia alert.create().show(); } - private String[] getTBLogTypes() { + private static String[] getTBLogTypes() { final LogTypeTrackable[] logTypeValues = LogTypeTrackable.values(); String[] logTypes = new String[logTypeValues.length]; for (int i = 0; i < logTypes.length; i++) { - logTypes[i] = res.getString(logTypeValues[i].resourceId); + logTypes[i] = logTypeValues[i].getLabel(); } return logTypes; } diff --git a/main/src/cgeo/geocaching/LogTrackableActivity.java b/main/src/cgeo/geocaching/LogTrackableActivity.java index 5246fa9..fabe391 100644 --- a/main/src/cgeo/geocaching/LogTrackableActivity.java +++ b/main/src/cgeo/geocaching/LogTrackableActivity.java @@ -3,8 +3,8 @@ package cgeo.geocaching; import butterknife.ButterKnife; import butterknife.InjectView; -import cgeo.geocaching.connector.gc.GCParser; import cgeo.geocaching.connector.gc.GCLogin; +import cgeo.geocaching.connector.gc.GCParser; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.network.Network; @@ -13,6 +13,7 @@ import cgeo.geocaching.settings.Settings; import cgeo.geocaching.twitter.Twitter; import cgeo.geocaching.ui.Formatter; import cgeo.geocaching.ui.dialog.DateDialog; +import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.LogTemplateProvider.LogContext; @@ -127,6 +128,7 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat if (StringUtils.isNotBlank(extras.getString(Intents.EXTRA_TRACKING_CODE))) { trackingEditText.setText(extras.getString(Intents.EXTRA_TRACKING_CODE)); + Dialogs.moveCursorToEnd(trackingEditText); } } diff --git a/main/src/cgeo/geocaching/MainActivity.java b/main/src/cgeo/geocaching/MainActivity.java index 924c66d..ba7795a 100644 --- a/main/src/cgeo/geocaching/MainActivity.java +++ b/main/src/cgeo/geocaching/MainActivity.java @@ -10,6 +10,7 @@ import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.maps.CGeoMap; import cgeo.geocaching.settings.Settings; @@ -19,14 +20,17 @@ import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.DatabaseBackupUtils; import cgeo.geocaching.utils.GeoDirHandler; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; import cgeo.geocaching.utils.Version; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.android.observables.AndroidObservable; +import rx.schedulers.Schedulers; +import rx.functions.Action1; import android.app.AlertDialog; import android.app.AlertDialog.Builder; @@ -48,7 +52,6 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -77,8 +80,6 @@ public class MainActivity extends AbstractActivity { private boolean cleanupRunning = false; private int countBubbleCnt = 0; private Geopoint addCoords = null; - private List<Address> addresses = null; - private boolean addressObtaining = false; private boolean initialized = false; private final UpdateLocation locationUpdater = new UpdateLocation(); @@ -104,7 +105,7 @@ public class MainActivity extends AbstractActivity { if (conn.isLoggedIn()) { userInfo.append(conn.getUserName()); if (conn.getCachesFound() >= 0) { - userInfo.append(" (").append(String.valueOf(conn.getCachesFound())).append(')'); + userInfo.append(" (").append(conn.getCachesFound()).append(')'); } userInfo.append(Formatter.SEPARATOR); } @@ -115,40 +116,24 @@ public class MainActivity extends AbstractActivity { } }; - private Handler obtainAddressHandler = new Handler() { + private static String formatAddress(final Address address) { + final ArrayList<String> addressParts = new ArrayList<String>(); - @Override - public void handleMessage(final Message msg) { - try { - if (CollectionUtils.isNotEmpty(addresses)) { - final Address address = addresses.get(0); - final ArrayList<String> addressParts = new ArrayList<String>(); - - final String countryName = address.getCountryName(); - if (countryName != null) { - addressParts.add(countryName); - } - final String locality = address.getLocality(); - if (locality != null) { - addressParts.add(locality); - } else { - final String adminArea = address.getAdminArea(); - if (adminArea != null) { - addressParts.add(adminArea); - } - } - - addCoords = app.currentGeo().getCoords(); - - navLocation.setText(StringUtils.join(addressParts, ", ")); - } - } catch (RuntimeException e) { - // nothing + final String countryName = address.getCountryName(); + if (countryName != null) { + addressParts.add(countryName); + } + final String locality = address.getLocality(); + if (locality != null) { + addressParts.add(locality); + } else { + final String adminArea = address.getAdminArea(); + if (adminArea != null) { + addressParts.add(adminArea); } - - addresses = null; } - }; + return StringUtils.join(addressParts, ", "); + } private class SatellitesHandler extends GeoDirHandler { @@ -285,7 +270,7 @@ public class MainActivity extends AbstractActivity { @Override public boolean onPrepareOptionsMenu(final Menu menu) { super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.menu_pocket_queries).setVisible(Settings.isPremiumMember()); + menu.findItem(R.id.menu_pocket_queries).setVisible(Settings.isGCPremiumMember()); return true; } @@ -309,13 +294,13 @@ public class MainActivity extends AbstractActivity { startScannerApplication(); return true; case R.id.menu_pocket_queries: - if (!Settings.isPremiumMember()) { + if (!Settings.isGCPremiumMember()) { return true; } - new PocketQueryList.UserInterface(MainActivity.this).promptForListSelection(new RunnableWithArgument<PocketQueryList>() { + PocketQueryList.promptForListSelection(this, new Action1<PocketQueryList>() { @Override - public void run(final PocketQueryList pql) { + public void call(final PocketQueryList pql) { CacheListActivity.startActivityPocket(MainActivity.this, pql); } }); @@ -388,14 +373,14 @@ public class MainActivity extends AbstractActivity { @Override public boolean onLongClick(final View v) { - new StoredList.UserInterface(MainActivity.this).promptForListSelection(R.string.list_title, new RunnableWithArgument<Integer>() { + new StoredList.UserInterface(MainActivity.this).promptForListSelection(R.string.list_title, new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { Settings.saveLastList(selectedListId); CacheListActivity.startActivityOffline(MainActivity.this); } - }); + }, false, PseudoList.HISTORY_LIST.id); return true; } }); @@ -428,7 +413,8 @@ public class MainActivity extends AbstractActivity { @Override public boolean onLongClick(final View v) { - selectGlobalTypeFilter(); + Settings.setCacheType(CacheType.ALL); + setFilterTitle(); return true; } }); @@ -525,52 +511,60 @@ public class MainActivity extends AbstractActivity { @Override public void updateGeoData(final IGeoData geo) { - try { - if (geo.getCoords() != null) { - if (!nearestView.isClickable()) { - nearestView.setFocusable(true); - nearestView.setClickable(true); - nearestView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - cgeoFindNearest(v); - } - }); - nearestView.setBackgroundResource(R.drawable.main_nearby); + if (!nearestView.isClickable()) { + nearestView.setFocusable(true); + nearestView.setClickable(true); + nearestView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View v) { + cgeoFindNearest(v); } + }); + nearestView.setBackgroundResource(R.drawable.main_nearby); + } - navType.setText(res.getString(geo.getLocationProvider().resourceId)); + navType.setText(res.getString(geo.getLocationProvider().resourceId)); - if (geo.getAccuracy() >= 0) { - int speed = Math.round(geo.getSpeed()) * 60 * 60 / 1000; - navAccuracy.setText("±" + Units.getDistanceFromMeters(geo.getAccuracy()) + Formatter.SEPARATOR + Units.getSpeed(speed)); - } else { - navAccuracy.setText(null); - } + if (geo.getAccuracy() >= 0) { + int speed = Math.round(geo.getSpeed()) * 60 * 60 / 1000; + navAccuracy.setText("±" + Units.getDistanceFromMeters(geo.getAccuracy()) + Formatter.SEPARATOR + Units.getSpeed(speed)); + } else { + navAccuracy.setText(null); + } - if (Settings.isShowAddress()) { - if (addCoords == null) { - navLocation.setText(res.getString(R.string.loc_no_addr)); - } - if (addCoords == null || (geo.getCoords().distanceTo(addCoords) > 0.5 && !addressObtaining)) { - (new ObtainAddressThread()).start(); + if (Settings.isShowAddress()) { + if (addCoords == null) { + navLocation.setText(R.string.loc_no_addr); + } + if (addCoords == null || (geo.getCoords().distanceTo(addCoords) > 0.5)) { + final Observable<String> address = Observable.create(new OnSubscribe<String>() { + @Override + public void call(final Subscriber<? super String> subscriber) { + try { + addCoords = geo.getCoords(); + final Geocoder geocoder = new Geocoder(MainActivity.this, Locale.getDefault()); + final Geopoint coords = app.currentGeo().getCoords(); + final List<Address> addresses = geocoder.getFromLocation(coords.getLatitude(), coords.getLongitude(), 1); + if (!addresses.isEmpty()) { + subscriber.onNext(formatAddress(addresses.get(0))); + } + subscriber.onCompleted(); + } catch (final Exception e) { + subscriber.onError(e); + } } - } else { - navLocation.setText(geo.getCoords().toString()); - } - } else { - if (nearestView.isClickable()) { - nearestView.setFocusable(false); - nearestView.setClickable(false); - nearestView.setOnClickListener(null); - nearestView.setBackgroundResource(R.drawable.main_nearby_disabled); - } - navType.setText(null); - navAccuracy.setText(null); - navLocation.setText(res.getString(R.string.loc_trying)); + }).subscribeOn(Schedulers.io()); + AndroidObservable.fromActivity(MainActivity.this, address) + .onErrorResumeNext(Observable.from(geo.getCoords().toString())) + .subscribe(new Action1<String>() { + @Override + public void call(final String address) { + navLocation.setText(address); + } + }); } - } catch (RuntimeException e) { - Log.w("Failed to update location."); + } else { + navLocation.setText(geo.getCoords().toString()); } } } @@ -579,7 +573,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoFindOnMap(final View v) { + public void cgeoFindOnMap(@SuppressWarnings("unused") final View v) { findOnMap.setPressed(true); CGeoMap.startActivityLiveMap(this); } @@ -588,7 +582,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoFindNearest(final View v) { + public void cgeoFindNearest(@SuppressWarnings("unused") final View v) { if (app.currentGeo().getCoords() == null) { return; } @@ -601,7 +595,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoFindByOffline(final View v) { + public void cgeoFindByOffline(@SuppressWarnings("unused") final View v) { findByOffline.setPressed(true); CacheListActivity.startActivityOffline(this); } @@ -610,7 +604,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoSearch(final View v) { + public void cgeoSearch(@SuppressWarnings("unused") final View v) { advanced.setPressed(true); startActivity(new Intent(this, SearchActivity.class)); } @@ -619,7 +613,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoPoint(final View v) { + public void cgeoPoint(@SuppressWarnings("unused") final View v) { any.setPressed(true); startActivity(new Intent(this, NavigateAnyPointActivity.class)); } @@ -628,7 +622,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoFilter(final View v) { + public void cgeoFilter(@SuppressWarnings("unused") final View v) { filter.setPressed(true); filter.performClick(); } @@ -637,7 +631,7 @@ public class MainActivity extends AbstractActivity { * @param v * unused here but needed since this method is referenced from XML layout */ - public void cgeoNavSettings(final View v) { + public void cgeoNavSettings(@SuppressWarnings("unused") final View v) { startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); } @@ -714,40 +708,11 @@ public class MainActivity extends AbstractActivity { } } - private class ObtainAddressThread extends Thread { - - public ObtainAddressThread() { - setPriority(Thread.MIN_PRIORITY); - } - - @Override - public void run() { - if (addressObtaining) { - return; - } - addressObtaining = true; - - try { - final Geocoder geocoder = new Geocoder(MainActivity.this, Locale.getDefault()); - final Geopoint coords = app.currentGeo().getCoords(); - addresses = geocoder.getFromLocation(coords.getLatitude(), coords.getLongitude(), 1); - } catch (final IOException e) { - Log.i("Failed to obtain address"); - } catch (final IllegalArgumentException e) { - Log.w("ObtainAddressThread.run", e); - } - - obtainAddressHandler.sendEmptyMessage(0); - - addressObtaining = false; - } - } - /** * @param view * unused here but needed since this method is referenced from XML layout */ - public void showAbout(final View view) { + public void showAbout(@SuppressWarnings("unused") final View view) { startActivity(new Intent(this, AboutActivity.class)); } @@ -755,7 +720,7 @@ public class MainActivity extends AbstractActivity { * @param view * unused here but needed since this method is referenced from XML layout */ - public void goSearch(final View view) { + public void goSearch(@SuppressWarnings("unused") final View view) { onSearchRequested(); } diff --git a/main/src/cgeo/geocaching/PocketQueryList.java b/main/src/cgeo/geocaching/PocketQueryList.java index 9d1110d..9b48f92 100644 --- a/main/src/cgeo/geocaching/PocketQueryList.java +++ b/main/src/cgeo/geocaching/PocketQueryList.java @@ -2,15 +2,20 @@ package cgeo.geocaching; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.connector.gc.GCParser; -import cgeo.geocaching.utils.RunnableWithArgument; + +import org.apache.commons.collections4.CollectionUtils; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.android.observables.AndroidObservable; +import rx.schedulers.Schedulers; +import rx.functions.Action1; import android.app.Activity; import android.app.AlertDialog; +import android.app.Dialog; import android.app.ProgressDialog; import android.content.DialogInterface; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Message; import java.util.List; @@ -20,105 +25,62 @@ public final class PocketQueryList { private final int maxCaches; private final String name; - public PocketQueryList(String guid, String name, int maxCaches) { + public PocketQueryList(final String guid, final String name, final int maxCaches) { this.guid = guid; this.name = name; this.maxCaches = maxCaches; } - public static class UserInterface { - - List<PocketQueryList> pocketQueryList = null; - RunnableWithArgument<PocketQueryList> runAfterwards; - - private Handler loadPocketQueryHandler = new Handler() { - - @Override - public void handleMessage(Message msg) { - if ((pocketQueryList == null) || (pocketQueryList.size() == 0)) { - if (waitDialog != null) { - waitDialog.dismiss(); - } - - ActivityMixin.showToast(activity, res.getString(R.string.warn_no_pocket_query_found)); - - return; - } - - if (waitDialog != null) { - waitDialog.dismiss(); - } - - final CharSequence[] items = new CharSequence[pocketQueryList.size()]; - - for (int i = 0; i < pocketQueryList.size(); i++) { - PocketQueryList pq = pocketQueryList.get(i); - items[i] = pq.name; - } + public String getGuid() { + return guid; + } - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(res.getString(R.string.search_pocket_select)); - builder.setItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int itemId) { - final PocketQueryList query = pocketQueryList.get(itemId); - dialogInterface.dismiss(); - runAfterwards.run(query); - } - }); - builder.create().show(); + public int getMaxCaches() { + return maxCaches; + } - } - }; + public String getName() { + return name; + } - private class LoadPocketQueryListThread extends Thread { - final private Handler handler; + public static void promptForListSelection(final Activity activity, final Action1<PocketQueryList> runAfterwards) { + final Dialog waitDialog = ProgressDialog.show(activity, activity.getString(R.string.search_pocket_title), activity.getString(R.string.search_pocket_loading), true, true); - public LoadPocketQueryListThread(Handler handlerIn) { - handler = handlerIn; + AndroidObservable.fromActivity(activity, Observable.create(new OnSubscribe<List<PocketQueryList>>() { + @Override + public void call(final Subscriber<? super List<PocketQueryList>> subscriber) { + subscriber.onNext(GCParser.searchPocketQueryList()); + subscriber.onCompleted(); } - + }).subscribeOn(Schedulers.io())).subscribe(new Action1<List<PocketQueryList>>() { @Override - public void run() { - pocketQueryList = GCParser.searchPocketQueryList(); - handler.sendMessage(Message.obtain()); + public void call(final List<PocketQueryList> pocketQueryLists) { + waitDialog.dismiss(); + selectFromPocketQueries(activity, pocketQueryLists, runAfterwards); } + }); + } + private static void selectFromPocketQueries(final Activity activity, final List<PocketQueryList> pocketQueryList, final Action1<PocketQueryList> runAfterwards) { + if (CollectionUtils.isEmpty(pocketQueryList)) { + ActivityMixin.showToast(activity, activity.getString(R.string.warn_no_pocket_query_found)); + return; } - private final Activity activity; - private final CgeoApplication app; - private final Resources res; - private ProgressDialog waitDialog = null; - - public UserInterface(final Activity activity) { - this.activity = activity; - app = CgeoApplication.getInstance(); - res = app.getResources(); - } - - public void promptForListSelection(final RunnableWithArgument<PocketQueryList> runAfterwards) { + final CharSequence[] items = new CharSequence[pocketQueryList.size()]; - this.runAfterwards = runAfterwards; - - waitDialog = ProgressDialog.show(activity, res.getString(R.string.search_pocket_title), res.getString(R.string.search_pocket_loading), true, true); - - LoadPocketQueryListThread thread = new LoadPocketQueryListThread(loadPocketQueryHandler); - thread.start(); + for (int i = 0; i < pocketQueryList.size(); i++) { + items[i] = pocketQueryList.get(i).name; } - - } - - public String getGuid() { - return guid; - } - - public int getMaxCaches() { - return maxCaches; - } - - public String getName() { - return name; + new AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.search_pocket_select)) + .setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialogInterface, final int itemId) { + dialogInterface.dismiss(); + runAfterwards.call(pocketQueryList.get(itemId)); + } + }).create().show(); } } diff --git a/main/src/cgeo/geocaching/SearchActivity.java b/main/src/cgeo/geocaching/SearchActivity.java index 4e1777d..8a89e5f 100644 --- a/main/src/cgeo/geocaching/SearchActivity.java +++ b/main/src/cgeo/geocaching/SearchActivity.java @@ -10,12 +10,17 @@ import cgeo.geocaching.connector.capability.ISearchByGeocode; import cgeo.geocaching.connector.trackable.TrackableConnector; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.geopoint.GeopointFormatter; +import cgeo.geocaching.search.AutoCompleteAdapter; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.dialog.CoordinatesInputDialog; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.EditUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import rx.functions.Func1; import android.app.Activity; import android.app.SearchManager; @@ -26,10 +31,8 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; -import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; -import android.widget.EditText; import java.util.Locale; @@ -39,19 +42,19 @@ public class SearchActivity extends AbstractActivity { @InjectView(R.id.buttonLongitude) protected Button buttonLongitude; @InjectView(R.id.search_coordinates) protected Button buttonSearchCoords; - @InjectView(R.id.address) protected EditText addressEditText; + @InjectView(R.id.address) protected AutoCompleteTextView addressEditText; @InjectView(R.id.search_address) protected Button buttonSearchAddress; @InjectView(R.id.geocode) protected AutoCompleteTextView geocodeEditText; @InjectView(R.id.display_geocode) protected Button buttonSearchGeocode; - @InjectView(R.id.keyword) protected EditText keywordEditText; + @InjectView(R.id.keyword) protected AutoCompleteTextView keywordEditText; @InjectView(R.id.search_keyword) protected Button buttonSearchKeyword; - @InjectView(R.id.finder) protected EditText finderNameEditText; + @InjectView(R.id.finder) protected AutoCompleteTextView finderNameEditText; @InjectView(R.id.search_finder) protected Button buttonSearchFinder; - @InjectView(R.id.owner) protected EditText ownerNameEditText; + @InjectView(R.id.owner) protected AutoCompleteTextView ownerNameEditText; @InjectView(R.id.search_owner) protected Button buttonSearchOwner; @InjectView(R.id.trackable) protected AutoCompleteTextView trackableEditText; @@ -60,9 +63,23 @@ public class SearchActivity extends AbstractActivity { @Override public final void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final Intent intent = getIntent(); + + // search suggestion for a cache + if (Intents.ACTION_GEOCACHE.equals(intent.getAction())) { + CacheDetailActivity.startActivity(this, intent.getStringExtra(SearchManager.QUERY)); + finish(); + return; + } + + // search suggestion for a trackable + if (Intents.ACTION_TRACKABLE.equals(intent.getAction())) { + TrackableActivity.startActivity(this, null, intent.getStringExtra(SearchManager.QUERY), null); + finish(); + return; + } // search query - final Intent intent = getIntent(); if (Intent.ACTION_SEARCH.equals(intent.getAction())) { hideKeyboard(); final String query = intent.getStringExtra(SearchManager.QUERY); @@ -173,7 +190,7 @@ public class SearchActivity extends AbstractActivity { public void run() { findByAddressFn(); } - }); + }, null); setSearchAction(geocodeEditText, buttonSearchGeocode, new Runnable() { @@ -181,8 +198,13 @@ public class SearchActivity extends AbstractActivity { public void run() { findByGeocodeFn(); } + }, new Func1<String, String[]>() { + + @Override + public String[] call(final String input) { + return DataStore.getSuggestionsGeocode(input); + } }); - addHistoryEntries(geocodeEditText, DataStore.getRecentGeocodesForSearch()); setSearchAction(keywordEditText, buttonSearchKeyword, new Runnable() { @@ -190,6 +212,12 @@ public class SearchActivity extends AbstractActivity { public void run() { findByKeywordFn(); } + }, new Func1<String, String[]>() { + + @Override + public String[] call(final String input) { + return DataStore.getSuggestionsKeyword(input); + } }); setSearchAction(finderNameEditText, buttonSearchFinder, new Runnable() { @@ -198,6 +226,12 @@ public class SearchActivity extends AbstractActivity { public void run() { findByFinderFn(); } + }, new Func1<String, String[]>() { + + @Override + public String[] call(final String input) { + return DataStore.getSuggestionsFinderName(input); + } }); setSearchAction(ownerNameEditText, buttonSearchOwner, new Runnable() { @@ -206,6 +240,12 @@ public class SearchActivity extends AbstractActivity { public void run() { findByOwnerFn(); } + }, new Func1<String, String[]>() { + + @Override + public String[] call(final String input) { + return DataStore.getSuggestionsOwnerName(input); + } }); setSearchAction(trackableEditText, buttonSearchTrackable, new Runnable() { @@ -214,12 +254,16 @@ public class SearchActivity extends AbstractActivity { public void run() { findTrackableFn(); } + }, new Func1<String, String[]>() { + + @Override + public String[] call(final String input) { + return DataStore.getSuggestionsTrackableCode(input); + } }); - addHistoryEntries(trackableEditText, DataStore.getTrackableCodes()); - disableSuggestions(trackableEditText); } - private static void setSearchAction(final EditText editText, final Button button, final Runnable runnable) { + private static void setSearchAction(final AutoCompleteTextView editText, final Button button, final @NonNull Runnable runnable, final @Nullable Func1<String, String[]> suggestionFunction) { EditUtils.setActionListener(editText, runnable); button.setOnClickListener(new View.OnClickListener() { @Override @@ -227,12 +271,8 @@ public class SearchActivity extends AbstractActivity { runnable.run(); } }); - } - - private void addHistoryEntries(final AutoCompleteTextView textView, final String[] entries) { - if (entries != null) { - final ArrayAdapter<String> historyAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, entries); - textView.setAdapter(historyAdapter); + if (suggestionFunction != null) { + editText.setAdapter(new AutoCompleteAdapter(editText.getContext(), android.R.layout.simple_dropdown_item_1line, suggestionFunction)); } } @@ -305,7 +345,7 @@ public class SearchActivity extends AbstractActivity { return; } - CacheListActivity.startActivityUserName(this, usernameText); + CacheListActivity.startActivityFinder(this, usernameText); } private void findByOwnerFn() { @@ -331,7 +371,7 @@ public class SearchActivity extends AbstractActivity { return; } - CacheDetailActivity.startActivity(this, geocodeText.toUpperCase()); + CacheDetailActivity.startActivity(this, geocodeText.toUpperCase(Locale.US)); } private void findTrackableFn() { diff --git a/main/src/cgeo/geocaching/SearchResult.java b/main/src/cgeo/geocaching/SearchResult.java index 131a01c..12a2522 100644 --- a/main/src/cgeo/geocaching/SearchResult.java +++ b/main/src/cgeo/geocaching/SearchResult.java @@ -1,5 +1,6 @@ package cgeo.geocaching; +import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.gc.GCLogin; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; @@ -10,6 +11,11 @@ import cgeo.geocaching.gcvote.GCVote; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.Nullable; +import rx.Observable; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.schedulers.Schedulers; import android.os.Parcel; import android.os.Parcelable; @@ -228,6 +234,7 @@ public class SearchResult implements Parcelable { return result; } + @Nullable public Geocache getFirstCacheFromResult(final EnumSet<LoadFlag> loadFlags) { return CollectionUtils.isNotEmpty(geocodes) ? DataStore.loadCache(geocodes.iterator().next(), loadFlags) : null; } @@ -294,4 +301,25 @@ public class SearchResult implements Parcelable { } } + public static <C extends IConnector> SearchResult parallelCombineActive(final Collection<C> connectors, + final Func1<C, SearchResult> func) { + return Observable.from(connectors).parallel(new Func1<Observable<C>, Observable<SearchResult>>() { + @Override + public Observable<SearchResult> call(final Observable<C> cObservable) { + return cObservable.flatMap(new Func1<C, Observable<? extends SearchResult>>() { + @Override + public Observable<? extends SearchResult> call(final C c) { + return c.isActive() ? Observable.from(func.call(c)) : Observable.<SearchResult>empty(); + } + }); + } + }, Schedulers.io()).reduce(new SearchResult(), new Func2<SearchResult, SearchResult, SearchResult>() { + @Override + public SearchResult call(final SearchResult searchResult, final SearchResult searchResult2) { + searchResult.addSearchResult(searchResult2); + return searchResult; + } + }).toBlockingObservable().first(); + } + } diff --git a/main/src/cgeo/geocaching/StaticMapsActivity.java b/main/src/cgeo/geocaching/StaticMapsActivity.java index 7811da5..16fce37 100644 --- a/main/src/cgeo/geocaching/StaticMapsActivity.java +++ b/main/src/cgeo/geocaching/StaticMapsActivity.java @@ -4,11 +4,10 @@ import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.utils.Log; -import com.googlecode.androidannotations.annotations.EActivity; -import com.googlecode.androidannotations.annotations.Extra; -import com.googlecode.androidannotations.annotations.OptionsItem; -import com.googlecode.androidannotations.annotations.OptionsMenu; - +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.Extra; +import org.androidannotations.annotations.OptionsItem; +import org.androidannotations.annotations.OptionsMenu; import org.apache.commons.collections4.CollectionUtils; import android.app.ProgressDialog; diff --git a/main/src/cgeo/geocaching/StaticMapsProvider.java b/main/src/cgeo/geocaching/StaticMapsProvider.java index d5cbb13..eaab159 100644 --- a/main/src/cgeo/geocaching/StaticMapsProvider.java +++ b/main/src/cgeo/geocaching/StaticMapsProvider.java @@ -298,8 +298,8 @@ public final class StaticMapsProvider { return true; } - public static Bitmap getPreviewMap(final String geocode) { - return decodeFile(StaticMapsProvider.getMapFile(geocode, PREFIX_PREVIEW, false)); + public static Bitmap getPreviewMap(final Geocache cache) { + return decodeFile(StaticMapsProvider.getMapFile(cache.getGeocode(), PREFIX_PREVIEW, false)); } public static Bitmap getWaypointMap(final String geocode, final Waypoint waypoint, final int level) { diff --git a/main/src/cgeo/geocaching/StatusFragment.java b/main/src/cgeo/geocaching/StatusFragment.java index 4f70f0e..3672a3b 100644 --- a/main/src/cgeo/geocaching/StatusFragment.java +++ b/main/src/cgeo/geocaching/StatusFragment.java @@ -1,15 +1,17 @@ package cgeo.geocaching; +import cgeo.geocaching.network.StatusUpdater; import cgeo.geocaching.network.StatusUpdater.Status; -import cgeo.geocaching.utils.IObserver; import cgeo.geocaching.utils.Log; +import rx.Subscription; +import rx.android.observables.AndroidObservable; +import rx.functions.Action1; + import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; @@ -20,91 +22,68 @@ import android.widget.TextView; public class StatusFragment extends Fragment { - private ViewGroup status; - private ImageView statusIcon; - private TextView statusMessage; - - final private StatusHandler statusHandler = new StatusHandler(); + private Subscription statusSubscription; @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); - status = (ViewGroup) inflater.inflate(R.layout.status, container, false); - statusIcon = (ImageView) status.findViewById(R.id.status_icon); - statusMessage = (TextView) status.findViewById(R.id.status_message); - return status; - } - - @Override - public void onResume() { - super.onResume(); - CgeoApplication.getInstance().getStatusUpdater().addObserver(statusHandler); - } - - @Override - public void onPause() { - CgeoApplication.getInstance().getStatusUpdater().deleteObserver(statusHandler); - super.onPause(); - } - - private class StatusHandler extends Handler implements IObserver<Status> { - - @Override - public void update(final Status data) { - obtainMessage(0, data).sendToTarget(); - } - - @Override - public void handleMessage(final Message msg) { - final Status data = (Status) msg.obj; - updateDisplay(data != null && data.message != null ? data : Status.defaultStatus()); - } - - private void updateDisplay(final Status data) { - - if (data == null) { - status.setVisibility(View.INVISIBLE); - return; - } - - final Resources res = getResources(); - final String packageName = getActivity().getPackageName(); + final ViewGroup statusGroup = (ViewGroup) inflater.inflate(R.layout.status, container, false); + final ImageView statusIcon = (ImageView) statusGroup.findViewById(R.id.status_icon); + final TextView statusMessage = (TextView) statusGroup.findViewById(R.id.status_message); + statusSubscription = AndroidObservable.fromFragment(this, StatusUpdater.latestStatus).subscribe(new Action1<Status>() { + @Override + public void call(final Status status) { + if (status == null) { + statusGroup.setVisibility(View.INVISIBLE); + return; + } - if (data.icon != null) { - final int iconId = res.getIdentifier(data.icon, "drawable", packageName); - if (iconId != 0) { - statusIcon.setImageResource(iconId); - statusIcon.setVisibility(View.VISIBLE); + final Resources res = getResources(); + final String packageName = getActivity().getPackageName(); + + if (status.icon != null) { + final int iconId = res.getIdentifier(status.icon, "drawable", packageName); + if (iconId != 0) { + statusIcon.setImageResource(iconId); + statusIcon.setVisibility(View.VISIBLE); + } else { + Log.w("StatusHandler: could not find icon corresponding to @drawable/" + status.icon); + statusIcon.setVisibility(View.GONE); + } } else { - Log.w("StatusHandler: could not find icon corresponding to @drawable/" + data.icon); statusIcon.setVisibility(View.GONE); } - } else { - statusIcon.setVisibility(View.GONE); - } - String message = data.message; - if (data.messageId != null) { - final int messageId = res.getIdentifier(data.messageId, "string", packageName); - if (messageId != 0) { - message = res.getString(messageId); + String message = status.message; + if (status.messageId != null) { + final int messageId = res.getIdentifier(status.messageId, "string", packageName); + if (messageId != 0) { + message = res.getString(messageId); + } } - } - statusMessage.setText(message); - status.setVisibility(View.VISIBLE); + statusMessage.setText(message); + statusGroup.setVisibility(View.VISIBLE); - if (data.url != null) { - status.setOnClickListener(new OnClickListener() { - @Override - public void onClick(final View v) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(data.url))); - } - }); - } else { - status.setClickable(false); + if (status.url != null) { + statusGroup.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View v) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(status.url))); + } + }); + } else { + statusGroup.setClickable(false); + } } - } + }); + return statusGroup; + } + @Override + public void onDestroy() { + statusSubscription.unsubscribe(); + super.onDestroy(); } + } diff --git a/main/src/cgeo/geocaching/TrackableActivity.java b/main/src/cgeo/geocaching/TrackableActivity.java index dcfd80a..948e668 100644 --- a/main/src/cgeo/geocaching/TrackableActivity.java +++ b/main/src/cgeo/geocaching/TrackableActivity.java @@ -34,6 +34,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.Html; +import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -109,6 +110,8 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi } }; + private CharSequence clickedItemText = null; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.viewpager_activity); @@ -130,6 +133,8 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi // try to get data from URI if (geocode == null && guid == null && id == null && uri != null) { + geocode = ConnectorFactory.getTrackableFromURL(uri.toString()); + final String uriHost = uri.getHost().toLowerCase(Locale.US); if (uriHost.contains("geocaching.com")) { geocode = uri.getQueryParameter("tracker"); @@ -190,6 +195,36 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi } @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo info) { + super.onCreateContextMenu(menu, view, info); + final int viewId = view.getId(); + assert view instanceof TextView; + clickedItemText = ((TextView) view).getText(); + switch (viewId) { + case R.id.value: // name, TB-code, origin, released, distance + final String itemTitle = (String) ((TextView) ((View) view.getParent()).findViewById(R.id.name)).getText(); + buildDetailsContextMenu(menu, clickedItemText, itemTitle, true); + break; + case R.id.goal: + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.trackable_goal), false); + break; + case R.id.details: + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.trackable_details), false); + break; + case R.id.log: + buildDetailsContextMenu(menu, clickedItemText, res.getString(R.string.cache_logs), false); + break; + default: + break; + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + return onClipboardItemSelected(item, clickedItemText) || onOptionsItemSelected(item); + } + + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.trackable_activity, menu); return true; @@ -360,7 +395,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi } // trackable name - details.add(R.string.trackable_name, StringUtils.isNotBlank(trackable.getName()) ? Html.fromHtml(trackable.getName()).toString() : res.getString(R.string.trackable_unknown)); + registerForContextMenu(details.add(R.string.trackable_name, StringUtils.isNotBlank(trackable.getName()) ? Html.fromHtml(trackable.getName()).toString() : res.getString(R.string.trackable_unknown))); // trackable type String tbType; @@ -372,7 +407,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi details.add(R.string.trackable_type, tbType); // trackable geocode - details.add(R.string.trackable_code, trackable.getGeocode()); + registerForContextMenu(details.add(R.string.trackable_code, trackable.getGeocode())); // trackable owner final TextView owner = details.add(R.string.trackable_owner, res.getString(R.string.trackable_unknown)); @@ -441,16 +476,17 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi if (StringUtils.isNotBlank(trackable.getOrigin())) { final TextView origin = details.add(R.string.trackable_origin, ""); origin.setText(Html.fromHtml(trackable.getOrigin()), TextView.BufferType.SPANNABLE); + registerForContextMenu(origin); } // trackable released if (trackable.getReleased() != null) { - details.add(R.string.trackable_released, Formatter.formatDate(trackable.getReleased().getTime())); + registerForContextMenu(details.add(R.string.trackable_released, Formatter.formatDate(trackable.getReleased().getTime()))); } // trackable distance if (trackable.getDistance() >= 0) { - details.add(R.string.trackable_distance, Units.getDistanceFromKilometers(trackable.getDistance())); + registerForContextMenu(details.add(R.string.trackable_distance, Units.getDistanceFromKilometers(trackable.getDistance()))); } // trackable goal @@ -459,6 +495,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi goalTextView.setVisibility(View.VISIBLE); goalTextView.setText(Html.fromHtml(trackable.getGoal(), new HtmlImage(geocode, true, 0, false), null), TextView.BufferType.SPANNABLE); goalTextView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); + registerForContextMenu(goalTextView); } // trackable details @@ -467,6 +504,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi detailsTextView.setVisibility(View.VISIBLE); detailsTextView.setText(Html.fromHtml(trackable.getDetails(), new HtmlImage(geocode, true, 0, false), new UnknownTagsHandler()), TextView.BufferType.SPANNABLE); detailsTextView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); + registerForContextMenu(detailsTextView); } // trackable image diff --git a/main/src/cgeo/geocaching/UsefulAppsActivity.java b/main/src/cgeo/geocaching/UsefulAppsActivity.java index c70143f..39c527d 100644 --- a/main/src/cgeo/geocaching/UsefulAppsActivity.java +++ b/main/src/cgeo/geocaching/UsefulAppsActivity.java @@ -62,6 +62,7 @@ public class UsefulAppsActivity extends AbstractActivity { private static final HelperApp[] HELPER_APPS = { new HelperApp(R.string.helper_calendar_title, R.string.helper_calendar_description, R.drawable.cgeo, "cgeo.calendar"), new HelperApp(R.string.helper_sendtocgeo_title, R.string.helper_sendtocgeo_description, R.drawable.cgeo, "http://send2.cgeo.org"), + new HelperApp(R.string.helper_contacts_title, R.string.helper_contacts_description, R.drawable.cgeo, "cgeo.contacts"), new HelperApp(R.string.helper_pocketquery_title, R.string.helper_pocketquery_description, R.drawable.helper_pocketquery, "org.pquery"), new HelperApp(R.string.helper_locus_title, R.string.helper_locus_description, R.drawable.helper_locus, "menion.android.locus"), new HelperApp(R.string.helper_google_translate_title, R.string.helper_google_translate_description, R.drawable.helper_google_translate, "com.google.android.apps.translate"), diff --git a/main/src/cgeo/geocaching/activity/AbstractActivity.java b/main/src/cgeo/geocaching/activity/AbstractActivity.java index 36b6d01..7127de4 100644 --- a/main/src/cgeo/geocaching/activity/AbstractActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractActivity.java @@ -3,18 +3,27 @@ package cgeo.geocaching.activity; import butterknife.ButterKnife; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.R; import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.network.Cookies; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.ClipboardUtils; +import cgeo.geocaching.utils.HtmlUtils; +import cgeo.geocaching.utils.TranslationUtils; -import android.content.Context; +import org.apache.commons.lang3.StringUtils; + +import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; import android.support.v4.app.FragmentActivity; +import android.view.ContextMenu; +import android.view.MenuItem; import android.view.View; -import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import java.util.Locale; + public abstract class AbstractActivity extends FragmentActivity implements IAbstractActivity { protected CgeoApplication app = null; @@ -96,7 +105,7 @@ public abstract class AbstractActivity extends FragmentActivity implements IAbst // create view variables ButterKnife.inject(this); } - + private void initializeCommonFields() { // initialize commonly used members res = this.getResources(); @@ -116,6 +125,48 @@ public abstract class AbstractActivity extends FragmentActivity implements IAbst } protected void hideKeyboard() { - ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); + new Keyboard(this).hide(); + } + + public void showKeyboard(final View view) { + new Keyboard(this).show(view); + } + + protected void buildDetailsContextMenu(final ContextMenu menu, final CharSequence clickedItemText, final String fieldTitle, final boolean copyOnly) { + menu.setHeaderTitle(fieldTitle); + getMenuInflater().inflate(R.menu.details_context, menu); + menu.findItem(R.id.menu_translate_to_sys_lang).setVisible(!copyOnly); + if (!copyOnly) { + if (clickedItemText.length() > TranslationUtils.TRANSLATION_TEXT_LENGTH_WARN) { + showToast(res.getString(R.string.translate_length_warning)); + } + menu.findItem(R.id.menu_translate_to_sys_lang).setTitle(res.getString(R.string.translate_to_sys_lang, Locale.getDefault().getDisplayLanguage())); + } + final boolean localeIsEnglish = StringUtils.equals(Locale.getDefault().getLanguage(), Locale.ENGLISH.getLanguage()); + menu.findItem(R.id.menu_translate_to_english).setVisible(!copyOnly && !localeIsEnglish); + } + + protected boolean onClipboardItemSelected(final MenuItem item, final CharSequence clickedItemText) { + switch (item.getItemId()) { + // detail fields + case R.id.menu_copy: + ClipboardUtils.copyToClipboard(clickedItemText); + showToast(res.getString(R.string.clipboard_copy_ok)); + return true; + case R.id.menu_translate_to_sys_lang: + TranslationUtils.startActivityTranslate(this, Locale.getDefault().getLanguage(), HtmlUtils.extractText(clickedItemText)); + return true; + case R.id.menu_translate_to_english: + TranslationUtils.startActivityTranslate(this, Locale.ENGLISH.getLanguage(), HtmlUtils.extractText(clickedItemText)); + return true; + case R.id.menu_cache_share_field: + final Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, clickedItemText.toString()); + startActivity(Intent.createChooser(intent, res.getText(R.string.cache_share_field))); + return true; + default: + return false; + } } } diff --git a/main/src/cgeo/geocaching/activity/AbstractListActivity.java b/main/src/cgeo/geocaching/activity/AbstractListActivity.java index 2adae7a..a5d5c14 100644 --- a/main/src/cgeo/geocaching/activity/AbstractListActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractListActivity.java @@ -11,7 +11,6 @@ public abstract class AbstractListActivity extends FragmentListActivity implemen IAbstractActivity { private boolean keepScreenOn = false; - private boolean paused = true; protected CgeoApplication app = null; protected Resources res = null; @@ -85,26 +84,4 @@ public abstract class AbstractListActivity extends FragmentListActivity implemen // initialize action bar title with activity title ActivityMixin.setTitle(this, getTitle()); } - - @Override - public void onResume() { - paused = false; - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - paused = true; - } - - /** - * Check if the current activity is paused. This must be called and acted - * upon only from the UI thread. - * - * @return <code>true</code> if the current activity is paused - */ - protected boolean isPaused() { - return paused; - } } diff --git a/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java b/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java index c7d4507..6e2900d 100644 --- a/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java @@ -5,7 +5,6 @@ import cgeo.geocaching.utils.Log; import com.viewpagerindicator.TitlePageIndicator; import com.viewpagerindicator.TitleProvider; - import org.apache.commons.lang3.tuple.Pair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; diff --git a/main/src/cgeo/geocaching/activity/ActivityMixin.java b/main/src/cgeo/geocaching/activity/ActivityMixin.java index c1a2678..bfd45da 100644 --- a/main/src/cgeo/geocaching/activity/ActivityMixin.java +++ b/main/src/cgeo/geocaching/activity/ActivityMixin.java @@ -125,6 +125,6 @@ public final class ActivityMixin { editText.getText().replace(start, end, completeText); int newCursor = moveCursor ? start + completeText.length() : start; - editText.setSelection(newCursor, newCursor); + editText.setSelection(newCursor); } } diff --git a/main/src/cgeo/geocaching/activity/Keyboard.java b/main/src/cgeo/geocaching/activity/Keyboard.java new file mode 100644 index 0000000..9bae7be --- /dev/null +++ b/main/src/cgeo/geocaching/activity/Keyboard.java @@ -0,0 +1,40 @@ +package cgeo.geocaching.activity; + +import org.eclipse.jdt.annotation.NonNull; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +/** + * Class for hiding/showing the soft keyboard on Android. + * + */ +public class Keyboard { + private final Activity activity; + + public Keyboard(final @NonNull Activity activity) { + this.activity = activity; + } + + public void hide() { + ((InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); + } + + public void show(final View view) { + view.requestFocus(); + ((InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(view, 0); + } + + public void showDelayed(final View view) { + view.postDelayed(new Runnable() { + + @Override + public void run() { + final InputMethodManager keyboard = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + keyboard.showSoftInput(view, 0); + } + }, 50); + } +} diff --git a/main/src/cgeo/geocaching/activity/Progress.java b/main/src/cgeo/geocaching/activity/Progress.java index 2710023..8ee88a7 100644 --- a/main/src/cgeo/geocaching/activity/Progress.java +++ b/main/src/cgeo/geocaching/activity/Progress.java @@ -17,7 +17,7 @@ public class Progress { private ProgressDialog dialog; private int progress = 0; private int progressDivider = 1; - private boolean hideAbsolute = false; + final private boolean hideAbsolute; public Progress(boolean hideAbsolute) { this.hideAbsolute = hideAbsolute; @@ -28,7 +28,7 @@ public class Progress { } public synchronized void dismiss() { - if (dialog != null && dialog.isShowing()) { + if (isShowing()) { try { dialog.dismiss(); } catch (final Exception e) { @@ -54,13 +54,8 @@ public class Progress { } } - private void createProgressDialog(Context context, String title, String message, Message cancelMessage) { - if (hideAbsolute) { - dialog = new CustomProgressDialog(context); - } - else { - dialog = new ProgressDialog(context); - } + private void createProgressDialog(final Context context, final String title, final String message, final Message cancelMessage) { + dialog = hideAbsolute ? new CustomProgressDialog(context) : new ProgressDialog(context); dialog.setTitle(title); dialog.setMessage(message); if (cancelMessage != null) { @@ -87,7 +82,7 @@ public class Progress { } public synchronized void setMaxProgressAndReset(final int max) { - if (dialog != null && dialog.isShowing()) { + if (isShowing()) { final int modMax = max / this.progressDivider; dialog.setMax(modMax); dialog.setProgress(0); @@ -97,7 +92,7 @@ public class Progress { public synchronized void setProgress(final int progress) { final int modProgress = progress / this.progressDivider; - if (dialog != null && dialog.isShowing()) { + if (isShowing()) { dialog.setProgress(modProgress); } this.progress = modProgress; diff --git a/main/src/cgeo/geocaching/apps/AbstractLocusApp.java b/main/src/cgeo/geocaching/apps/AbstractLocusApp.java index d6c2fe6..8e9181d 100644 --- a/main/src/cgeo/geocaching/apps/AbstractLocusApp.java +++ b/main/src/cgeo/geocaching/apps/AbstractLocusApp.java @@ -6,6 +6,7 @@ import cgeo.geocaching.Waypoint; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.WaypointType; +import cgeo.geocaching.utils.SynchronizedDateFormat; import menion.android.locus.addon.publiclib.DisplayData; import menion.android.locus.addon.publiclib.LocusUtils; @@ -14,8 +15,6 @@ import menion.android.locus.addon.publiclib.geoData.PointGeocachingData; import menion.android.locus.addon.publiclib.geoData.PointGeocachingDataWaypoint; import menion.android.locus.addon.publiclib.geoData.PointsData; -import org.apache.commons.lang3.time.FastDateFormat; - import android.app.Activity; import android.location.Location; @@ -30,7 +29,7 @@ import java.util.Locale; * @see <a href="http://forum.asamm.cz/viewtopic.php?f=29&t=767">Locus forum</a> */ public abstract class AbstractLocusApp extends AbstractApp { - private static final FastDateFormat ISO8601DATE = FastDateFormat.getInstance("yyyy-MM-dd'T'", Locale.US); + private static final SynchronizedDateFormat ISO8601DATE = new SynchronizedDateFormat("yyyy-MM-dd'T'", Locale.US); protected AbstractLocusApp(final String text, int id, final String intent) { super(text, id, intent); @@ -122,7 +121,7 @@ public abstract class AbstractLocusApp extends AbstractApp { pg.placedBy = cache.getOwnerDisplayName(); final Date hiddenDate = cache.getHiddenDate(); if (hiddenDate != null) { - pg.hidden = ISO8601DATE.format(hiddenDate.getTime()); + pg.hidden = ISO8601DATE.format(hiddenDate); } int locusId = toLocusType(cache.getType()); if (locusId != NO_LOCUS_ID) { diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java index a1c752c..ec9705c 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java @@ -8,6 +8,7 @@ import cgeo.geocaching.apps.AbstractApp; import cgeo.geocaching.geopoint.Geopoint; import android.app.Activity; +import android.content.Intent; /** * navigation app for simple point navigation (no differentiation between cache/waypoint/point) @@ -49,4 +50,17 @@ abstract class AbstractPointNavigationApp extends AbstractApp implements CacheNa public boolean isEnabled(Waypoint waypoint) { return waypoint.getCoords() != null; } + + protected static void addIntentExtras(final Intent intent, final Waypoint waypoint) { + intent.putExtra("name", waypoint.getName()); + intent.putExtra("code", waypoint.getGeocode()); + } + + protected static void addIntentExtras(final Intent intent, final Geocache cache) { + intent.putExtra("difficulty", cache.getDifficulty()); + intent.putExtra("terrain", cache.getTerrain()); + intent.putExtra("name", cache.getName()); + intent.putExtra("code", cache.getGeocode()); + intent.putExtra("size", cache.getSize().getL10n()); + } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java new file mode 100644 index 0000000..6c6ffda --- /dev/null +++ b/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java @@ -0,0 +1,45 @@ +package cgeo.geocaching.apps.cache.navi; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.Waypoint; +import cgeo.geocaching.geopoint.Geopoint; + +import android.app.Activity; +import android.content.Intent; + +public abstract class AbstractRadarApp extends AbstractPointNavigationApp { + + private final String intentAction; + + protected AbstractRadarApp(final String name, final int id, final String intent, final String packageName) { + super(name, id, intent, packageName); + this.intentAction = intent; + } + + private Intent createIntent(final Geopoint point) { + final Intent intent = new Intent(intentAction); + addCoordinates(intent, point); + return intent; + } + + @Override + public void navigate(final Activity activity, final Geopoint point) { + activity.startActivity(createIntent(point)); + } + + @Override + public void navigate(final Activity activity, final Geocache cache) { + final Intent intent = createIntent(cache.getCoords()); + addIntentExtras(intent, cache); + activity.startActivity(intent); + } + + @Override + public void navigate(final Activity activity, final Waypoint waypoint) { + final Intent intent = createIntent(waypoint.getCoords()); + addIntentExtras(intent, waypoint); + activity.startActivity(intent); + } + + protected abstract void addCoordinates(final Intent intent, final Geopoint point); +} diff --git a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java index 60d6e31..819638c 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java @@ -1,6 +1,8 @@ package cgeo.geocaching.apps.cache.navi; +import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import cgeo.geocaching.Waypoint; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.utils.Log; @@ -22,10 +24,15 @@ class GoogleMapsApp extends AbstractPointNavigationApp { @Override public void navigate(Activity activity, Geopoint point) { - // INFO: q parameter works with Google Maps, but breaks cooperation with all other apps + navigate(activity, point, activity.getString(R.string.waypoint)); + } + + private static void navigate(Activity activity, Geopoint point, String label) { try { - activity.startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse("geo:" + point.getLatitude() + "," + point.getLongitude()))); + final String geoLocation = "geo:" + point.getLatitude() + "," + point.getLongitude(); + final String query = point.getLatitude() + "," + point.getLongitude() + "(" + label + ")"; + final String uriString = geoLocation + "?q=" + Uri.encode(query); + activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(uriString))); return; } catch (RuntimeException e) { // nothing @@ -35,4 +42,13 @@ class GoogleMapsApp extends AbstractPointNavigationApp { ActivityMixin.showToast(activity, getString(R.string.err_application_no)); } + @Override + public void navigate(Activity activity, Geocache cache) { + navigate(activity, cache.getCoords(), cache.getName()); + } + + @Override + public void navigate(Activity activity, Waypoint waypoint) { + navigate(activity, waypoint.getCoords(), waypoint.getName()); + } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java b/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java index 8ba3bef..ac83085 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java @@ -1,17 +1,15 @@ package cgeo.geocaching.apps.cache.navi;
-import cgeo.geocaching.Geocache;
import cgeo.geocaching.R;
import cgeo.geocaching.geopoint.Geopoint;
-import android.app.Activity;
import android.content.Intent;
/**
* Application for communication with the Pebble watch.
- *
+ *
*/
-class PebbleApp extends AbstractPointNavigationApp {
+class PebbleApp extends AbstractRadarApp {
private static final String INTENT = "com.webmajstr.pebble_gc.NAVIGATE_TO";
private static final String PACKAGE_NAME = "com.webmajstr.pebble_gc";
@@ -21,24 +19,8 @@ class PebbleApp extends AbstractPointNavigationApp { }
@Override
- public void navigate(Activity activity, Geopoint point) {
- final Intent pebbleIntent = new Intent(INTENT);
- pebbleIntent.putExtra("latitude", point.getLatitude());
- pebbleIntent.putExtra("longitude", point.getLongitude());
- activity.startActivity(pebbleIntent);
+ protected void addCoordinates(final Intent intent, final Geopoint coords) {
+ intent.putExtra("latitude", coords.getLatitude());
+ intent.putExtra("longitude", coords.getLongitude());
}
-
- @Override
- public void navigate(Activity activity, Geocache cache) {
- final Intent pebbleIntent = new Intent(INTENT);
- pebbleIntent.putExtra("latitude", cache.getCoords().getLatitude());
- pebbleIntent.putExtra("longitude", cache.getCoords().getLongitude());
- pebbleIntent.putExtra("difficulty", cache.getDifficulty());
- pebbleIntent.putExtra("terrain", cache.getTerrain());
- pebbleIntent.putExtra("name", cache.getName());
- pebbleIntent.putExtra("code", cache.getGeocode());
- pebbleIntent.putExtra("size", cache.getSize().getL10n());
- activity.startActivity(pebbleIntent);
- }
-
}
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java b/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java index ffa6650..41cf2d8 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java @@ -3,10 +3,9 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.R; import cgeo.geocaching.geopoint.Geopoint; -import android.app.Activity; import android.content.Intent; -class RadarApp extends AbstractPointNavigationApp { +class RadarApp extends AbstractRadarApp { private static final String INTENT = "com.google.android.radar.SHOW_RADAR"; private static final String PACKAGE_NAME = "com.eclipsim.gpsstatus2"; @@ -16,10 +15,9 @@ class RadarApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint point) { - final Intent radarIntent = new Intent(INTENT); - radarIntent.putExtra("latitude", (float) point.getLatitude()); - radarIntent.putExtra("longitude", (float) point.getLongitude()); - activity.startActivity(radarIntent); + protected void addCoordinates(final Intent intent, final Geopoint coords) { + intent.putExtra("latitude", (float) coords.getLatitude()); + intent.putExtra("longitude", (float) coords.getLongitude()); } + }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java b/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java index ac5809e..40c4d92 100644 --- a/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java +++ b/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java @@ -1,7 +1,7 @@ package cgeo.geocaching.apps.cachelist; -import cgeo.geocaching.SearchResult; import cgeo.geocaching.Geocache; +import cgeo.geocaching.SearchResult; import cgeo.geocaching.apps.App; import android.app.Activity; diff --git a/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java b/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java index 76379de..0da198b 100644 --- a/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java +++ b/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java @@ -1,5 +1,7 @@ package cgeo.geocaching.concurrent; +import org.eclipse.jdt.annotation.NonNull; + import java.util.concurrent.ThreadFactory; /** @@ -12,6 +14,7 @@ public class PriorityThreadFactory implements ThreadFactory { this.priority = priority; } + @NonNull @Override public Thread newThread(Runnable r) { Thread result = new Thread(r); diff --git a/main/src/cgeo/geocaching/connector/AbstractConnector.java b/main/src/cgeo/geocaching/connector/AbstractConnector.java index 53a3bcb..6d8d79e 100644 --- a/main/src/cgeo/geocaching/connector/AbstractConnector.java +++ b/main/src/cgeo/geocaching/connector/AbstractConnector.java @@ -16,10 +16,10 @@ import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.utils.RunnableWithArgument; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Action1; import java.util.ArrayList; import java.util.Collection; @@ -255,21 +255,21 @@ public abstract class AbstractConnector implements IConnector { List<UserAction> actions = getDefaultUserActions(); if (this instanceof ISearchByOwner) { - actions.add(new UserAction(R.string.user_menu_view_hidden, new RunnableWithArgument<UserAction.Context>() { + actions.add(new UserAction(R.string.user_menu_view_hidden, new Action1<Context>() { @Override - public void run(Context context) { + public void call(Context context) { CacheListActivity.startActivityOwner(context.activity, context.userName); } })); } if (this instanceof ISearchByFinder) { - actions.add(new UserAction(R.string.user_menu_view_found, new RunnableWithArgument<UserAction.Context>() { + actions.add(new UserAction(R.string.user_menu_view_found, new Action1<UserAction.Context>() { @Override - public void run(Context context) { - CacheListActivity.startActivityUserName(context.activity, context.userName); + public void call(Context context) { + CacheListActivity.startActivityFinder(context.activity, context.userName); } })); } @@ -283,10 +283,10 @@ public abstract class AbstractConnector implements IConnector { public List<UserAction> getDefaultUserActions() { final ArrayList<UserAction> actions = new ArrayList<UserAction>(); if (ContactsAddon.isAvailable()) { - actions.add(new UserAction(R.string.user_menu_open_contact, new RunnableWithArgument<UserAction.Context>() { + actions.add(new UserAction(R.string.user_menu_open_contact, new Action1<UserAction.Context>() { @Override - public void run(Context context) { + public void call(Context context) { ContactsAddon.openContactCard(context.activity, context.userName); } })); diff --git a/main/src/cgeo/geocaching/connector/ConnectorFactory.java b/main/src/cgeo/geocaching/connector/ConnectorFactory.java index 0081951..18344f5 100644 --- a/main/src/cgeo/geocaching/connector/ConnectorFactory.java +++ b/main/src/cgeo/geocaching/connector/ConnectorFactory.java @@ -28,6 +28,11 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import rx.Observable; +import rx.schedulers.Schedulers; +import rx.functions.Func1; +import rx.functions.Func2; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -176,14 +181,30 @@ public final class ConnectorFactory { } /** @see ISearchByViewPort#searchByViewport */ - public static SearchResult searchByViewport(final @NonNull Viewport viewport, final MapTokens tokens) { - final SearchResult result = new SearchResult(); - for (final ISearchByViewPort connector : searchByViewPortConns) { - if (connector.isActive()) { - result.addSearchResult(connector.searchByViewport(viewport, tokens)); + public static Observable<SearchResult> searchByViewport(final @NonNull Viewport viewport, final MapTokens tokens) { + return Observable.from(searchByViewPortConns).filter(new Func1<ISearchByViewPort, Boolean>() { + @Override + public Boolean call(final ISearchByViewPort connector) { + return connector.isActive(); } - } - return result; + }).parallel(new Func1<Observable<ISearchByViewPort>, Observable<SearchResult>>() { + @Override + public Observable<SearchResult> call(final Observable<ISearchByViewPort> connector) { + return connector.map(new Func1<ISearchByViewPort, SearchResult>() { + @Override + public SearchResult call(final ISearchByViewPort connector) { + return connector.searchByViewport(viewport, tokens); + } + }); + } + }, Schedulers.io()).reduce(new SearchResult(), new Func2<SearchResult, SearchResult, SearchResult>() { + + @Override + public SearchResult call(final SearchResult result, final SearchResult searchResult) { + result.addSearchResult(searchResult); + return result; + } + }); } public static String getGeocodeFromURL(final String url) { @@ -200,6 +221,12 @@ public final class ConnectorFactory { return TRACKABLE_CONNECTORS; } + /** + * Get the geocode of a trackable from a URL. + * + * @param url + * @return {@code null} if the URL cannot be decoded + */ public static String getTrackableFromURL(final String url) { if (url == null) { return null; diff --git a/main/src/cgeo/geocaching/connector/UserAction.java b/main/src/cgeo/geocaching/connector/UserAction.java index d0c97bb..e9ee4a3 100644 --- a/main/src/cgeo/geocaching/connector/UserAction.java +++ b/main/src/cgeo/geocaching/connector/UserAction.java @@ -1,8 +1,7 @@ package cgeo.geocaching.connector; -import cgeo.geocaching.utils.RunnableWithArgument; - import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Action1; import android.app.Activity; @@ -19,14 +18,15 @@ public class UserAction { } public final int displayResourceId; - private final @NonNull RunnableWithArgument<Context> runnable; + private final @NonNull + Action1<Context> runnable; - public UserAction(int displayResourceId, final @NonNull RunnableWithArgument<UserAction.Context> runnable) { + public UserAction(int displayResourceId, final @NonNull Action1<Context> runnable) { this.displayResourceId = displayResourceId; this.runnable = runnable; } public void run(Context context) { - runnable.run(context); + runnable.call(context); } } diff --git a/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java b/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java new file mode 100644 index 0000000..4da9705 --- /dev/null +++ b/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java @@ -0,0 +1,13 @@ +package cgeo.geocaching.connector.capability; + +import cgeo.geocaching.connector.IConnector; + +import java.io.File; + +/** + * Connector interface to implement an upload of (already exported) field notes + * + */ +public interface FieldNotesCapability extends IConnector { + public boolean uploadFieldNotes(final File exportFile); +} diff --git a/main/src/cgeo/geocaching/connector/ec/ECApi.java b/main/src/cgeo/geocaching/connector/ec/ECApi.java index 03fce4d..702e557 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECApi.java +++ b/main/src/cgeo/geocaching/connector/ec/ECApi.java @@ -15,12 +15,12 @@ import cgeo.geocaching.list.StoredList; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.SynchronizedDateFormat; import ch.boye.httpclientandroidlib.HttpResponse; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.FastDateFormat; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -39,7 +39,7 @@ public class ECApi { private static final String API_HOST = "https://extremcaching.com/exports/"; private static final ECLogin ecLogin = ECLogin.getInstance(); - private static final FastDateFormat LOG_DATE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); + private static final SynchronizedDateFormat LOG_DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); public static String getIdFromGeocode(final String geocode) { return StringUtils.removeStartIgnoreCase(geocode, "EC"); diff --git a/main/src/cgeo/geocaching/connector/ec/ECConnector.java b/main/src/cgeo/geocaching/connector/ec/ECConnector.java index 6da076b..71716fe 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECConnector.java +++ b/main/src/cgeo/geocaching/connector/ec/ECConnector.java @@ -140,7 +140,7 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, // invoke settings activity to insert login details if (status == StatusCode.NO_LOGIN_INFO_STORED && fromActivity != null) { - SettingsActivity.jumpToServicesPage(fromActivity); + SettingsActivity.openForScreen(R.string.preference_screen_ec, fromActivity); } } return status == StatusCode.NO_ERROR; diff --git a/main/src/cgeo/geocaching/connector/ec/ECLogin.java b/main/src/cgeo/geocaching/connector/ec/ECLogin.java index 52bd423..012bdc9 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECLogin.java +++ b/main/src/cgeo/geocaching/connector/ec/ECLogin.java @@ -10,7 +10,6 @@ import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.Nullable; diff --git a/main/src/cgeo/geocaching/connector/gc/GCConnector.java b/main/src/cgeo/geocaching/connector/gc/GCConnector.java index e946748..925f6f0 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConnector.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConnector.java @@ -10,6 +10,7 @@ import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.AbstractConnector; import cgeo.geocaching.connector.ILoggingManager; import cgeo.geocaching.connector.UserAction; +import cgeo.geocaching.connector.capability.FieldNotesCapability; import cgeo.geocaching.connector.capability.ICredentials; import cgeo.geocaching.connector.capability.ILogin; import cgeo.geocaching.connector.capability.ISearchByCenter; @@ -23,26 +24,29 @@ import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.loaders.RecaptchaReceiver; import cgeo.geocaching.network.Network; +import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.SettingsActivity; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import rx.functions.Action1; + import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Handler; +import java.io.File; import java.util.List; import java.util.regex.Pattern; -public class GCConnector extends AbstractConnector implements ISearchByGeocode, ISearchByCenter, ISearchByViewPort, ISearchByKeyword, ILogin, ICredentials, ISearchByOwner, ISearchByFinder { +public class GCConnector extends AbstractConnector implements ISearchByGeocode, ISearchByCenter, ISearchByViewPort, ISearchByKeyword, ILogin, ICredentials, ISearchByOwner, ISearchByFinder, FieldNotesCapability { private static final String CACHE_URL_SHORT = "http://coord.info/"; // Double slash is used to force open in browser @@ -91,7 +95,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, @Override public boolean supportsPersonalNote() { - return Settings.isPremiumMember(); + return Settings.isGCPremiumMember(); } @Override @@ -285,7 +289,22 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, @Override protected String getCacheUrlPrefix() { - return CACHE_URL_SHORT; + return null; // UNUSED + } + + @Override + public String getGeocodeFromUrl(String url) { + // coord.info URLs + String code = StringUtils.substringAfterLast(url, "coord.info/"); + if (code != null && canHandle(code)) { + return code; + } + // expanded geocaching.com URLs + code = StringUtils.substringBetween(url, "/geocache/", "_"); + if (code != null && canHandle(code)) { + return code; + } + return null; } @Override @@ -316,7 +335,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, // invoke settings activity to insert login details if (status == StatusCode.NO_LOGIN_INFO_STORED && fromActivity != null) { - SettingsActivity.jumpToServicesPage(fromActivity); + SettingsActivity.openForScreen(R.string.preference_screen_gc, fromActivity); } } return status == StatusCode.NO_ERROR; @@ -379,17 +398,17 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, public @NonNull List<UserAction> getUserActions() { List<UserAction> actions = super.getUserActions(); - actions.add(new UserAction(R.string.user_menu_open_browser, new RunnableWithArgument<UserAction.Context>() { + actions.add(new UserAction(R.string.user_menu_open_browser, new Action1<UserAction.Context>() { @Override - public void run(cgeo.geocaching.connector.UserAction.Context context) { + public void call(cgeo.geocaching.connector.UserAction.Context context) { context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/profile/?u=" + Network.encode(context.userName)))); } })); - actions.add(new UserAction(R.string.user_menu_send_message, new RunnableWithArgument<UserAction.Context>() { + actions.add(new UserAction(R.string.user_menu_send_message, new Action1<UserAction.Context>() { @Override - public void run(cgeo.geocaching.connector.UserAction.Context context) { + public void call(cgeo.geocaching.connector.UserAction.Context context) { context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/email/?u=" + Network.encode(context.userName)))); } })); @@ -406,4 +425,40 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, return GCParser.searchByUsername(username, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver); } + @Override + public boolean uploadFieldNotes(final File exportFile) { + if (!GCLogin.getInstance().isActualLoginStatus()) { + // no need to upload (possibly large file) if we're not logged in + final StatusCode loginState = GCLogin.getInstance().login(); + if (loginState != StatusCode.NO_ERROR) { + Log.e("FieldnoteExport.ExportTask upload: Login failed"); + } + } + + final String uri = "http://www.geocaching.com/my/uploadfieldnotes.aspx"; + final String page = GCLogin.getInstance().getRequestLogged(uri, null); + + if (StringUtils.isBlank(page)) { + Log.e("FieldnoteExport.ExportTask get page: No data from server"); + return false; + } + + final String[] viewstates = GCLogin.getViewstates(page); + + final Parameters uploadParams = new Parameters( + "__EVENTTARGET", "", + "__EVENTARGUMENT", "", + "ctl00$ContentBody$btnUpload", "Upload Field Note"); + + GCLogin.putViewstates(uploadParams, viewstates); + + Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$FieldNoteLoader", "text/plain", exportFile)); + + if (StringUtils.isBlank(page)) { + Log.e("FieldnoteExport.ExportTask upload: No data from server"); + return false; + } + return true; + } + } diff --git a/main/src/cgeo/geocaching/connector/gc/GCConstants.java b/main/src/cgeo/geocaching/connector/gc/GCConstants.java index 3900a95..cfc369d 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConstants.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConstants.java @@ -1,5 +1,7 @@ package cgeo.geocaching.connector.gc; +import org.eclipse.jdt.annotation.NonNull; + import java.util.Locale; import java.util.regex.Pattern; @@ -15,13 +17,13 @@ public final class GCConstants { static final String GC_URL = "http://www.geocaching.com/"; static final String GC_TILE_URL = "http://tiles.geocaching.com/"; /** Live Map */ - public final static String URL_LIVE_MAP = GC_URL + "map/default.aspx"; + public final static @NonNull String URL_LIVE_MAP = GC_URL + "map/default.aspx"; /** Live Map pop-up */ - public final static String URL_LIVE_MAP_DETAILS = GC_TILE_URL + "map.details"; + public final static @NonNull String URL_LIVE_MAP_DETAILS = GC_TILE_URL + "map.details"; /** Caches in a tile */ - public final static String URL_MAP_INFO = GC_TILE_URL + "map.info"; + public final static @NonNull String URL_MAP_INFO = GC_TILE_URL + "map.info"; /** Tile itself */ - public final static String URL_MAP_TILE = GC_TILE_URL + "map.png"; + public final static @NonNull String URL_MAP_TILE = GC_TILE_URL + "map.png"; /** * Patterns for parsing the result of a (detailed) search diff --git a/main/src/cgeo/geocaching/connector/gc/GCLogin.java b/main/src/cgeo/geocaching/connector/gc/GCLogin.java index a7cf6cf..250b0b2 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCLogin.java +++ b/main/src/cgeo/geocaching/connector/gc/GCLogin.java @@ -14,7 +14,6 @@ import cgeo.geocaching.utils.MatcherWrapper; import cgeo.geocaching.utils.TextUtils; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -97,7 +96,7 @@ public class GCLogin extends AbstractLogin { } if (getLoginStatus(loginData)) { - Log.i("Already logged in Geocaching.com as " + username + " (" + Settings.getMemberStatus() + ')'); + Log.i("Already logged in Geocaching.com as " + username + " (" + Settings.getGCMemberStatus() + ')'); if (switchToEnglish(loginData) && retry) { return login(false); } @@ -132,7 +131,7 @@ public class GCLogin extends AbstractLogin { assert loginData != null; // Caught above if (getLoginStatus(loginData)) { - Log.i("Successfully logged in Geocaching.com as " + username + " (" + Settings.getMemberStatus() + ')'); + Log.i("Successfully logged in Geocaching.com as " + username + " (" + Settings.getGCMemberStatus() + ')'); if (switchToEnglish(loginData) && retry) { return login(false); @@ -204,9 +203,9 @@ public class GCLogin extends AbstractLogin { Log.e("getLoginStatus: bad cache count", e); } setActualCachesFound(cachesCount); - Settings.setMemberStatus(TextUtils.getMatch(page, GCConstants.PATTERN_MEMBER_STATUS, true, null)); - if ( page.contains(GCConstants.MEMBER_STATUS_RENEW) ) { - Settings.setMemberStatus(GCConstants.MEMBER_STATUS_PM); + Settings.setGCMemberStatus(TextUtils.getMatch(page, GCConstants.PATTERN_MEMBER_STATUS, true, null)); + if (page.contains(GCConstants.MEMBER_STATUS_RENEW)) { + Settings.setGCMemberStatus(GCConstants.MEMBER_STATUS_PM); } return true; } @@ -259,9 +258,9 @@ public class GCLogin extends AbstractLogin { final String responseData = StringUtils.defaultString(Network.getResponseData(Network.getRequest("http://www.geocaching.com/my/"))); final String profile = TextUtils.replaceWhitespace(responseData); - Settings.setMemberStatus(TextUtils.getMatch(profile, GCConstants.PATTERN_MEMBER_STATUS, true, null)); + Settings.setGCMemberStatus(TextUtils.getMatch(profile, GCConstants.PATTERN_MEMBER_STATUS, true, null)); if (profile.contains(GCConstants.MEMBER_STATUS_RENEW)) { - Settings.setMemberStatus(GCConstants.MEMBER_STATUS_PM); + Settings.setGCMemberStatus(GCConstants.MEMBER_STATUS_PM); } setActualCachesFound(Integer.parseInt(TextUtils.getMatch(profile, GCConstants.PATTERN_CACHES_FOUND, true, "-1").replaceAll("[,.]", ""))); diff --git a/main/src/cgeo/geocaching/connector/gc/GCMap.java b/main/src/cgeo/geocaching/connector/gc/GCMap.java index 6c94150..2782b64 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCMap.java +++ b/main/src/cgeo/geocaching/connector/gc/GCMap.java @@ -368,7 +368,7 @@ public class GCMap { } } - if (strategy.flags.contains(StrategyFlag.SEARCH_NEARBY) && Settings.isPremiumMember()) { + if (strategy.flags.contains(StrategyFlag.SEARCH_NEARBY) && Settings.isGCPremiumMember()) { final Geopoint center = viewport.getCenter(); if ((lastSearchViewport == null) || !lastSearchViewport.contains(center)) { //FIXME We don't have a RecaptchaReceiver!? diff --git a/main/src/cgeo/geocaching/connector/gc/GCParser.java b/main/src/cgeo/geocaching/connector/gc/GCParser.java index 219adc8..8e59e5e 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCParser.java +++ b/main/src/cgeo/geocaching/connector/gc/GCParser.java @@ -36,7 +36,6 @@ import cgeo.geocaching.utils.SynchronizedDateFormat; import cgeo.geocaching.utils.TextUtils; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringEscapeUtils; @@ -63,12 +62,13 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.regex.Pattern; public abstract class GCParser { private final static SynchronizedDateFormat dateTbIn1 = new SynchronizedDateFormat("EEEEE, dd MMMMM yyyy", Locale.ENGLISH); // Saturday, 28 March 2009 private final static SynchronizedDateFormat dateTbIn2 = new SynchronizedDateFormat("EEEEE, MMMMM dd, yyyy", Locale.ENGLISH); // Saturday, March 28, 2009 - private static SearchResult parseSearch(final String url, final String pageContent, final boolean showCaptcha, RecaptchaReceiver thread) { + private static SearchResult parseSearch(final String url, final String pageContent, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(pageContent)) { Log.e("GCParser.parseSearch: No page given"); return null; @@ -86,12 +86,12 @@ public abstract class GCParser { final String recaptchaJsParam = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_RECAPTCHA, false, null); if (recaptchaJsParam != null) { - thread.setKey(recaptchaJsParam.trim()); + recaptchaReceiver.setKey(recaptchaJsParam.trim()); - thread.fetchChallenge(); + recaptchaReceiver.fetchChallenge(); } - if (thread != null && StringUtils.isNotBlank(thread.getChallenge())) { - thread.notifyNeed(); + if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) { + recaptchaReceiver.notifyNeed(); } } @@ -277,12 +277,12 @@ public abstract class GCParser { } String recaptchaText = null; - if (thread != null && StringUtils.isNotBlank(thread.getChallenge())) { - thread.waitForUser(); - recaptchaText = thread.getText(); + if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) { + recaptchaReceiver.waitForUser(); + recaptchaText = recaptchaReceiver.getText(); } - if (!cids.isEmpty() && (Settings.isPremiumMember() || showCaptcha) && ((thread == null || StringUtils.isBlank(thread.getChallenge())) || StringUtils.isNotBlank(recaptchaText))) { + if (!cids.isEmpty() && (Settings.isGCPremiumMember() || showCaptcha) && ((recaptchaReceiver == null || StringUtils.isBlank(recaptchaReceiver.getChallenge())) || StringUtils.isNotBlank(recaptchaText))) { Log.i("Trying to get .loc for " + cids.size() + " caches"); try { @@ -303,8 +303,8 @@ public abstract class GCParser { params.put("CID", cid); } - if (thread != null && StringUtils.isNotBlank(thread.getChallenge()) && StringUtils.isNotBlank(recaptchaText)) { - params.put("recaptcha_challenge_field", thread.getChallenge()); + if (StringUtils.isNotBlank(recaptchaText) && recaptchaReceiver != null) { + params.put("recaptcha_challenge_field", recaptchaReceiver.getChallenge()); params.put("recaptcha_response_field", recaptchaText); } params.put("ctl00$ContentBody$uxDownloadLoc", "Download Waypoints"); @@ -347,6 +347,9 @@ public abstract class GCParser { // attention: parseCacheFromText already stores implicitly through searchResult.addCache if (searchResult != null && !searchResult.getGeocodes().isEmpty()) { final Geocache cache = searchResult.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB); + if (cache == null) { + return null; + } getExtraOnlineInfo(cache, page, handler); // too late: it is already stored through parseCacheFromText cache.setDetailedUpdatedNow(); @@ -741,7 +744,7 @@ public abstract class GCParser { cache.parseWaypointsFromNote(); // logs - cache.setLogs(loadLogsFromDetails(page, cache, false, true)); + cache.setLogs(getLogsFromDetails(page, false)); // last check for necessary cache conditions if (StringUtils.isBlank(cache.getGeocode())) { @@ -1308,7 +1311,7 @@ public abstract class GCParser { return false; // error } - final boolean guidOnPage = cache.isGuidContainedInPage(page); + final boolean guidOnPage = isGuidContainedInPage(cache, page); if (guidOnPage) { Log.i("GCParser.addToWatchlist: cache is on watchlist"); cache.setOnWatchlist(true); @@ -1342,7 +1345,7 @@ public abstract class GCParser { GCLogin.transferViewstates(page, params); page = Network.getResponseData(Network.postRequest(uri, params)); - final boolean guidOnPage = cache.isGuidContainedInPage(page); + final boolean guidOnPage = isGuidContainedInPage(cache, page); if (!guidOnPage) { Log.i("GCParser.removeFromWatchlist: cache removed from watchlist"); cache.setOnWatchlist(false); @@ -1352,6 +1355,21 @@ public abstract class GCParser { return !guidOnPage; // on watch list (=error) / not on watch list } + /** + * Checks if a page contains the guid of a cache + * + * @param cache the geocache + * @param page + * the page to search in, may be null + * @return true if the page contains the guid of the cache, false otherwise + */ + private static boolean isGuidContainedInPage(final Geocache cache, final String page) { + if (StringUtils.isBlank(page) || StringUtils.isBlank(cache.getGuid())) { + return false; + } + return Pattern.compile(cache.getGuid(), Pattern.CASE_INSENSITIVE).matcher(page).find(); + } + @Nullable static String requestHtmlPage(@Nullable final String geocode, @Nullable final String guid, final String log, final String numlogs) { final Parameters params = new Parameters("decrypt", "y"); @@ -1380,8 +1398,7 @@ public abstract class GCParser { } private static boolean changeFavorite(final Geocache cache, final boolean add) { - final String page = requestHtmlPage(cache.getGeocode(), null, "n", "0"); - final String userToken = TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); + final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } @@ -1400,6 +1417,11 @@ public abstract class GCParser { return false; } + private static String getUserToken(final Geocache cache) { + final String page = requestHtmlPage(cache.getGeocode(), null, "n", "0"); + return TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); + } + /** * Removes the cache from the favorites. * @@ -1617,19 +1639,20 @@ public abstract class GCParser { } /** - * Load logs from a cache details page. + * Extract logs from a cache details page. * * @param page * the text of the details page - * @param cache - * the cache object to put the logs in * @param friends - * retrieve friend logs + * return friends logs only (will require a network request) + * @return a list of log entries or <code>null</code> if the logs could not be retrieved + * */ - private static List<LogEntry> loadLogsFromDetails(final String page, final Geocache cache, final boolean friends, final boolean getDataFromPage) { + @Nullable + private static List<LogEntry> getLogsFromDetails(final String page, final boolean friends) { String rawResponse; - if (!getDataFromPage) { + if (friends) { final MatcherWrapper userTokenMatcher = new MatcherWrapper(GCConstants.PATTERN_USERTOKEN, page); if (!userTokenMatcher.find()) { Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); @@ -1716,7 +1739,7 @@ public abstract class GCParser { final JSONArray images = entry.getJSONArray("Images"); for (int i = 0; i < images.length(); i++) { final JSONObject image = images.getJSONObject(i); - final String url = "http://img.geocaching.com/cache/log/large/" + image.getString("FileName"); + final String url = "http://imgcdn.geocaching.com/cache/log/large/" + image.getString("FileName"); final String title = TextUtils.removeControlCharacters(image.getString("Name")); final Image logImage = new Image(url, title); logDone.addLogImage(logImage); @@ -1829,7 +1852,7 @@ public abstract class GCParser { if (Settings.isFriendLogsWanted()) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); final List<LogEntry> allLogs = cache.getLogs(); - final List<LogEntry> friendLogs = loadLogsFromDetails(page, cache, true, false); + final List<LogEntry> friendLogs = getLogsFromDetails(page, true); if (friendLogs != null) { for (final LogEntry log : friendLogs) { if (allLogs.contains(log)) { @@ -1864,8 +1887,7 @@ public abstract class GCParser { } public static boolean editModifiedCoordinates(Geocache cache, Geopoint wpt) { - final String page = requestHtmlPage(cache.getGeocode(), null, "n", "0"); - final String userToken = TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); + final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } @@ -1900,8 +1922,7 @@ public abstract class GCParser { } public static boolean uploadPersonalNote(Geocache cache) { - final String page = requestHtmlPage(cache.getGeocode(), null, "n", "0"); - final String userToken = TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); + final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } diff --git a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java new file mode 100644 index 0000000..ff3f5ef --- /dev/null +++ b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java @@ -0,0 +1,114 @@ +package cgeo.geocaching.connector.gc; + +import cgeo.geocaching.R; +import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.network.Network; +import cgeo.geocaching.utils.Log; + +import org.apache.commons.io.IOUtils; + +import rx.Observable; +import rx.android.observables.AndroidObservable; +import rx.schedulers.Schedulers; +import rx.functions.Action1; +import rx.functions.Func0; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; + +import java.io.InputStream; + +public class RecaptchaHandler extends Handler { + final public static int SHOW_CAPTCHA = 1; + + final private Activity activity; + final private RecaptchaReceiver recaptchaReceiver; + + public RecaptchaHandler(final Activity activity, final RecaptchaReceiver recaptchaReceiver) { + this.activity = activity; + this.recaptchaReceiver = recaptchaReceiver; + } + + private void loadChallenge(final ImageView imageView, final View reloadButton) { + getCaptcha().subscribe(new Action1<Bitmap>() { + @Override + public void call(final Bitmap bitmap) { + imageView.setImageBitmap(bitmap); + } + }, new Action1<Throwable>() { + @Override + public void call(final Throwable throwable) { + // Do nothing + } + }); + reloadButton.setEnabled(true); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == SHOW_CAPTCHA) { + final AlertDialog.Builder dlg = new AlertDialog.Builder(activity); + final View view = activity.getLayoutInflater().inflate(R.layout.recaptcha_dialog, null); + + final ImageView imageView = (ImageView) view.findViewById(R.id.image); + + final ImageButton reloadButton = (ImageButton) view.findViewById(R.id.button_recaptcha_refresh); + reloadButton.setEnabled(false); + reloadButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + recaptchaReceiver.fetchChallenge(); + loadChallenge(imageView, reloadButton); + } + }); + + loadChallenge(imageView, reloadButton); + + dlg.setTitle(activity.getString(R.string.caches_recaptcha_title)); + dlg.setView(view); + dlg.setNeutralButton(activity.getString(R.string.caches_recaptcha_continue), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + final String text = ((EditText) view.findViewById(R.id.text)).getText().toString(); + recaptchaReceiver.setText(text); + dialog.cancel(); + } + }); + + dlg.create().show(); + } + } + + private Observable<Bitmap> getCaptcha() { + return AndroidObservable.fromActivity(activity, + Observable.defer(new Func0<Observable<? extends Bitmap>>() { + @Override + public Observable<? extends Bitmap> call() { + final String url = "http://www.google.com/recaptcha/api/image?c=" + recaptchaReceiver.getChallenge(); + final InputStream is = Network.getResponseStream(Network.getRequest(url)); + if (is != null) { + try { + final Bitmap img = BitmapFactory.decodeStream(is); + return Observable.from(img); + } catch (final Exception e) { + Log.e("RecaptchaHandler.getCaptcha", e); + return Observable.error(e); + } finally { + IOUtils.closeQuietly(is); + } + } + return Observable.empty(); + } + }).subscribeOn(Schedulers.io())); + } + +} diff --git a/main/src/cgeo/geocaching/connector/gc/SearchHandler.java b/main/src/cgeo/geocaching/connector/gc/SearchHandler.java deleted file mode 100644 index 795ed2f..0000000 --- a/main/src/cgeo/geocaching/connector/gc/SearchHandler.java +++ /dev/null @@ -1,126 +0,0 @@ -package cgeo.geocaching.connector.gc; - -import cgeo.geocaching.R; -import cgeo.geocaching.loaders.RecaptchaReceiver; -import cgeo.geocaching.utils.Log; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Handler; -import android.os.Message; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.ImageView; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - -public class SearchHandler extends Handler { - private Activity activity = null; - private Resources res = null; - private RecaptchaReceiver recaptchaThread = null; - private ImageView imageView = null; - private Bitmap img = null; - - private Handler imgHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - try { - if (img != null && imageView != null) { - imageView.setImageBitmap(img); - } - } catch (Exception e) { - Log.e("Error setting reCAPTCHA image", e); - } - } - }; - - public SearchHandler(Activity activityIn, Resources resIn, RecaptchaReceiver recaptchaThreadIn) { - activity = activityIn; - res = resIn; - recaptchaThread = recaptchaThreadIn; - } - - @Override - public void handleMessage(Message msg) { - try { - if (msg.what == 1) { - final AlertDialog.Builder dlg = new AlertDialog.Builder(activity); - final LayoutInflater inflater = activity.getLayoutInflater(); - final View view = inflater.inflate(R.layout.recaptcha_dialog, null); - - imageView = (ImageView) view.findViewById(R.id.image); - - ImageButton reloadButton = (ImageButton) view.findViewById(R.id.button_recaptcha_refresh); - reloadButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - recaptchaThread.fetchChallenge(); - try { - (new GetCaptchaThread(new URL("http://www.google.com/recaptcha/api/image?c=" + recaptchaThread.getChallenge()))).start(); - } catch (MalformedURLException e) { - Log.e("Bad reCAPTCHA image url", e); - } - } - }); - - (new GetCaptchaThread(new URL("http://www.google.com/recaptcha/api/image?c=" + recaptchaThread.getChallenge()))).start(); - - dlg.setTitle(res.getString(R.string.caches_recaptcha_title)); - dlg.setView(view); - dlg.setNeutralButton(res.getString(R.string.caches_recaptcha_continue), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - final String text = ((EditText) view.findViewById(R.id.text)).getText().toString(); - - recaptchaThread.setText(text); - - dialog.cancel(); - } - }); - - dlg.create().show(); - } - } catch (MalformedURLException e) { - Log.e("Error in reCAPTCHA handler", e); - } - } - - private class GetCaptchaThread extends Thread { - private URL uri = null; - - public GetCaptchaThread(URL uriIn) { - uri = uriIn; - } - - @Override - public void run() { - try { - Log.d("Getting reCAPTCHA image from: " + uri.toString()); - HttpURLConnection connection = (HttpURLConnection) uri.openConnection(); - connection.setDoInput(true); - connection.connect(); - - InputStream is = connection.getInputStream(); - - img = BitmapFactory.decodeStream(is); - - is.close(); - - imgHandler.sendEmptyMessage(0); - } catch (IOException e) { - Log.e("Failed to download reCAPTCHA image", e); - } - } - } -} diff --git a/main/src/cgeo/geocaching/connector/gc/Tile.java b/main/src/cgeo/geocaching/connector/gc/Tile.java index 623730a..6a257cd 100644 --- a/main/src/cgeo/geocaching/connector/gc/Tile.java +++ b/main/src/cgeo/geocaching/connector/gc/Tile.java @@ -10,6 +10,8 @@ import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; +import org.eclipse.jdt.annotation.NonNull; + import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -110,6 +112,7 @@ public class Tile { * @see <a * href="http://developers.cloudmade.com/projects/tiles/examples/convert-coordinates-to-tile-numbers">Cloudmade</a> */ + @NonNull public Geopoint getCoord(UTFGridPosition pos) { double pixX = tileX * TILE_SIZE + pos.x * 4; @@ -244,7 +247,7 @@ public class Tile { return null; } - public boolean containsPoint(final ICoordinates point) { + public boolean containsPoint(final @NonNull ICoordinates point) { return viewPort.contains(point); } diff --git a/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java b/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java index dacb626..acf7b48 100644 --- a/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java +++ b/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java @@ -1,5 +1,7 @@ package cgeo.geocaching.connector.oc; +import org.eclipse.jdt.annotation.NonNull; + public interface IOCAuthParams { /** @@ -7,6 +9,7 @@ public interface IOCAuthParams { * * @return */ + @NonNull String getSite(); /** @@ -63,5 +66,6 @@ public interface IOCAuthParams { * * @return */ + @NonNull String getCallbackUri(); } diff --git a/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java b/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java index 46e4c96..284234e 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java +++ b/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java @@ -90,4 +90,16 @@ public class OCApiConnector extends OCConnector implements ISearchByGeocode { public int getTokenSecretPrefKeyId() { return 0; } + + /** + * Checks if a search based on a user name targets the current user + * + * @param username + * Name of the user the query is searching after + * @return True - search target and current is same, False - current user not known or not the same as username + */ + @SuppressWarnings("static-method") + public boolean isSearchForMyCaches(String username) { + return false; + } } diff --git a/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java b/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java index 0b7493c..049c633 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java +++ b/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java @@ -173,4 +173,10 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente final Geopoint currentPos = CgeoApplication.getInstance().currentGeo().getCoords(); return new SearchResult(OkapiClient.getCachesNamed(currentPos, name, this)); } + + @Override + public boolean isSearchForMyCaches(String username) { + return StringUtils.equalsIgnoreCase(username, getUserName()); + } + } diff --git a/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java b/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java index c082bac..19f4447 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java +++ b/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java @@ -2,9 +2,13 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; +import cgeo.geocaching.connector.oc.OkapiError.OkapiErrors; import cgeo.geocaching.network.OAuthAuthorizationActivity; import cgeo.geocaching.settings.Settings; +import ch.boye.httpclientandroidlib.HttpResponse; + +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.Nullable; @@ -52,4 +56,16 @@ public abstract class OCAuthorizationActivity extends OAuthAuthorizationActivity return res.getString(R.string.auth_dialog_completed_oc, getAuthTitle()); } + /** + * Return an extended error in case of an invalid time stamp + */ + @Override + protected String getExtendedErrorMsg(HttpResponse response) { + OkapiError error = OkapiClient.decodeErrorResponse(response); + if (error.getResult() == OkapiErrors.INVALID_TIMESTAMP) { + return res.getString(R.string.init_login_popup_invalid_timestamp); + } + return StringUtils.EMPTY; + } + } diff --git a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java index 2175935..712bb26 100644 --- a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java +++ b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java @@ -34,7 +34,6 @@ import cgeo.geocaching.utils.SynchronizedDateFormat; import ch.boye.httpclientandroidlib.HttpResponse; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.FastDateFormat; import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -61,7 +60,7 @@ final class OkapiClient { private static final char SEPARATOR = '|'; private static final String SEPARATOR_STRING = Character.toString(SEPARATOR); - private static final FastDateFormat LOG_DATE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); + private static final SynchronizedDateFormat LOG_DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); private static final SynchronizedDateFormat ISO8601DATEFORMAT = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); private static final String CACHE_ATTRNAMES = "attrnames"; @@ -75,7 +74,8 @@ final class OkapiClient { private static final String CACHE_STATUS_ARCHIVED = "Archived"; private static final String CACHE_STATUS_DISABLED = "Temporarily unavailable"; private static final String CACHE_IS_FOUND = "is_found"; - private static final String CACHE_SIZE = "size"; + private static final String CACHE_SIZE_DEPRECATED = "size"; + private static final String CACHE_SIZE2 = "size2"; private static final String CACHE_VOTES = "rating_votes"; private static final String CACHE_NOTFOUNDS = "notfounds"; private static final String CACHE_FOUNDS = "founds"; @@ -112,7 +112,7 @@ final class OkapiClient { // the several realms of possible fields for cache retrieval: // Core: for livemap requests (L3 - only with level 3 auth) // Additional: additional fields for full cache (L3 - only for level 3 auth, current - only for connectors with current api) - private static final String SERVICE_CACHE_CORE_FIELDS = "code|name|location|type|status|difficulty|terrain|size|date_hidden"; + private static final String SERVICE_CACHE_CORE_FIELDS = "code|name|location|type|status|difficulty|terrain|size|size2|date_hidden"; private static final String SERVICE_CACHE_CORE_L3_FIELDS = "is_found"; private static final String SERVICE_CACHE_ADDITIONAL_FIELDS = "owner|founds|notfounds|rating|rating_votes|recommendations|description|hint|images|latest_logs|alt_wpts|attrnames|req_passwd"; private static final String SERVICE_CACHE_ADDITIONAL_CURRENT_FIELDS = "gc_code|attribution_note|attr_acodes"; @@ -148,23 +148,18 @@ final class OkapiClient { valueMap.put("limit", "20"); valueMap.put("radius", "200"); - return requestCaches(connector, params, valueMap); + return requestCaches(connector, params, valueMap, false); } public static List<Geocache> getCachesByOwner(final String username, final OCApiConnector connector) { - final Parameters params = new Parameters("search_method", METHOD_SEARCH_ALL); - final Map<String, String> valueMap = new LinkedHashMap<String, String>(); - final @Nullable - String uuid = getUserUUID(connector, username); - if (StringUtils.isEmpty(uuid)) { - return Collections.emptyList(); - } - valueMap.put("owner_uuid", uuid); - - return requestCaches(connector, params, valueMap); + return getCachesByUser(username, connector, "owner_uuid"); } public static List<Geocache> getCachesByFinder(final String username, final OCApiConnector connector) { + return getCachesByUser(username, connector, "found_by"); + } + + private static List<Geocache> getCachesByUser(final String username, final OCApiConnector connector, final String userRequestParam) { final Parameters params = new Parameters("search_method", METHOD_SEARCH_ALL); final Map<String, String> valueMap = new LinkedHashMap<String, String>(); final @Nullable @@ -172,9 +167,9 @@ final class OkapiClient { if (StringUtils.isEmpty(uuid)) { return Collections.emptyList(); } - valueMap.put("found_by", uuid); + valueMap.put(userRequestParam, uuid); - return requestCaches(connector, params, valueMap); + return requestCaches(connector, params, valueMap, connector.isSearchForMyCaches(username)); } public static List<Geocache> getCachesNamed(final Geopoint center, final String namePart, final OCApiConnector connector) { @@ -195,16 +190,16 @@ final class OkapiClient { // full wildcard search, maybe we need to change this after some testing and evaluation valueMap.put("name", "*" + namePart + "*"); - return requestCaches(connector, params, valueMap); + return requestCaches(connector, params, valueMap, false); } - private static List<Geocache> requestCaches(final OCApiConnector connector, final Parameters params, final Map<String, String> valueMap) { + private static List<Geocache> requestCaches(final OCApiConnector connector, final Parameters params, final Map<String, String> valueMap, final boolean my) { // if a global type filter is set, and OKAPI does not know that type, then return an empty list instead of all caches if (Settings.getCacheType() != CacheType.ALL && StringUtils.isBlank(getFilterFromType())) { return Collections.emptyList(); } - addFilterParams(valueMap, connector); + addFilterParams(valueMap, connector, my); params.add("search_params", new JSONObject(valueMap).toString()); addRetrieveParams(params, connector); @@ -234,7 +229,7 @@ final class OkapiClient { final Map<String, String> valueMap = new LinkedHashMap<String, String>(); valueMap.put("bbox", bboxString); - return requestCaches(connector, params, valueMap); + return requestCaches(connector, params, valueMap, false); } public static boolean setWatchState(final Geocache cache, final boolean watched, final OCApiConnector connector) { @@ -386,6 +381,7 @@ final class OkapiClient { } if (!response.isNull(CACHE_MY_NOTES)) { cache.setPersonalNote(response.getString(CACHE_MY_NOTES)); + cache.parseWaypointsFromNote(); } cache.setLogPasswordRequired(response.getBoolean(CACHE_REQ_PASSWORD)); @@ -489,6 +485,30 @@ final class OkapiClient { if ("Didn't find it".equalsIgnoreCase(logType)) { return LogType.DIDNT_FIND_IT; } + if ("Will attend".equalsIgnoreCase(logType)) { + return LogType.WILL_ATTEND; + } + if ("Attended".equalsIgnoreCase(logType)) { + return LogType.ATTENDED; + } + if ("Temporarily unavailable".equalsIgnoreCase(logType)) { + return LogType.TEMP_DISABLE_LISTING; + } + if ("Ready to search".equalsIgnoreCase(logType)) { + return LogType.ENABLE_LISTING; + } + if ("Archived".equalsIgnoreCase(logType)) { + return LogType.ARCHIVE; + } + if ("Needs maintenance".equalsIgnoreCase(logType)) { + return LogType.NEEDS_MAINTENANCE; + } + if ("Moved".equalsIgnoreCase(logType)) { + return LogType.UPDATE_COORDINATES; + } + if ("OC Team comment".equalsIgnoreCase(logType)) { + return LogType.POST_REVIEWER_NOTE; + } return LogType.NOTE; } @@ -567,12 +587,25 @@ final class OkapiClient { } private static CacheSize getCacheSize(final JSONObject response) { - if (response.isNull(CACHE_SIZE)) { + if (response.isNull(CACHE_SIZE2)) { + return getCacheSizeDeprecated(response); + } + try { + final String size = response.getString(CACHE_SIZE2); + return CacheSize.getById(size); + } catch (JSONException e) { + Log.e("OkapiClient.getCacheSize", e); + return getCacheSizeDeprecated(response); + } + } + + private static CacheSize getCacheSizeDeprecated(final JSONObject response) { + if (response.isNull(CACHE_SIZE_DEPRECATED)) { return CacheSize.NOT_CHOSEN; } double size = 0; try { - size = response.getDouble(CACHE_SIZE); + size = response.getDouble(CACHE_SIZE_DEPRECATED); } catch (final JSONException e) { Log.e("OkapiClient.getCacheSize", e); } @@ -586,7 +619,7 @@ final class OkapiClient { case 4: return CacheSize.LARGE; case 5: - return CacheSize.LARGE; + return CacheSize.VERY_LARGE; default: break; } @@ -687,11 +720,11 @@ final class OkapiClient { return "en"; } - private static void addFilterParams(final Map<String, String> valueMap, final OCApiConnector connector) { + private static void addFilterParams(final Map<String, String> valueMap, final OCApiConnector connector, final boolean my) { if (!Settings.isExcludeDisabledCaches()) { valueMap.put("status", "Available|Temporarily unavailable"); } - if (Settings.isExcludeMyCaches() && connector.getSupportedAuthLevel() == OAuthLevel.Level3) { + if (!my && Settings.isExcludeMyCaches() && connector.getSupportedAuthLevel() == OAuthLevel.Level3) { valueMap.put("exclude_my_own", "true"); valueMap.put("found_status", "notfound_only"); } @@ -789,6 +822,21 @@ final class OkapiClient { } /** + * Retrieves error information from an unsuccessful Okapi-response + * + * @param response + * response containing an error object + * @return OkapiError object with detailed information + */ + public static OkapiError decodeErrorResponse(HttpResponse response) { + final JSONResult result = new JSONResult(response); + if (!result.isSuccess) { + return new OkapiError(result.data); + } + return new OkapiError(new JSONObject()); + } + + /** * Encapsulates response state and content of an HTTP-request that expects a JSON result. <code>isSuccess</code> is * only true, if the response state was success and <code>data</code> is not null. */ diff --git a/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java b/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java index f72b698..5f11a11 100644 --- a/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java +++ b/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java @@ -28,10 +28,10 @@ public class OXGPXParser extends GPX10Parser { /** * The short description of OX caches contains "title by owner, type(T/D/Awesomeness)". That is a lot of * duplication. - * + * * @param cache */ private static void removeTitleFromShortDescription(final @NonNull Geocache cache) { - cache.setShortDescription(StringUtils.substringAfterLast(cache.getShortDescription(), ",")); + cache.setShortDescription(StringUtils.trim(StringUtils.substringAfterLast(cache.getShortDescription(), ","))); } } diff --git a/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java b/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java index 2defc52..0137af4 100644 --- a/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java +++ b/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java @@ -11,7 +11,6 @@ import cgeo.geocaching.utils.CryptUtils; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.collections4.CollectionUtils; import org.eclipse.jdt.annotation.NonNull; diff --git a/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java b/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java index 69efddc..fb554b9 100644 --- a/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java @@ -4,6 +4,7 @@ import cgeo.geocaching.connector.AbstractConnector; import cgeo.geocaching.connector.UserAction; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import java.util.List; @@ -15,7 +16,8 @@ public abstract class AbstractTrackableConnector implements TrackableConnector { } @Override - public String getTrackableCodeFromUrl(@NonNull String url) { + public @Nullable + String getTrackableCodeFromUrl(@NonNull String url) { return null; } diff --git a/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java b/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java index 8387076..03052f9 100644 --- a/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java @@ -4,6 +4,10 @@ import cgeo.geocaching.Trackable; import cgeo.geocaching.network.Network; import cgeo.geocaching.utils.Log; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import java.util.regex.Pattern; public class GeokretyConnector extends AbstractTrackableConnector { @@ -29,7 +33,7 @@ public class GeokretyConnector extends AbstractTrackableConnector { return GeokretyParser.parse(page); } - private static int getId(String geocode) { + protected static int getId(String geocode) { try { final String hex = geocode.substring(2); return Integer.parseInt(hex, 16); @@ -39,4 +43,25 @@ public class GeokretyConnector extends AbstractTrackableConnector { return -1; } + @Override + public @Nullable + String getTrackableCodeFromUrl(@NonNull String url) { + // http://geokrety.org/konkret.php?id=38545 + String id = StringUtils.substringAfterLast(url, "konkret.php?id="); + if (StringUtils.isNumeric(id)) { + return geocode(Integer.parseInt(id)); + } + return null; + } + + /** + * Get geocode from geokrety id + * + * @param id + * @return + */ + public static String geocode(final int id) { + return String.format("GK%04X", id); + } + } diff --git a/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java b/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java index ee8c8c0..0e64abd 100644 --- a/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java +++ b/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java @@ -37,8 +37,8 @@ public class GeokretyParser { public void start(Attributes attributes) { try { final String kretyId = attributes.getValue("id"); - if (StringUtils.isNotBlank(kretyId)) { - trackable.setGeocode(geocode(Integer.parseInt(kretyId))); + if (StringUtils.isNumeric(kretyId)) { + trackable.setGeocode(GeokretyConnector.geocode(Integer.parseInt(kretyId))); } final String distance = attributes.getValue("dist"); if (StringUtils.isNotBlank(distance)) { @@ -88,8 +88,4 @@ public class GeokretyParser { } return null; } - - protected static String geocode(final int id) { - return String.format("GK%04X", id); - } } diff --git a/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java b/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java index 0990d96..6071b5f 100644 --- a/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java @@ -4,6 +4,7 @@ import cgeo.geocaching.Trackable; import cgeo.geocaching.connector.UserAction; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import java.util.List; @@ -21,7 +22,8 @@ public interface TrackableConnector { public Trackable searchTrackable(String geocode, String guid, String id); - public String getTrackableCodeFromUrl(final @NonNull String url); + public @Nullable + String getTrackableCodeFromUrl(final @NonNull String url); public @NonNull List<UserAction> getUserActions(); diff --git a/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java b/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java index dad285c..77848d7 100644 --- a/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java @@ -7,6 +7,7 @@ import cgeo.geocaching.connector.gc.GCParser; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import java.util.List; import java.util.regex.Pattern; @@ -20,7 +21,7 @@ public class TravelBugConnector extends AbstractTrackableConnector { @Override public boolean canHandleTrackable(String geocode) { - return TravelBugConnector.PATTERN_TB_CODE.matcher(geocode).matches(); + return TravelBugConnector.PATTERN_TB_CODE.matcher(geocode).matches() && !StringUtils.startsWithIgnoreCase(geocode, "GC"); } @Override @@ -54,8 +55,18 @@ public class TravelBugConnector extends AbstractTrackableConnector { } @Override - public String getTrackableCodeFromUrl(@NonNull String url) { - return StringUtils.substringAfterLast(url, "?tracker="); + public @Nullable + String getTrackableCodeFromUrl(@NonNull String url) { + // coord.info URLs + String code = StringUtils.substringAfterLast(url, "coord.info/"); + if (code != null && canHandleTrackable(code)) { + return code; + } + code = StringUtils.substringAfterLast(url, "?tracker="); + if (code != null && canHandleTrackable(code)) { + return code; + } + return null; } @Override diff --git a/main/src/cgeo/geocaching/enumerations/CacheAttribute.java b/main/src/cgeo/geocaching/enumerations/CacheAttribute.java index 472bad5..0703c3c 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheAttribute.java +++ b/main/src/cgeo/geocaching/enumerations/CacheAttribute.java @@ -164,15 +164,11 @@ public enum CacheAttribute { } private final static Map<String, CacheAttribute> FIND_BY_GCRAWNAME; - private final static SparseArray<CacheAttribute> FIND_BY_GCID = new SparseArray<CacheAttribute>(); private final static SparseArray<CacheAttribute> FIND_BY_OCACODE = new SparseArray<CacheAttribute>(); static { final HashMap<String, CacheAttribute> mapGcRawNames = new HashMap<String, CacheAttribute>(); for (CacheAttribute attr : values()) { mapGcRawNames.put(attr.rawName, attr); - if (attr.gcid != NO_ID) { - FIND_BY_GCID.put(attr.gcid, attr); - } if (attr.ocacode != NO_ID) { FIND_BY_OCACODE.put(attr.ocacode, attr); } @@ -184,10 +180,6 @@ public enum CacheAttribute { return rawName != null ? FIND_BY_GCRAWNAME.get(rawName) : null; } - public static CacheAttribute getByGcId(final int gcid) { - return FIND_BY_GCID.get(gcid); - } - public static CacheAttribute getByOcACode(final int ocAcode) { return FIND_BY_OCACODE.get(ocAcode); } diff --git a/main/src/cgeo/geocaching/enumerations/CacheListType.java b/main/src/cgeo/geocaching/enumerations/CacheListType.java index f482d5b..1fce282 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheListType.java +++ b/main/src/cgeo/geocaching/enumerations/CacheListType.java @@ -1,23 +1,32 @@ package cgeo.geocaching.enumerations; +import cgeo.geocaching.loaders.AbstractSearchLoader.CacheListLoaderType; + public enum CacheListType { - OFFLINE(true), - POCKET(false), - HISTORY(true), - NEAREST(false), - COORDINATE(false), - KEYWORD(false), - ADDRESS(false), - USERNAME(false), - OWNER(false), - MAP(false); + OFFLINE(true, CacheListLoaderType.OFFLINE), + POCKET(false, CacheListLoaderType.POCKET), + HISTORY(true, CacheListLoaderType.HISTORY), + NEAREST(false, CacheListLoaderType.NEAREST), + COORDINATE(false, CacheListLoaderType.COORDINATE), + KEYWORD(false, CacheListLoaderType.KEYWORD), + ADDRESS(false, CacheListLoaderType.ADDRESS), + FINDER(false, CacheListLoaderType.FINDER), + OWNER(false, CacheListLoaderType.OWNER), + MAP(false, CacheListLoaderType.MAP); /** * whether or not this list allows switching to another list */ public final boolean canSwitch; - private CacheListType(final boolean canSwitch) { + public final CacheListLoaderType loaderType; + + CacheListType(final boolean canSwitch, final CacheListLoaderType loaderType) { this.canSwitch = canSwitch; + this.loaderType = loaderType; + } + + public int getLoaderId() { + return loaderType.getLoaderId(); } } diff --git a/main/src/cgeo/geocaching/enumerations/CacheSize.java b/main/src/cgeo/geocaching/enumerations/CacheSize.java index ee42c66..1255455 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheSize.java +++ b/main/src/cgeo/geocaching/enumerations/CacheSize.java @@ -12,30 +12,38 @@ import java.util.Map; * Enum listing cache sizes */ public enum CacheSize { - MICRO("Micro", 1, R.string.cache_size_micro), - SMALL("Small", 2, R.string.cache_size_small), - REGULAR("Regular", 3, R.string.cache_size_regular), - LARGE("Large", 4, R.string.cache_size_large), - VIRTUAL("Virtual", 0, R.string.cache_size_virtual), - NOT_CHOSEN("Not chosen", 0, R.string.cache_size_notchosen), - OTHER("Other", 0, R.string.cache_size_other), - UNKNOWN("Unknown", 0, R.string.cache_size_unknown); // CacheSize not init. yet + NANO("Nano", 0, R.string.cache_size_nano, "nano"), // used by OC only + MICRO("Micro", 1, R.string.cache_size_micro, "micro"), + SMALL("Small", 2, R.string.cache_size_small, "small"), + REGULAR("Regular", 3, R.string.cache_size_regular, "regular"), + LARGE("Large", 4, R.string.cache_size_large, "large"), + VERY_LARGE("Very large", 5, R.string.cache_size_very_large, "xlarge"), // used by OC only + NOT_CHOSEN("Not chosen", 6, R.string.cache_size_notchosen, ""), + VIRTUAL("Virtual", 7, R.string.cache_size_virtual, "none"), + OTHER("Other", 8, R.string.cache_size_other, "other"), + UNKNOWN("Unknown", -1, R.string.cache_size_unknown, ""); // CacheSize not init. yet public final String id; public final int comparable; private final int stringId; + /** + * lookup for OC JSON requests (the numeric size is deprecated for OC) + */ + private final String ocSize2; - CacheSize(String id, int comparable, int stringId) { + CacheSize(final String id, final int comparable, final int stringId, final String ocSize2) { this.id = id; this.comparable = comparable; this.stringId = stringId; + this.ocSize2 = ocSize2; } final private static Map<String, CacheSize> FIND_BY_ID; static { final HashMap<String, CacheSize> mapping = new HashMap<String, CacheSize>(); - for (CacheSize cs : values()) { + for (final CacheSize cs : values()) { mapping.put(cs.id.toLowerCase(Locale.US), cs); + mapping.put(cs.ocSize2.toLowerCase(Locale.US), cs); } // add medium as additional string for Regular mapping.put("medium", CacheSize.REGULAR); @@ -61,21 +69,21 @@ public enum CacheSize { /** * Bad GPX files can contain the container size encoded as number. - * + * * @param id * @return */ private static CacheSize getByNumber(final String id) { try { - int numerical = Integer.parseInt(id); + final int numerical = Integer.parseInt(id); if (numerical != 0) { - for (CacheSize size : CacheSize.values()) { + for (final CacheSize size : CacheSize.values()) { if (size.comparable == numerical) { return size; } } } - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { // ignore, as this might be a number or not } return UNKNOWN; @@ -85,4 +93,3 @@ public enum CacheSize { return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); } } - diff --git a/main/src/cgeo/geocaching/enumerations/CacheType.java b/main/src/cgeo/geocaching/enumerations/CacheType.java index c952ba0..35fe7a1 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheType.java +++ b/main/src/cgeo/geocaching/enumerations/CacheType.java @@ -1,8 +1,8 @@ package cgeo.geocaching.enumerations; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.ICache; import cgeo.geocaching.R; -import cgeo.geocaching.CgeoApplication; import java.util.Collections; import java.util.HashMap; diff --git a/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java b/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java index 710c3ba..5bcaf4a 100644 --- a/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java +++ b/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java @@ -1,7 +1,7 @@ package cgeo.geocaching.enumerations; -import cgeo.geocaching.R; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.R; import java.util.EnumSet; diff --git a/main/src/cgeo/geocaching/enumerations/LogType.java b/main/src/cgeo/geocaching/enumerations/LogType.java index 3240e8f..fa65b71 100644 --- a/main/src/cgeo/geocaching/enumerations/LogType.java +++ b/main/src/cgeo/geocaching/enumerations/LogType.java @@ -19,19 +19,19 @@ public enum LogType { DIDNT_FIND_IT(3, "3", "didn't find it", "Didn't find it", R.string.log_dnf, R.drawable.mark_red), NOTE(4, "4", "write note", "Comment", R.string.log_note), PUBLISH_LISTING(1003, "24", "publish listing", "", R.string.log_published, R.drawable.mark_green_more), - ENABLE_LISTING(23, "23", "enable listing", "", R.string.log_enabled, R.drawable.mark_green_more), - ARCHIVE(5, "5", "archive", "", R.string.log_archived, R.drawable.mark_red_more), + ENABLE_LISTING(23, "23", "enable listing", "Ready to search", R.string.log_enabled, R.drawable.mark_green_more), + ARCHIVE(5, "5", "archive", "Archived", R.string.log_archived, R.drawable.mark_red_more), UNARCHIVE(12, "12", "unarchive", "", R.string.log_unarchived, R.drawable.mark_green_more), - TEMP_DISABLE_LISTING(22, "22", "temporarily disable listing", "", R.string.log_disabled, R.drawable.mark_red_more), + TEMP_DISABLE_LISTING(22, "22", "temporarily disable listing", "Temporarily unavailable", R.string.log_disabled, R.drawable.mark_red_more), NEEDS_ARCHIVE(7, "7", "needs archived", "", R.string.log_needs_archived, R.drawable.mark_red), WILL_ATTEND(9, "9", "will attend", "Will attend", R.string.log_attend), ATTENDED(10, "10", "attended", "Attended", R.string.log_attended, R.drawable.mark_green), RETRIEVED_IT(13, "13", "retrieved it", "", R.string.log_retrieved, R.drawable.mark_green_more), PLACED_IT(14, "14", "placed it", "", R.string.log_placed, R.drawable.mark_green_more), GRABBED_IT(19, "19", "grabbed it", "", R.string.log_grabbed, R.drawable.mark_green_more), - NEEDS_MAINTENANCE(45, "45", "needs maintenance", "Comment", R.string.log_maintenance_needed, R.drawable.mark_red), + NEEDS_MAINTENANCE(45, "45", "needs maintenance", "Needs maintenance", R.string.log_maintenance_needed, R.drawable.mark_red), OWNER_MAINTENANCE(46, "46", "owner maintenance", "", R.string.log_maintained, R.drawable.mark_green_more), - UPDATE_COORDINATES(47, "47", "update coordinates", "", R.string.log_update), + UPDATE_COORDINATES(47, "47", "update coordinates", "Moved", R.string.log_update), DISCOVERED_IT(48, "48", "discovered it", "", R.string.log_discovered, R.drawable.mark_green), POST_REVIEWER_NOTE(18, "18", "post reviewer note", "", R.string.log_reviewer), SUBMIT_FOR_REVIEW(76, "76", "submit for review", "", R.string.log_submit_for_review), @@ -42,6 +42,7 @@ public enum LogType { MOVE_INVENTORY(70, "70", "unused_inventory", "", R.string.log_moveinventory), RETRACT(25, "25", "retract listing", "", R.string.log_retractlisting), MARKED_MISSING(16, "16", "marked missing", "", R.string.log_marked_missing, R.drawable.mark_red), + OC_TEAM_COMMENT(83, null, "X1", "OC Team comment", R.string.log_oc_team_comment), UNKNOWN(0, "unknown", "", "", R.string.err_unknown, R.drawable.mark_red); // LogType not init. yet public final int id; @@ -70,7 +71,9 @@ public enum LogType { final HashMap<String, LogType> mappingPattern = new HashMap<String, LogType>(); final HashMap<String, LogType> mappingType = new HashMap<String, LogType>(); for (LogType lt : values()) { - mappingPattern.put(lt.iconName, lt); + if (lt.iconName != null) { + mappingPattern.put(lt.iconName, lt); + } mappingType.put(lt.type, lt); } FIND_BY_ICONNAME = Collections.unmodifiableMap(mappingPattern); diff --git a/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java b/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java index 68a17a5..e008294 100644 --- a/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java +++ b/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java @@ -1,29 +1,22 @@ package cgeo.geocaching.enumerations; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; public enum LogTypeTrackable { - DO_NOTHING(0, "", R.string.log_tb_nothing), - VISITED(1, "_Visited", R.string.log_tb_visit), - DROPPED_OFF(2, "_DroppedOff", R.string.log_tb_drop); + DO_NOTHING("", R.string.log_tb_nothing), + VISITED("_Visited", R.string.log_tb_visit), + DROPPED_OFF("_DroppedOff", R.string.log_tb_drop); - final public int id; final public String action; - final public int resourceId; + final private int resourceId; - LogTypeTrackable(int id, String action, int resourceId) { - this.id = id; + LogTypeTrackable(String action, int resourceId) { this.action = action; this.resourceId = resourceId; } - public static LogTypeTrackable findById(int id) { - for (LogTypeTrackable logType : values()) { - if (logType.id == id) { - return logType; - } - } - return DO_NOTHING; + public String getLabel() { + return CgeoApplication.getInstance().getString(resourceId); } - } diff --git a/main/src/cgeo/geocaching/enumerations/StatusCode.java b/main/src/cgeo/geocaching/enumerations/StatusCode.java index e1a1aaa..8bda371 100644 --- a/main/src/cgeo/geocaching/enumerations/StatusCode.java +++ b/main/src/cgeo/geocaching/enumerations/StatusCode.java @@ -6,7 +6,6 @@ import android.content.res.Resources; public enum StatusCode { - COMMUNICATION_NOT_STARTED(R.string.err_start), NO_ERROR(R.string.err_none), LOG_SAVED(R.string.info_log_saved), LOGIN_PARSE_ERROR(R.string.err_parse), @@ -24,7 +23,6 @@ public enum StatusCode { LOG_POST_ERROR(R.string.err_log_post_failed), LOG_POST_ERROR_EC(R.string.err_log_post_failed_ec), NO_LOG_TEXT(R.string.warn_log_text_fill), - NO_DATA_FROM_SERVER(R.string.err_log_failed_server), NOT_LOGGED_IN(R.string.init_login_popup_failed), LOGIMAGE_POST_ERROR(R.string.err_logimage_post_failed); diff --git a/main/src/cgeo/geocaching/export/FieldNotes.java b/main/src/cgeo/geocaching/export/FieldNotes.java new file mode 100644 index 0000000..11d725a --- /dev/null +++ b/main/src/cgeo/geocaching/export/FieldNotes.java @@ -0,0 +1,65 @@ +package cgeo.geocaching.export; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.LogEntry; +import cgeo.geocaching.files.LocalStorage; +import cgeo.geocaching.utils.FileUtils; +import cgeo.geocaching.utils.SynchronizedDateFormat; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Field Notes are simple plain text files, but poorly documented. Syntax:<br> + * <code>GCxxxxx,yyyy-mm-ddThh:mm:ssZ,Found it,"logtext"</code> + */ +class FieldNotes { + + private static final SynchronizedDateFormat FIELD_NOTE_DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"), Locale.US); + + private int size = 0; + private final StringBuilder buffer = new StringBuilder(); + + void add(final Geocache cache, final LogEntry log) { + size++; + buffer.append(cache.getGeocode()) + .append(',') + .append(FIELD_NOTE_DATE_FORMAT.format(new Date(log.date))) + .append(',') + .append(StringUtils.capitalize(log.type.type)) + .append(",\"") + .append(StringUtils.replaceChars(log.log, '"', '\'')) + .append("\"\n"); + } + + public String getContent() { + return buffer.toString(); + } + + File writeToDirectory(File exportLocation) { + if (!LocalStorage.isExternalStorageAvailable()) { + return null; + } + + FileUtils.mkdirs(exportLocation); + + final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); + final File exportFile = new File(exportLocation.toString() + '/' + fileNameDateFormat.format(new Date()) + ".txt"); + + if (!FileUtils.writeFileUTF16(exportFile, getContent())) { + return null; + } + + return exportFile; + } + + public int size() { + return size; + } + +} diff --git a/main/src/cgeo/geocaching/export/FieldnoteExport.java b/main/src/cgeo/geocaching/export/FieldnoteExport.java index 4da480a..7d3e07e 100644 --- a/main/src/cgeo/geocaching/export/FieldnoteExport.java +++ b/main/src/cgeo/geocaching/export/FieldnoteExport.java @@ -5,21 +5,14 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.LogEntry; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.connector.gc.GCLogin; -import cgeo.geocaching.enumerations.StatusCode; -import cgeo.geocaching.network.Network; -import cgeo.geocaching.network.Parameters; +import cgeo.geocaching.connector.ConnectorFactory; +import cgeo.geocaching.connector.IConnector; +import cgeo.geocaching.connector.capability.FieldNotesCapability; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.Formatter; import cgeo.geocaching.utils.AsyncTaskWithProgress; -import cgeo.geocaching.utils.FileUtils; import cgeo.geocaching.utils.Log; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.CharEncoding; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.FastDateFormat; - import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -29,29 +22,16 @@ import android.view.ContextThemeWrapper; import android.view.View; import android.widget.CheckBox; -import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.List; -import java.util.Locale; -import java.util.TimeZone; /** - * Exports offline logs in the Groundspeak Field Note format.<br> - * <br> + * Exports offline logs in the Groundspeak Field Note format. * - * Field Notes are simple plain text files, but poorly documented. Syntax:<br> - * <code>GCxxxxx,yyyy-mm-ddThh:mm:ssZ,Found it,"logtext"</code> */ class FieldnoteExport extends AbstractExport { private static final File exportLocation = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/field-notes"); - private static final FastDateFormat fieldNoteDateFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"), Locale.US); + private static int fieldNotesCount = 0; protected FieldnoteExport() { super(getString(R.string.export_fieldnotes)); @@ -89,7 +69,7 @@ class FieldnoteExport extends AbstractExport { builder.setPositiveButton(R.string.export, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { + public void onClick(final DialogInterface dialog, final int which) { final boolean upload = uploadOption.isChecked(); final boolean onlyNew = onlyNewOption.isChecked(); Settings.setFieldNoteExportUpload(upload); @@ -121,22 +101,32 @@ class FieldnoteExport extends AbstractExport { * Upload/export only new logs since last export */ public ExportTask(final Activity activity, final boolean upload, final boolean onlyNew) { - super(activity, getProgressTitle(), getString(R.string.export_fieldnotes_creating)); + super(activity, getProgressTitle(), getString(R.string.export_fieldnotes_creating), true); this.activity = activity; this.upload = upload; this.onlyNew = onlyNew; } @Override - protected Boolean doInBackgroundInternal(Geocache[] caches) { - final StringBuilder fieldNoteBuffer = new StringBuilder(); + protected Boolean doInBackgroundInternal(final Geocache[] caches) { + // export field notes separately for each connector, so the file can be uploaded to the respective site afterwards + for (IConnector connector : ConnectorFactory.getConnectors()) { + if (connector instanceof FieldNotesCapability) { + exportFieldNotes((FieldNotesCapability) connector, caches); + } + } + return true; + } + + private boolean exportFieldNotes(final FieldNotesCapability connector, Geocache[] caches) { + final FieldNotes fieldNotes = new FieldNotes(); try { int i = 0; for (final Geocache cache : caches) { - if (cache.isLogOffline()) { + if (ConnectorFactory.getConnector(cache).equals(connector) && cache.isLogOffline()) { final LogEntry log = DataStore.loadLogOffline(cache.getGeocode()); if (!onlyNew || log.date > Settings.getFieldnoteExportDate()) { - appendFieldNote(fieldNoteBuffer, cache, log); + fieldNotes.add(cache, log); } } publishProgress(++i); @@ -145,65 +135,16 @@ class FieldnoteExport extends AbstractExport { Log.e("FieldnoteExport.ExportTask generation", e); return false; } + fieldNotesCount += fieldNotes.size(); - fieldNoteBuffer.append('\n'); - - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - return false; - } - - FileUtils.mkdirs(exportLocation); - - final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); - exportFile = new File(exportLocation.toString() + '/' + fileNameDateFormat.format(new Date()) + ".txt"); - - Writer fileWriter = null; - BufferedOutputStream buffer = null; - try { - final OutputStream os = new FileOutputStream(exportFile); - buffer = new BufferedOutputStream(os); - fileWriter = new OutputStreamWriter(buffer, CharEncoding.UTF_16); - fileWriter.write(fieldNoteBuffer.toString()); - } catch (final IOException e) { - Log.e("FieldnoteExport.ExportTask export", e); + exportFile = fieldNotes.writeToDirectory(exportLocation); + if (exportFile == null) { return false; - } finally { - IOUtils.closeQuietly(fileWriter); - IOUtils.closeQuietly(buffer); } if (upload) { publishProgress(STATUS_UPLOAD); - - if (!GCLogin.getInstance().isActualLoginStatus()) { - // no need to upload (possibly large file) if we're not logged in - final StatusCode loginState = GCLogin.getInstance().login(); - if (loginState != StatusCode.NO_ERROR) { - Log.e("FieldnoteExport.ExportTask upload: Login failed"); - } - } - - final String uri = "http://www.geocaching.com/my/uploadfieldnotes.aspx"; - final String page = GCLogin.getInstance().getRequestLogged(uri, null); - - if (StringUtils.isBlank(page)) { - Log.e("FieldnoteExport.ExportTask get page: No data from server"); - return false; - } - - final String[] viewstates = GCLogin.getViewstates(page); - - final Parameters uploadParams = new Parameters( - "__EVENTTARGET", "", - "__EVENTARGUMENT", "", - "ctl00$ContentBody$btnUpload", "Upload Field Note"); - - GCLogin.putViewstates(uploadParams, viewstates); - - Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$FieldNoteLoader", "text/plain", exportFile)); - - if (StringUtils.isBlank(page)) { - Log.e("FieldnoteExport.ExportTask upload: No data from server"); + if (!connector.uploadFieldNotes(exportFile)) { return false; } } @@ -212,7 +153,7 @@ class FieldnoteExport extends AbstractExport { } @Override - protected void onPostExecuteInternal(Boolean result) { + protected void onPostExecuteInternal(final Boolean result) { if (null != activity) { if (result) { Settings.setFieldnoteExportDate(System.currentTimeMillis()); @@ -229,25 +170,11 @@ class FieldnoteExport extends AbstractExport { } @Override - protected void onProgressUpdateInternal(int status) { + protected void onProgressUpdateInternal(final int status) { if (null != activity) { - if (STATUS_UPLOAD == status) { - setMessage(getString(R.string.export_fieldnotes_uploading)); - } else { - setMessage(getString(R.string.export_fieldnotes_creating) + " (" + status + ')'); - } + setMessage(getString(STATUS_UPLOAD == status ? R.string.export_fieldnotes_uploading : R.string.export_fieldnotes_creating) + " (" + fieldNotesCount + ')'); } } } - static void appendFieldNote(final StringBuilder fieldNoteBuffer, final Geocache cache, final LogEntry log) { - fieldNoteBuffer.append(cache.getGeocode()) - .append(',') - .append(fieldNoteDateFormat.format(new Date(log.date))) - .append(',') - .append(StringUtils.capitalize(log.type.type)) - .append(",\"") - .append(StringUtils.replaceChars(log.log, '"', '\'')) - .append("\"\n"); - } } diff --git a/main/src/cgeo/geocaching/export/GpxExport.java b/main/src/cgeo/geocaching/export/GpxExport.java index a2e0f93..08fca0b 100644 --- a/main/src/cgeo/geocaching/export/GpxExport.java +++ b/main/src/cgeo/geocaching/export/GpxExport.java @@ -1,8 +1,8 @@ package cgeo.geocaching.export; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.AsyncTaskWithProgress; diff --git a/main/src/cgeo/geocaching/export/GpxSerializer.java b/main/src/cgeo/geocaching/export/GpxSerializer.java index df07f17..962f0d3 100644 --- a/main/src/cgeo/geocaching/export/GpxSerializer.java +++ b/main/src/cgeo/geocaching/export/GpxSerializer.java @@ -8,6 +8,7 @@ import cgeo.geocaching.Waypoint; import cgeo.geocaching.enumerations.CacheAttribute; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.utils.SynchronizedDateFormat; import cgeo.geocaching.utils.TextUtils; import cgeo.geocaching.utils.XmlUtils; import cgeo.org.kxml2.io.KXmlSerializer; @@ -15,7 +16,6 @@ import cgeo.org.kxml2.io.KXmlSerializer; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.FastDateFormat; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; @@ -29,7 +29,7 @@ import java.util.Set; public final class GpxSerializer { - private static final FastDateFormat dateFormatZ = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + private static final SynchronizedDateFormat dateFormatZ = new SynchronizedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); public static final String PREFIX_XSI = "http://www.w3.org/2001/XMLSchema-instance"; public static final String PREFIX_GPX = "http://www.topografix.com/GPX/1/0"; public static final String PREFIX_GROUNDSPEAK = "http://www.groundspeak.com/cache/1/0"; diff --git a/main/src/cgeo/geocaching/files/GPXParser.java b/main/src/cgeo/geocaching/files/GPXParser.java index 157ea9d..f5380be 100644 --- a/main/src/cgeo/geocaching/files/GPXParser.java +++ b/main/src/cgeo/geocaching/files/GPXParser.java @@ -500,52 +500,9 @@ public abstract class GPXParser extends FileParser { // for GPX 1.1 from extensions node final Element cacheParent = getCacheParent(waypoint); - // GSAK extensions - for (final String gsakNamespace : GSAK_NS) { - final Element gsak = cacheParent.getChild(gsakNamespace, "wptExtension"); - gsak.getChild(gsakNamespace, "Watch").setEndTextElementListener(new EndTextElementListener() { + registerGsakExtensions(cacheParent); - @Override - public void end(String watchList) { - cache.setOnWatchlist(Boolean.valueOf(watchList.trim())); - } - }); - - gsak.getChild(gsakNamespace, "UserData").setEndTextElementListener(new UserDataListener(1)); - - for (int i = 2; i <= 4; i++) { - gsak.getChild(gsakNamespace, "User" + i).setEndTextElementListener(new UserDataListener(i)); - } - - gsak.getChild(gsakNamespace, "Parent").setEndTextElementListener(new EndTextElementListener() { - - @Override - public void end(String body) { - parentCacheCode = body; - } - }); - } - - // c:geo extensions - final Element cgeoVisited = cacheParent.getChild(CGEO_NS, "visited"); - - cgeoVisited.setEndTextElementListener(new EndTextElementListener() { - - @Override - public void end(String visited) { - wptVisited = Boolean.valueOf(visited.trim()); - } - }); - - final Element cgeoUserDefined = cacheParent.getChild(CGEO_NS, "userdefined"); - - cgeoUserDefined.setEndTextElementListener(new EndTextElementListener() { - - @Override - public void end(String userDefined) { - wptUserDefined = Boolean.valueOf(userDefined.trim()); - } - }); + registerCgeoExtensions(cacheParent); // 3 different versions of the GC schema for (final String nsGC : GROUNDSPEAK_NAMESPACE) { @@ -857,6 +814,94 @@ public abstract class GPXParser extends FileParser { } /** + * Add listeners for GSAK extensions + * + * @param cacheParent + */ + private void registerGsakExtensions(final Element cacheParent) { + for (final String gsakNamespace : GSAK_NS) { + final Element gsak = cacheParent.getChild(gsakNamespace, "wptExtension"); + gsak.getChild(gsakNamespace, "Watch").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String watchList) { + cache.setOnWatchlist(Boolean.valueOf(watchList.trim())); + } + }); + + gsak.getChild(gsakNamespace, "UserData").setEndTextElementListener(new UserDataListener(1)); + + for (int i = 2; i <= 4; i++) { + gsak.getChild(gsakNamespace, "User" + i).setEndTextElementListener(new UserDataListener(i)); + } + + gsak.getChild(gsakNamespace, "Parent").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + parentCacheCode = body; + } + }); + + gsak.getChild(gsakNamespace, "FavPoints").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String favoritePoints) { + try { + cache.setFavoritePoints(Integer.parseInt(favoritePoints)); + } + catch (final NumberFormatException e) { + Log.w("Failed to parse favorite points", e); + } + } + }); + + gsak.getChild(gsakNamespace, "GcNote").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(final String personalNote) { + cache.setPersonalNote(StringUtils.trim(personalNote)); + } + }); + + gsak.getChild(gsakNamespace, "IsPremium").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(final String premium) { + cache.setPremiumMembersOnly(Boolean.parseBoolean(premium)); + } + }); + } + } + + /** + * Add listeners for c:geo extensions + * + * @param cacheParent + */ + private void registerCgeoExtensions(final Element cacheParent) { + final Element cgeoVisited = cacheParent.getChild(CGEO_NS, "visited"); + + cgeoVisited.setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String visited) { + wptVisited = Boolean.valueOf(visited.trim()); + } + }); + + final Element cgeoUserDefined = cacheParent.getChild(CGEO_NS, "userdefined"); + + cgeoUserDefined.setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String userDefined) { + wptUserDefined = Boolean.valueOf(userDefined.trim()); + } + }); + } + + /** * Overwrite this method in a GPX parser sub class to modify the {@link Geocache}, after it has been fully parsed * from the GPX file and before it gets stored. * diff --git a/main/src/cgeo/geocaching/files/LocalStorage.java b/main/src/cgeo/geocaching/files/LocalStorage.java index d57c247..626f6e6 100644 --- a/main/src/cgeo/geocaching/files/LocalStorage.java +++ b/main/src/cgeo/geocaching/files/LocalStorage.java @@ -7,7 +7,6 @@ import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.Nullable; @@ -122,7 +121,7 @@ public final class LocalStorage { * the geocode * @return the cache directory */ - public static File getStorageDir(final String geocode) { + public static File getStorageDir(@Nullable final String geocode) { return storageDir(getStorage(), geocode); } @@ -134,11 +133,11 @@ public final class LocalStorage { * the geocode * @return the cache directory */ - private static File getStorageSecDir(final String geocode) { + private static File getStorageSecDir(@Nullable final String geocode) { return storageDir(getStorageSec(), geocode); } - private static File storageDir(final File base, final String geocode) { + private static File storageDir(final File base, @Nullable final String geocode) { return new File(base, StringUtils.defaultIfEmpty(geocode, "_others")); } @@ -155,7 +154,7 @@ public final class LocalStorage { * true if an url was given, false if a file name was given * @return the file */ - public static File getStorageFile(final String geocode, final String fileNameOrUrl, final boolean isUrl, final boolean createDirs) { + public static File getStorageFile(@Nullable final String geocode, final String fileNameOrUrl, final boolean isUrl, final boolean createDirs) { return buildFile(getStorageDir(geocode), fileNameOrUrl, isUrl, createDirs); } @@ -276,15 +275,18 @@ public final class LocalStorage { return false; } + try { try { - final FileOutputStream fos = new FileOutputStream(targetFile); + final File tempFile = File.createTempFile("download", null, targetFile.getParentFile()); + final FileOutputStream fos = new FileOutputStream(tempFile); final boolean written = copy(inputStream, fos); fos.close(); - if (!written) { - FileUtils.deleteIgnoringFailure(targetFile); + if (written) { + return tempFile.renameTo(targetFile); } - return written; + FileUtils.deleteIgnoringFailure(tempFile); + return false; } finally { IOUtils.closeQuietly(inputStream); } diff --git a/main/src/cgeo/geocaching/files/SimpleDirChooser.java b/main/src/cgeo/geocaching/files/SimpleDirChooser.java index 67cbb34..1e1296a 100644 --- a/main/src/cgeo/geocaching/files/SimpleDirChooser.java +++ b/main/src/cgeo/geocaching/files/SimpleDirChooser.java @@ -88,9 +88,9 @@ public class SimpleDirChooser extends AbstractListActivity { } public void editPath() { - AlertDialog.Builder builder = new AlertDialog.Builder(SimpleDirChooser.this); + AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.simple_dir_chooser_current_path); - final EditText input = new EditText(SimpleDirChooser.this); + final EditText input = new EditText(this); input.setInputType(InputType.TYPE_CLASS_TEXT); input.setText(currentDir.getPath()); builder.setView(input); diff --git a/main/src/cgeo/geocaching/filter/DistanceFilter.java b/main/src/cgeo/geocaching/filter/DistanceFilter.java index 54225d2..cddcf72 100644 --- a/main/src/cgeo/geocaching/filter/DistanceFilter.java +++ b/main/src/cgeo/geocaching/filter/DistanceFilter.java @@ -1,9 +1,9 @@ package cgeo.geocaching.filter; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.IGeoData; import cgeo.geocaching.R; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.geopoint.Geopoint; import java.util.ArrayList; diff --git a/main/src/cgeo/geocaching/filter/FilterUserInterface.java b/main/src/cgeo/geocaching/filter/FilterUserInterface.java index d77341b..b6eaa78 100644 --- a/main/src/cgeo/geocaching/filter/FilterUserInterface.java +++ b/main/src/cgeo/geocaching/filter/FilterUserInterface.java @@ -5,7 +5,8 @@ import cgeo.geocaching.R; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; + +import rx.functions.Action1; import android.app.Activity; import android.app.AlertDialog; @@ -77,7 +78,7 @@ public final class FilterUserInterface { registry.add(new FactoryEntry(res.getString(resourceId), factoryClass)); } - public void selectFilter(final RunnableWithArgument<IFilter> runAfterwards) { + public void selectFilter(final Action1<IFilter> runAfterwards) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.caches_filter_title); @@ -89,7 +90,7 @@ public final class FilterUserInterface { FactoryEntry entry = adapter.getItem(itemIndex); // reset? if (entry.filterFactory == null) { - runAfterwards.run(null); + runAfterwards.call(null); } else { try { @@ -105,10 +106,10 @@ public final class FilterUserInterface { builder.create().show(); } - private void selectFromFactory(final IFilterFactory factory, final String menuTitle, final RunnableWithArgument<IFilter> runAfterwards) { + private void selectFromFactory(final IFilterFactory factory, final String menuTitle, final Action1<IFilter> runAfterwards) { final List<IFilter> filters = Collections.unmodifiableList(factory.getFilters()); if (filters.size() == 1) { - runAfterwards.run(filters.get(0)); + runAfterwards.call(filters.get(0)); return; } @@ -119,7 +120,7 @@ public final class FilterUserInterface { builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int item) { - runAfterwards.run(filters.get(item)); + runAfterwards.call(filters.get(item)); } }); diff --git a/main/src/cgeo/geocaching/filter/ModifiedFilter.java b/main/src/cgeo/geocaching/filter/ModifiedFilter.java index d976b69..2ac088a 100644 --- a/main/src/cgeo/geocaching/filter/ModifiedFilter.java +++ b/main/src/cgeo/geocaching/filter/ModifiedFilter.java @@ -1,8 +1,8 @@ package cgeo.geocaching.filter; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; -import cgeo.geocaching.CgeoApplication; import java.util.Collections; import java.util.List; diff --git a/main/src/cgeo/geocaching/geopoint/Geopoint.java b/main/src/cgeo/geocaching/geopoint/Geopoint.java index 3d69bd4..8fe75ef 100644 --- a/main/src/cgeo/geocaching/geopoint/Geopoint.java +++ b/main/src/cgeo/geocaching/geopoint/Geopoint.java @@ -4,6 +4,7 @@ import cgeo.geocaching.ICoordinates; import cgeo.geocaching.R; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import android.location.Location; import android.os.Build; @@ -11,10 +12,13 @@ import android.os.Parcel; import android.os.Parcelable; /** - * Abstraction of geographic point. + * Abstraction of geographic point. This class is immutable. */ public final class Geopoint implements ICoordinates, Parcelable { - public static final Geopoint ZERO = new Geopoint(0.0, 0.0); + /** + * Reusable default object + */ + public static final @NonNull Geopoint ZERO = new Geopoint(0.0, 0.0); private static final double DEG_TO_RAD = Math.PI / 180; private static final double RAD_TO_DEG = 180 / Math.PI; @@ -64,7 +68,6 @@ public final class Geopoint implements ICoordinates, Parcelable { * longitude string to parse * @throws Geopoint.ParseException * if any argument string cannot be parsed - * @see GeopointParser#parse(String, String) */ public Geopoint(final String latText, final String lonText) { this(GeopointParser.parseLatitude(latText), GeopointParser.parseLongitude(lonText)); @@ -273,19 +276,6 @@ public final class Geopoint implements ICoordinates, Parcelable { } /** - * Checks if given Geopoint is similar to this Geopoint with tolerance. - * - * @param gp - * Geopoint to check - * @param tolerance - * tolerance in km - * @return true if similar, false otherwise - */ - public boolean isEqualTo(Geopoint gp, double tolerance) { - return null != gp && distanceTo(gp) <= tolerance; - } - - /** * Returns formatted coordinates. * * @param format @@ -300,7 +290,7 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Returns formatted coordinates with default format. * Default format is decimalminutes, e.g. N 52° 36.123 E 010° 03.456 - * + * * @return formatted coordinates */ @Override @@ -365,7 +355,7 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get longitude character (E or W). - * + * * @return */ public char getLonDir() { diff --git a/main/src/cgeo/geocaching/geopoint/GeopointParser.java b/main/src/cgeo/geocaching/geopoint/GeopointParser.java index e59cd23..2809598 100644 --- a/main/src/cgeo/geocaching/geopoint/GeopointParser.java +++ b/main/src/cgeo/geocaching/geopoint/GeopointParser.java @@ -76,37 +76,11 @@ class GeopointParser { } /** - * Parses a pair of coordinates (latitude and longitude) out of a String. - * Accepts following formats and combinations of it: - * X DD - * X DD° - * X DD° MM - * X DD° MM.MMM - * X DD° MM SS - * - * as well as: - * DD.DDDDDDD - * - * Both . and , are accepted, also variable count of spaces (also 0) - * - * @param latitude - * the latitude string to parse - * @param longitude - * the longitude string to parse - * @return an Geopoint with parsed latitude and longitude - * @throws Geopoint.ParseException - * if lat or lon could not be parsed - */ - public static Geopoint parse(final String latitude, final String longitude) { - final double lat = parseLatitude(latitude); - final double lon = parseLongitude(longitude); - - return new Geopoint(lat, lon); - } - - /* - * (non JavaDoc) - * Helper for coordinates-parsing. + * Helper for coordinates-parsing + * + * @param text + * @param latlon + * @return */ private static ResultWrapper parseHelper(final String text, final LatLon latlon) { diff --git a/main/src/cgeo/geocaching/geopoint/Viewport.java b/main/src/cgeo/geocaching/geopoint/Viewport.java index 9d55f69..ba0e040 100644 --- a/main/src/cgeo/geocaching/geopoint/Viewport.java +++ b/main/src/cgeo/geocaching/geopoint/Viewport.java @@ -5,16 +5,15 @@ import cgeo.geocaching.ICoordinates; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import java.util.Set; +import java.util.Collection; - -public class Viewport { +public final class Viewport { public final @NonNull Geopoint center; public final @NonNull Geopoint bottomLeft; public final @NonNull Geopoint topRight; - public Viewport(final ICoordinates point1, final ICoordinates point2) { + public Viewport(final @NonNull ICoordinates point1, final @NonNull ICoordinates point2) { final Geopoint gp1 = point1.getCoords(); final Geopoint gp2 = point2.getCoords(); this.bottomLeft = new Geopoint(Math.min(gp1.getLatitude(), gp2.getLatitude()), @@ -25,7 +24,7 @@ public class Viewport { (gp1.getLongitude() + gp2.getLongitude()) / 2); } - public Viewport(final ICoordinates center, final double latSpan, final double lonSpan) { + public Viewport(final @NonNull ICoordinates center, final double latSpan, final double lonSpan) { this.center = center.getCoords(); final double centerLat = this.center.getLatitude(); final double centerLon = this.center.getLongitude(); @@ -71,7 +70,7 @@ public class Viewport { * the coordinates to check * @return true if the point is contained in this viewport, false otherwise or if the point contains no coordinates */ - public boolean contains(final ICoordinates point) { + public boolean contains(final @NonNull ICoordinates point) { final Geopoint coords = point.getCoords(); return coords != null && coords.getLongitudeE6() >= bottomLeft.getLongitudeE6() @@ -87,12 +86,12 @@ public class Viewport { /** * Check whether another viewport is fully included into the current one. - * + * * @param vp * the other viewport - * @return true if the vp is fully included into this one, false otherwise + * @return true if the viewport is fully included into this one, false otherwise */ - public boolean includes(final Viewport vp) { + public boolean includes(final @NonNull Viewport vp) { return contains(vp.bottomLeft) && contains(vp.topRight); } @@ -124,46 +123,37 @@ public class Viewport { } /** - * Return a viewport that contains the current viewport as well as another point. - * - * @param point - * the point we want in the viewport - * @return either the same or an expanded viewport - */ - public Viewport expand(final ICoordinates point) { - if (contains(point)) { - return this; - } - - final Geopoint coords = point.getCoords(); - final double latitude = coords.getLatitude(); - final double longitude = coords.getLongitude(); - final double latMin = Math.min(getLatitudeMin(), latitude); - final double latMax = Math.max(getLatitudeMax(), latitude); - final double lonMin = Math.min(getLongitudeMin(), longitude); - final double lonMax = Math.max(getLongitudeMax(), longitude); - return new Viewport(new Geopoint(latMin, lonMin), new Geopoint(latMax, lonMax)); - } - - /** * Return the smallest viewport containing all the given points. * * @param points * a set of points. Point with null coordinates (or null themselves) will be ignored * @return the smallest viewport containing the non-null coordinates, or null if no coordinates are non-null */ - static public Viewport containing(final Set<? extends ICoordinates> points) { - Viewport viewport = null; + static public @Nullable + Viewport containing(final Collection<? extends ICoordinates> points) { + boolean valid = false; + double latMin = Double.MAX_VALUE; + double latMax = -Double.MAX_VALUE; + double lonMin = Double.MAX_VALUE; + double lonMax = -Double.MAX_VALUE; for (final ICoordinates point : points) { - if (point != null && point.getCoords() != null) { - if (viewport == null) { - viewport = new Viewport(point, point); - } else { - viewport = viewport.expand(point); + if (point != null) { + final Geopoint coords = point.getCoords(); + if (coords != null) { + valid = true; + final double latitude = coords.getLatitude(); + final double longitude = coords.getLongitude(); + latMin = Math.min(latMin, latitude); + latMax = Math.max(latMax, latitude); + lonMin = Math.min(lonMin, longitude); + lonMax = Math.max(lonMax, longitude); } } } - return viewport; + if (!valid) { + return null; + } + return new Viewport(new Geopoint(latMin, lonMin), new Geopoint(latMax, lonMax)); } @Override diff --git a/main/src/cgeo/geocaching/list/PseudoList.java b/main/src/cgeo/geocaching/list/PseudoList.java index 365d6fd..f2cc7ed 100644 --- a/main/src/cgeo/geocaching/list/PseudoList.java +++ b/main/src/cgeo/geocaching/list/PseudoList.java @@ -17,6 +17,12 @@ public class PseudoList extends AbstractList { */ public static final AbstractList NEW_LIST = new PseudoList(NEW_LIST_ID, R.string.list_menu_create); + private static final int HISTORY_LIST_ID = 4; + /** + * list entry to create a new list + */ + public static final AbstractList HISTORY_LIST = new PseudoList(HISTORY_LIST_ID, R.string.menu_history); + /** * private constructor to have all instances as constants in the class */ diff --git a/main/src/cgeo/geocaching/list/StoredList.java b/main/src/cgeo/geocaching/list/StoredList.java index 8106073..7e2a83c 100644 --- a/main/src/cgeo/geocaching/list/StoredList.java +++ b/main/src/cgeo/geocaching/list/StoredList.java @@ -5,11 +5,12 @@ import cgeo.geocaching.DataStore; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.ui.dialog.Dialogs; -import cgeo.geocaching.utils.RunnableWithArgument; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Action1; + import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; @@ -67,15 +68,15 @@ public final class StoredList extends AbstractList { res = app.getResources(); } - public void promptForListSelection(final int titleId, @NonNull final RunnableWithArgument<Integer> runAfterwards) { + public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards) { promptForListSelection(titleId, runAfterwards, false, -1); } - public void promptForListSelection(final int titleId, @NonNull final RunnableWithArgument<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId) { + public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId) { promptForListSelection(titleId, runAfterwards, onlyConcreteLists, exceptListId, StringUtils.EMPTY); } - public void promptForListSelection(final int titleId, @NonNull final RunnableWithArgument<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId, final String newListName) { + public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId, final String newListName) { final List<AbstractList> lists = new ArrayList<AbstractList>(); lists.addAll(getSortedLists()); @@ -87,7 +88,12 @@ public final class StoredList extends AbstractList { } if (!onlyConcreteLists) { - lists.add(PseudoList.ALL_LIST); + if (exceptListId != PseudoList.ALL_LIST.id) { + lists.add(PseudoList.ALL_LIST); + } + if (exceptListId != PseudoList.HISTORY_LIST.id) { + lists.add(PseudoList.HISTORY_LIST); + } } lists.add(PseudoList.NEW_LIST); @@ -109,7 +115,7 @@ public final class StoredList extends AbstractList { promptForListCreation(runAfterwards, newListName); } else { - runAfterwards.run(lists.get(itemId).id); + runAfterwards.call(lists.get(itemId).id); } } }); @@ -138,17 +144,17 @@ public final class StoredList extends AbstractList { return lists; } - public void promptForListCreation(@NonNull final RunnableWithArgument<Integer> runAfterwards, String newListName) { - handleListNameInput(newListName, R.string.list_dialog_create_title, R.string.list_dialog_create, new RunnableWithArgument<String>() { + public void promptForListCreation(@NonNull final Action1<Integer> runAfterwards, String newListName) { + handleListNameInput(newListName, R.string.list_dialog_create_title, R.string.list_dialog_create, new Action1<String>() { @Override - public void run(final String listName) { + public void call(final String listName) { final int newId = DataStore.createList(listName); new StoredList(newId, listName, 0); if (newId >= DataStore.customListIdOffset) { ActivityMixin.showToast(activity, res.getString(R.string.list_dialog_create_ok)); - runAfterwards.run(newId); + runAfterwards.call(newId); } else { ActivityMixin.showToast(activity, res.getString(R.string.list_dialog_create_err)); } @@ -156,15 +162,15 @@ public final class StoredList extends AbstractList { }); } - private void handleListNameInput(final String defaultValue, int dialogTitle, int buttonTitle, final RunnableWithArgument<String> runnable) { - Dialogs.input(activity, dialogTitle, defaultValue, buttonTitle, new RunnableWithArgument<String>() { + private void handleListNameInput(final String defaultValue, int dialogTitle, int buttonTitle, final Action1<String> runnable) { + Dialogs.input(activity, dialogTitle, defaultValue, buttonTitle, new Action1<String>() { @Override - public void run(final String input) { + public void call(final String input) { // remove whitespaces added by autocompletion of Android keyboard String listName = StringUtils.trim(input); if (StringUtils.isNotBlank(listName)) { - runnable.run(listName); + runnable.call(listName); } } }); @@ -172,10 +178,10 @@ public final class StoredList extends AbstractList { public void promptForListRename(final int listId, @NonNull final Runnable runAfterRename) { final StoredList list = DataStore.getList(listId); - handleListNameInput(list.title, R.string.list_dialog_rename_title, R.string.list_dialog_rename, new RunnableWithArgument<String>() { + handleListNameInput(list.title, R.string.list_dialog_rename_title, R.string.list_dialog_rename, new Action1<String>() { @Override - public void run(final String listName) { + public void call(final String listName) { DataStore.renameList(listId, listName); runAfterRename.run(); } @@ -197,7 +203,7 @@ public final class StoredList extends AbstractList { * Return the given list, if it is a concrete list. Return the default list otherwise. */ public static int getConcreteList(int listId) { - if (listId == PseudoList.ALL_LIST.id || listId == TEMPORARY_LIST_ID) { + if (listId == PseudoList.ALL_LIST.id || listId == TEMPORARY_LIST_ID || listId == PseudoList.HISTORY_LIST.id) { return STANDARD_LIST_ID; } return listId; diff --git a/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java b/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java index 7524b76..b2cb0b2 100644 --- a/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java +++ b/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java @@ -2,6 +2,7 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.gc.GCConstants; +import cgeo.geocaching.connector.gc.RecaptchaHandler; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.utils.Log; @@ -13,6 +14,8 @@ import android.content.Context; import android.os.Handler; import android.support.v4.content.AsyncTaskLoader; +import java.util.concurrent.CountDownLatch; + public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> implements RecaptchaReceiver { public enum CacheListLoaderType { @@ -28,6 +31,10 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> MAP, REMOVE_FROM_HISTORY, NEXT_PAGE; + + public int getLoaderId() { + return ordinal(); + } } private Handler recaptchaHandler = null; @@ -36,6 +43,7 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> private String recaptchaText = null; private SearchResult search; private boolean loading; + private CountDownLatch latch = new CountDownLatch(1); public AbstractSearchLoader(Context context) { super(context); @@ -72,23 +80,21 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> forceLoad(); } - public void setRecaptchaHandler(Handler recaptchaHandlerIn) { - recaptchaHandler = recaptchaHandlerIn; + public void setRecaptchaHandler(final Handler recaptchaHandler) { + this.recaptchaHandler = recaptchaHandler; } @Override public void notifyNeed() { if (recaptchaHandler != null) { - recaptchaHandler.sendEmptyMessage(1); + recaptchaHandler.sendEmptyMessage(RecaptchaHandler.SHOW_CAPTCHA); } } @Override - public synchronized void waitForUser() { + public void waitForUser() { try { - while (getText() == null) { - wait(); - } + latch.await(); } catch (InterruptedException e) { Log.w("searchThread is not waiting for user…"); } @@ -100,16 +106,11 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> } @Override - public String getKey() { - return recaptchaKey; - } - - @Override public void fetchChallenge() { recaptchaChallenge = null; if (StringUtils.isNotEmpty(recaptchaKey)) { - final Parameters params = new Parameters("k", getKey()); + final Parameters params = new Parameters("k", recaptchaKey); final String recaptchaJs = Network.getResponseData(Network.getRequest("http://www.google.com/recaptcha/api/challenge", params)); if (StringUtils.isNotBlank(recaptchaJs)) { @@ -124,18 +125,16 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> } @Override - public synchronized void setText(String text) { + public void setText(String text) { recaptchaText = text; - - notify(); + latch.countDown(); } @Override - public synchronized String getText() { + public String getText() { return recaptchaText; } - @Override public void reset() { super.reset(); diff --git a/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java index dd7c7a6..e1573c9 100644 --- a/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java @@ -1,8 +1,8 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.SearchResult; -import cgeo.geocaching.settings.Settings; import cgeo.geocaching.connector.gc.GCParser; +import cgeo.geocaching.settings.Settings; import android.content.Context; diff --git a/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java index 3874b47..c2d92bf 100644 --- a/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java @@ -6,6 +6,7 @@ import cgeo.geocaching.connector.capability.ISearchByCenter; import cgeo.geocaching.geopoint.Geopoint; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Func1; import android.content.Context; @@ -19,16 +20,13 @@ public class CoordsGeocacheListLoader extends AbstractSearchLoader { @Override public SearchResult runSearch() { - - SearchResult search = new SearchResult(); - - for (ISearchByCenter centerConn : ConnectorFactory.getSearchByCenterConnectors()) { - if (centerConn.isActive()) { - search.addSearchResult(centerConn.searchByCenter(coords, this)); - } - } - - return search; + return SearchResult.parallelCombineActive(ConnectorFactory.getSearchByCenterConnectors(), + new Func1<ISearchByCenter, SearchResult>() { + @Override + public SearchResult call(final ISearchByCenter connector) { + return connector.searchByCenter(coords, CoordsGeocacheListLoader.this); + } + }); } } diff --git a/main/src/cgeo/geocaching/loaders/FinderGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/FinderGeocacheListLoader.java index 7edd436..378fa1b 100644 --- a/main/src/cgeo/geocaching/loaders/FinderGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/FinderGeocacheListLoader.java @@ -5,6 +5,7 @@ import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ISearchByFinder; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Func1; import android.content.Context; @@ -19,15 +20,13 @@ public class FinderGeocacheListLoader extends AbstractSearchLoader { @Override public SearchResult runSearch() { - SearchResult searchResult = new SearchResult(); - - for (ISearchByFinder connector : ConnectorFactory.getSearchByFinderConnectors()) { - if (connector.isActive()) { - searchResult.addSearchResult(connector.searchByFinder(username, this)); - } - } - - return searchResult; + return SearchResult.parallelCombineActive(ConnectorFactory.getSearchByFinderConnectors(), + new Func1<ISearchByFinder, SearchResult>() { + @Override + public SearchResult call(final ISearchByFinder connector) { + return connector.searchByFinder(username, FinderGeocacheListLoader.this); + } + }); } } diff --git a/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java index 605f461..fdb35f2 100644 --- a/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java @@ -2,9 +2,9 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.DataStore; import cgeo.geocaching.SearchResult; -import cgeo.geocaching.settings.Settings; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.settings.Settings; import android.content.Context; diff --git a/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java index 9c16ee4..45b264f 100644 --- a/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java @@ -5,6 +5,7 @@ import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ISearchByKeyword; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Func1; import android.content.Context; @@ -19,15 +20,13 @@ public class KeywordGeocacheListLoader extends AbstractSearchLoader { @Override public SearchResult runSearch() { - SearchResult searchResult = new SearchResult(); - - for (ISearchByKeyword connector : ConnectorFactory.getSearchByKeywordConnectors()) { - if (connector.isActive()) { - searchResult.addSearchResult(connector.searchByKeyword(keyword, this)); - } - } - - return searchResult; + return SearchResult.parallelCombineActive(ConnectorFactory.getSearchByKeywordConnectors(), + new Func1<ISearchByKeyword, SearchResult>() { + @Override + public SearchResult call(final ISearchByKeyword connector) { + return connector.searchByKeyword(keyword, KeywordGeocacheListLoader.this); + } + }); } } diff --git a/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java index 1104f83..05eac18 100644 --- a/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java @@ -1,8 +1,8 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.SearchResult; -import cgeo.geocaching.settings.Settings; import cgeo.geocaching.connector.gc.GCParser; +import cgeo.geocaching.settings.Settings; import android.content.Context; diff --git a/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java index 5088484..b80a1b8 100644 --- a/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java @@ -1,9 +1,9 @@ package cgeo.geocaching.loaders; -import cgeo.geocaching.SearchResult; -import cgeo.geocaching.settings.Settings; import cgeo.geocaching.DataStore; +import cgeo.geocaching.SearchResult; import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.settings.Settings; import android.content.Context; diff --git a/main/src/cgeo/geocaching/loaders/OwnerGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/OwnerGeocacheListLoader.java index 4d530fb..70db74c 100644 --- a/main/src/cgeo/geocaching/loaders/OwnerGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/OwnerGeocacheListLoader.java @@ -5,6 +5,7 @@ import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ISearchByOwner; import org.eclipse.jdt.annotation.NonNull; +import rx.functions.Func1; import android.content.Context; @@ -19,15 +20,13 @@ public class OwnerGeocacheListLoader extends AbstractSearchLoader { @Override public SearchResult runSearch() { - SearchResult searchResult = new SearchResult(); - - for (ISearchByOwner connector : ConnectorFactory.getSearchByOwnerConnectors()) { - if (connector.isActive()) { - searchResult.addSearchResult(connector.searchByOwner(username, this)); + return SearchResult.parallelCombineActive(ConnectorFactory.getSearchByOwnerConnectors(), + new Func1<ISearchByOwner, SearchResult>() { + @Override + public SearchResult call(final ISearchByOwner connector) { + return connector.searchByOwner(username, OwnerGeocacheListLoader.this); } - } - - return searchResult; + }); } } diff --git a/main/src/cgeo/geocaching/loaders/RecaptchaReceiver.java b/main/src/cgeo/geocaching/loaders/RecaptchaReceiver.java index fd5189c..881e048 100644 --- a/main/src/cgeo/geocaching/loaders/RecaptchaReceiver.java +++ b/main/src/cgeo/geocaching/loaders/RecaptchaReceiver.java @@ -10,8 +10,6 @@ public interface RecaptchaReceiver { public void fetchChallenge(); - public String getKey(); - public void setKey(String key); public void notifyNeed(); diff --git a/main/src/cgeo/geocaching/maps/CGeoMap.java b/main/src/cgeo/geocaching/maps/CGeoMap.java index c98ba72..6730ba9 100644 --- a/main/src/cgeo/geocaching/maps/CGeoMap.java +++ b/main/src/cgeo/geocaching/maps/CGeoMap.java @@ -38,12 +38,13 @@ import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.GeoDirHandler; import cgeo.geocaching.utils.LeastRecentlyUsedSet; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.HashCodeBuilder; +import rx.functions.Action1; + import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; @@ -289,8 +290,6 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto waitDialog.dismiss(); waitDialog.setOnCancelListener(null); } - - geoDirUpdate.startDir(); } } @@ -299,8 +298,6 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto if (loadDetailsThread != null) { loadDetailsThread.stopIt(); } - - geoDirUpdate.startDir(); } } @@ -466,7 +463,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto prepareFilterBar(); - if (!app.isLiveMapHintShown() && !Settings.getHideLiveMapHint()) { + if (!app.isLiveMapHintShownInThisSession() && !Settings.getHideLiveMapHint() && Settings.getLiveMapHintShowCount() <= 3) { LiveMapInfoDialogBuilder.create(activity).show(); } } @@ -495,7 +492,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto public void onResume() { super.onResume(); - addGeoDirObservers(); + geoDirUpdate.startGeoAndDir(); if (!CollectionUtils.isEmpty(dirtyCaches)) { for (String geocode : dirtyCaches) { @@ -515,18 +512,10 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto startTimer(); } - private void addGeoDirObservers() { - geoDirUpdate.startGeoAndDir(); - } - - private void deleteGeoDirObservers() { - geoDirUpdate.stopGeoAndDir(); - } - @Override public void onPause() { stopTimer(); - deleteGeoDirObservers(); + geoDirUpdate.stopGeoAndDir(); savePrefs(); if (mapView != null) { @@ -661,9 +650,9 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto if (Settings.getChooseList()) { // let user select list to store cache in new StoredList.UserInterface(activity).promptForListSelection(R.string.list_title, - new RunnableWithArgument<Integer>() { + new Action1<Integer>() { @Override - public void run(final Integer selectedListId) { + public void call(final Integer selectedListId) { storeCaches(geocodes, selectedListId); } }, true, StoredList.TEMPORARY_LIST_ID); @@ -800,7 +789,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto * @return true if a restart is needed, false otherwise */ private boolean changeMapSource(final MapSource mapSource) { - final boolean restartRequired = !MapProviderFactory.isSameActivity(Settings.getMapSource(), mapSource); + final boolean restartRequired = !MapProviderFactory.isSameActivity(MapProviderFactory.getMapSource(currentSourceId), mapSource); Settings.setMapSource(mapSource); currentSourceId = mapSource.getNumericalId(); @@ -898,7 +887,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto private long timeLastPositionOverlayCalculation = 0; @Override - protected void updateGeoData(final IGeoData geo) { + public void updateGeoData(final IGeoData geo) { if (geo.isPseudoLocation()) { locationValid = false; } else { @@ -994,7 +983,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto public synchronized void startTimer() { if (coordsIntent != null) { // display just one point - (new DisplayPointThread()).start(); + displayPoint(coordsIntent); } else { // start timer stopTimer(); @@ -1063,7 +1052,6 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto } } - yield(); } catch (Exception e) { Log.w("CGeoMap.LoadTimer.run", e); } @@ -1182,7 +1170,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto } } } - final SearchResult searchResult = ConnectorFactory.searchByViewport(viewport.resize(0.8), tokens); + final SearchResult searchResult = ConnectorFactory.searchByViewport(viewport.resize(0.8), tokens).toBlockingObservable().single(); downloaded = true; Set<Geocache> result = searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB); @@ -1271,32 +1259,16 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto } } - /** - * Thread to display one point. Started on opening if in single mode. - */ - private class DisplayPointThread extends Thread { - - @Override - public void run() { - if (mapView == null || caches == null) { - return; - } - - if (coordsIntent != null) { - final Waypoint waypoint = new Waypoint("some place", waypointTypeIntent != null ? waypointTypeIntent : WaypointType.WAYPOINT, false); - waypoint.setCoords(coordsIntent); + private void displayPoint(final Geopoint coords) { + final Waypoint waypoint = new Waypoint("some place", waypointTypeIntent != null ? waypointTypeIntent : WaypointType.WAYPOINT, false); + waypoint.setCoords(coords); - final CachesOverlayItemImpl item = getWaypointItem(waypoint); - overlayCaches.updateItems(item); - displayHandler.sendEmptyMessage(INVALIDATE_MAP); + final CachesOverlayItemImpl item = getWaypointItem(waypoint); + overlayCaches.updateItems(item); + displayHandler.sendEmptyMessage(INVALIDATE_MAP); + displayHandler.sendEmptyMessage(UPDATE_TITLE); - cachesCnt = 1; - } else { - cachesCnt = 0; - } - - displayHandler.sendEmptyMessage(UPDATE_TITLE); - } + cachesCnt = 1; } private static abstract class DoRunnable implements Runnable { @@ -1343,8 +1315,6 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto if (loadDetailsThread != null) { loadDetailsThread.stopIt(); } - - geoDirUpdate.startDir(); } catch (Exception e) { Log.e("CGeoMap.storeCaches.onCancel", e); } @@ -1392,8 +1362,6 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto return; } - deleteGeoDirObservers(); - for (final String geocode : geocodes) { try { if (handler.isCancelled()) { @@ -1410,14 +1378,10 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto detailProgress++; handler.sendEmptyMessage(UPDATE_PROGRESS); } - - // FIXME: what does this yield() do here? - yield(); } // we're done handler.sendEmptyMessage(FINISHED_LOADING_DETAILS); - addGeoDirObservers(); } } @@ -1609,7 +1573,7 @@ public class CGeoMap extends AbstractMap implements OnMapDragListener, ViewFacto } private CachesOverlayItemImpl getCacheItem(final Geocache cache) { - final CachesOverlayItemImpl item = mapItemFactory.getCachesOverlayItem(cache, cache.getType().applyDistanceRule()); + final CachesOverlayItemImpl item = mapItemFactory.getCachesOverlayItem(cache, cache.applyDistanceRule()); final int hashcode = new HashCodeBuilder() .append(cache.isReliableLatLon()) diff --git a/main/src/cgeo/geocaching/maps/MapProviderFactory.java b/main/src/cgeo/geocaching/maps/MapProviderFactory.java index 2e43e19..b928a1e 100644 --- a/main/src/cgeo/geocaching/maps/MapProviderFactory.java +++ b/main/src/cgeo/geocaching/maps/MapProviderFactory.java @@ -1,7 +1,7 @@ package cgeo.geocaching.maps; -import cgeo.geocaching.R; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.R; import cgeo.geocaching.maps.google.GoogleMapProvider; import cgeo.geocaching.maps.interfaces.MapProvider; import cgeo.geocaching.maps.interfaces.MapSource; diff --git a/main/src/cgeo/geocaching/maps/google/GoogleCacheOverlay.java b/main/src/cgeo/geocaching/maps/google/GoogleCacheOverlay.java index 3339650..d14c687 100644 --- a/main/src/cgeo/geocaching/maps/google/GoogleCacheOverlay.java +++ b/main/src/cgeo/geocaching/maps/google/GoogleCacheOverlay.java @@ -77,11 +77,6 @@ public class GoogleCacheOverlay extends ItemizedOverlay<GoogleCacheOverlayItem> } @Override - public Drawable superBoundCenter(Drawable markerIn) { - return ItemizedOverlay.boundCenter(markerIn); - } - - @Override public Drawable superBoundCenterBottom(Drawable marker) { return ItemizedOverlay.boundCenterBottom(marker); } diff --git a/main/src/cgeo/geocaching/maps/google/GoogleMapProvider.java b/main/src/cgeo/geocaching/maps/google/GoogleMapProvider.java index cb95b2c..38d7d96 100644 --- a/main/src/cgeo/geocaching/maps/google/GoogleMapProvider.java +++ b/main/src/cgeo/geocaching/maps/google/GoogleMapProvider.java @@ -1,7 +1,7 @@ package cgeo.geocaching.maps.google; -import cgeo.geocaching.R; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.R; import cgeo.geocaching.maps.AbstractMapProvider; import cgeo.geocaching.maps.AbstractMapSource; import cgeo.geocaching.maps.interfaces.MapItemFactory; diff --git a/main/src/cgeo/geocaching/maps/google/GoogleMapView.java b/main/src/cgeo/geocaching/maps/google/GoogleMapView.java index d02e3c2..610dbe1 100644 --- a/main/src/cgeo/geocaching/maps/google/GoogleMapView.java +++ b/main/src/cgeo/geocaching/maps/google/GoogleMapView.java @@ -11,15 +11,14 @@ import cgeo.geocaching.maps.interfaces.MapControllerImpl; import cgeo.geocaching.maps.interfaces.MapProjectionImpl; import cgeo.geocaching.maps.interfaces.MapViewImpl; import cgeo.geocaching.maps.interfaces.OnMapDragListener; -import cgeo.geocaching.maps.interfaces.OverlayImpl; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapView; -import com.google.android.maps.Overlay; import org.apache.commons.lang3.reflect.MethodUtils; +import org.eclipse.jdt.annotation.NonNull; import android.app.Activity; import android.content.Context; @@ -54,7 +53,7 @@ public class GoogleMapView extends MapView implements MapViewImpl { } @Override - public void draw(Canvas canvas) { + public void draw(final Canvas canvas) { try { if (getMapZoomLevel() > 22) { // to avoid too close zoom level (mostly on Samsung Galaxy S series) getController().setZoom(22); @@ -91,6 +90,7 @@ public class GoogleMapView extends MapView implements MapViewImpl { } @Override + @NonNull public GeoPointImpl getMapViewCenter() { GeoPoint point = getMapCenter(); return new GoogleGeoPoint(point.getLatitudeE6(), point.getLongitudeE6()); @@ -102,11 +102,6 @@ public class GoogleMapView extends MapView implements MapViewImpl { } @Override - public void addOverlay(OverlayImpl ovl) { - getOverlays().add((Overlay) ovl); - } - - @Override public void clearOverlays() { getOverlays().clear(); } diff --git a/main/src/cgeo/geocaching/maps/interfaces/ItemizedOverlayImpl.java b/main/src/cgeo/geocaching/maps/interfaces/ItemizedOverlayImpl.java index 90c5b31..ee61f12 100644 --- a/main/src/cgeo/geocaching/maps/interfaces/ItemizedOverlayImpl.java +++ b/main/src/cgeo/geocaching/maps/interfaces/ItemizedOverlayImpl.java @@ -18,8 +18,6 @@ public interface ItemizedOverlayImpl extends OverlayImpl { void superSetLastFocusedItemIndex(int i); - Drawable superBoundCenter(Drawable markerIn); - Drawable superBoundCenterBottom(Drawable marker); boolean superOnTap(int index); diff --git a/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java b/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java index cb7ddc6..5ae8e15 100644 --- a/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java +++ b/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java @@ -4,6 +4,8 @@ import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.maps.CachesOverlay; import cgeo.geocaching.maps.PositionAndScaleOverlay; +import org.eclipse.jdt.annotation.NonNull; + import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; @@ -22,12 +24,11 @@ public interface MapViewImpl { void clearOverlays(); - void addOverlay(OverlayImpl ovl); - MapControllerImpl getMapController(); void destroyDrawingCache(); + @NonNull GeoPointImpl getMapViewCenter(); int getLatitudeSpan(); @@ -75,7 +76,7 @@ public interface MapViewImpl { /** * Indicates if the current map view supports different themes * for map rendering - * + * * @return true - supports custom themes, false - does not support custom themes */ boolean hasMapThemes(); diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java index 9e14e36..b9e40d7 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java @@ -70,11 +70,6 @@ public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlay } @Override - public Drawable superBoundCenter(Drawable markerIn) { - return ItemizedOverlay.boundCenter(markerIn); - } - - @Override public Drawable superBoundCenterBottom(Drawable marker) { return ItemizedOverlay.boundCenterBottom(marker); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java index 78aa47d..e993548 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java @@ -11,7 +11,6 @@ import cgeo.geocaching.maps.interfaces.MapProjectionImpl; import cgeo.geocaching.maps.interfaces.MapSource; import cgeo.geocaching.maps.interfaces.MapViewImpl; import cgeo.geocaching.maps.interfaces.OnMapDragListener; -import cgeo.geocaching.maps.interfaces.OverlayImpl; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; @@ -86,11 +85,6 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { } @Override - public void addOverlay(OverlayImpl ovl) { - getOverlays().add((Overlay) ovl); - } - - @Override public void clearOverlays() { getOverlays().clear(); } @@ -229,7 +223,7 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { @Override public void setMapTheme() { String customRenderTheme = Settings.getCustomRenderThemeFilePath(); - if (!StringUtils.isEmpty(customRenderTheme)) { + if (StringUtils.isNotEmpty(customRenderTheme)) { try { setRenderTheme(new File(customRenderTheme)); } catch (FileNotFoundException e) { diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java index 30355fd..a8111ed 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java @@ -70,11 +70,6 @@ public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlay } @Override - public Drawable superBoundCenter(Drawable markerIn) { - return ItemizedOverlay.boundCenter(markerIn); - } - - @Override public Drawable superBoundCenterBottom(Drawable marker) { return ItemizedOverlay.boundCenterBottom(marker); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java index c741a31..30caed5 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java @@ -10,10 +10,10 @@ import cgeo.geocaching.maps.interfaces.MapControllerImpl; import cgeo.geocaching.maps.interfaces.MapProjectionImpl; import cgeo.geocaching.maps.interfaces.MapViewImpl; import cgeo.geocaching.maps.interfaces.OnMapDragListener; -import cgeo.geocaching.maps.interfaces.OverlayImpl; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; +import org.eclipse.jdt.annotation.NonNull; import org.mapsforge.android.mapsold.GeoPoint; import org.mapsforge.android.mapsold.MapDatabase; import org.mapsforge.android.mapsold.MapView; @@ -66,6 +66,7 @@ public class MapsforgeMapView024 extends MapView implements MapViewImpl { } @Override + @NonNull public GeoPointImpl getMapViewCenter() { GeoPoint point = getMapCenter(); return new MapsforgeGeoPoint(point.getLatitudeE6(), point.getLongitudeE6()); @@ -77,11 +78,6 @@ public class MapsforgeMapView024 extends MapView implements MapViewImpl { } @Override - public void addOverlay(OverlayImpl ovl) { - getOverlays().add((Overlay) ovl); - } - - @Override public void clearOverlays() { getOverlays().clear(); } diff --git a/main/src/cgeo/geocaching/network/HtmlImage.java b/main/src/cgeo/geocaching/network/HtmlImage.java index 0daa588..524617c 100644 --- a/main/src/cgeo/geocaching/network/HtmlImage.java +++ b/main/src/cgeo/geocaching/network/HtmlImage.java @@ -6,14 +6,31 @@ import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.files.LocalStorage; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.FileUtils; import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.androidextra.Base64; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Scheduler; +import rx.Scheduler.Inner; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; +import rx.subscriptions.CompositeSubscription; import android.content.res.Resources; import android.graphics.Bitmap; @@ -31,9 +48,23 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Date; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; public class HtmlImage implements Html.ImageGetter { + // This class implements an all-purpose image getter that can also be used as a ImageGetter interface + // when displaying caches. An instance mainly has three possible use cases: + // - If onlySave is true, getDrawable() will return null immediately and will queue the image retrieval + // and saving in the loading subject. Downloads will start in parallel when the blocking + // waitForBackgroundLoading() method is called, and they can be cancelled through the given handler. + // - If onlySave is false and the instance is called through fetchDrawable(), then an observable for the + // given URL will be returned. This observable will emit the local copy of the image if it is present, + // regardless of its freshness, then if needed an updated fresher copy after retrieving it from the network. + // - If onlySave is false and the instance is used as an ImageGetter, only the final version of the + // image will be returned. + private static final String[] BLOCKED = new String[] { "gccounter.de", "gccounter.com", @@ -59,105 +90,183 @@ public class HtmlImage implements Html.ImageGetter { final private boolean returnErrorImage; final private int listId; final private boolean onlySave; - final private BitmapFactory.Options bfOptions; final private int maxWidth; final private int maxHeight; final private Resources resources; + // Background loading + final private PublishSubject<Observable<String>> loading = PublishSubject.create(); + final Observable<String> waitForEnd = Observable.merge(loading).publish().refCount(); + final CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe()); + final private Scheduler downloadScheduler = Schedulers.executor(new ThreadPoolExecutor(10, 10, 5, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>())); + public HtmlImage(final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) { this.geocode = geocode; this.returnErrorImage = returnErrorImage; this.listId = listId; this.onlySave = onlySave; - bfOptions = new BitmapFactory.Options(); - bfOptions.inTempStorage = new byte[16 * 1024]; - bfOptions.inPreferredConfig = Bitmap.Config.RGB_565; - Point displaySize = Compatibility.getDisplaySize(); this.maxWidth = displaySize.x - 25; this.maxHeight = displaySize.y - 25; this.resources = CgeoApplication.getInstance().getResources(); } + @Nullable @Override public BitmapDrawable getDrawable(final String url) { - // Reject empty and counter images URL + final Observable<BitmapDrawable> drawable = fetchDrawable(url); + if (onlySave) { + loading.onNext(drawable.map(new Func1<BitmapDrawable, String>() { + @Override + public String call(final BitmapDrawable bitmapDrawable) { + return url; + } + })); + return null; + } + return drawable.toBlockingObservable().lastOrDefault(null); + } + + // Caches are loaded from disk on Schedulers.computation() to avoid using more threads than processors + // on the phone while decoding the image. Downloads happen on downloadScheduler, in parallel with image + // decoding. + public Observable<BitmapDrawable> fetchDrawable(final String url) { + if (StringUtils.isBlank(url) || isCounter(url)) { - return new BitmapDrawable(resources, getTransparent1x1Image()); + return Observable.from(getTransparent1x1Image(resources)); } final boolean shared = url.contains("/images/icons/icon_"); final String pseudoGeocode = shared ? SHARED : geocode; - Bitmap imagePre = loadImageFromStorage(url, pseudoGeocode, shared); - - // Download image and save it to the cache - if (imagePre == null) { - final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, true); - if (url.startsWith("data:image/")) { - if (url.contains(";base64,")) { - // TODO: when we use SDK level 8 or above, we can use the streaming version of the base64 - // Android utilities. - byte[] decoded = Base64.decode(StringUtils.substringAfter(url, ";base64,"), Base64.DEFAULT); - OutputStream out = null; - try { - out = new FileOutputStream(file); - out.write(decoded); - } catch (final IOException e) { - Log.e("HtmlImage.getDrawable: cannot write file for decoded inline image", e); - return null; - } finally { - IOUtils.closeQuietly(out); + return Observable.create(new OnSubscribe<BitmapDrawable>() { + @Override + public void call(final Subscriber<? super BitmapDrawable> subscriber) { + Schedulers.computation().schedule(new Action1<Inner>() { + @Override + public void call(final Inner inner) { + final Pair<BitmapDrawable, Boolean> loaded = loadFromDisk(); + final BitmapDrawable bitmap = loaded.getLeft(); + if (loaded.getRight()) { + subscriber.onNext(bitmap); + subscriber.onCompleted(); + return; + } + if (bitmap != null && !onlySave) { + subscriber.onNext(bitmap); + } + downloadScheduler.schedule(new Action1<Inner>() { + @Override + public void call(final Inner inner) { + downloadAndSave(subscriber); + } + }); + } + }); + } + + private Pair<BitmapDrawable, Boolean> loadFromDisk() { + final Pair<Bitmap, Boolean> loadResult = loadImageFromStorage(url, pseudoGeocode, shared); + final Bitmap bitmap = loadResult.getLeft(); + return new ImmutablePair<BitmapDrawable, Boolean>(bitmap != null ? + ImageUtils.scaleBitmapToFitDisplay(bitmap) : + null, + loadResult.getRight()); + } + + private void downloadAndSave(final Subscriber<? super BitmapDrawable> subscriber) { + final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, true); + if (url.startsWith("data:image/")) { + if (url.contains(";base64,")) { + saveBase64ToFile(url, file); + } else { + Log.e("HtmlImage.getDrawable: unable to decode non-base64 inline image"); + subscriber.onCompleted(); + return; } } else { - Log.e("HtmlImage.getDrawable: unable to decode non-base64 inline image"); - return null; + if (subscription.isUnsubscribed() || downloadOrRefreshCopy(url, file)) { + // The existing copy was fresh enough or we were unsubscribed earlier. + subscriber.onCompleted(); + return; + } } - } else { - final String absoluteURL = makeAbsoluteURL(url); - - if (absoluteURL != null) { - try { - final HttpResponse httpResponse = Network.getRequest(absoluteURL, null, file); - if (httpResponse != null) { - final int statusCode = httpResponse.getStatusLine().getStatusCode(); - if (statusCode == 200) { - LocalStorage.saveEntityToFile(httpResponse, file); - } else if (statusCode == 304) { - if (!file.setLastModified(System.currentTimeMillis())) { - makeFreshCopy(file); - } + if (onlySave) { + subscriber.onCompleted(); + } else { + Schedulers.computation().schedule(new Action1<Inner>() { + @Override + public void call(final Inner inner) { + final Pair<BitmapDrawable, Boolean> loaded = loadFromDisk(); + final BitmapDrawable image = loaded.getLeft(); + if (image != null) { + subscriber.onNext(image); + } else { + subscriber.onNext(returnErrorImage ? + new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded)) : + getTransparent1x1Image(resources)); } + subscriber.onCompleted(); } - } catch (Exception e) { - Log.e("HtmlImage.getDrawable (downloading from web)", e); - } + }); } } - } - - if (onlySave) { - return null; - } + }); + } - // now load the newly downloaded image - if (imagePre == null) { - imagePre = loadImageFromStorage(url, pseudoGeocode, shared); + public void waitForBackgroundLoading(@Nullable final CancellableHandler handler) { + if (handler != null) { + handler.unsubscribeIfCancelled(subscription); } + loading.onCompleted(); + waitForEnd.toBlockingObservable().lastOrDefault(null); + } - // get image and return - if (imagePre == null) { - Log.d("HtmlImage.getDrawable: Failed to obtain image"); + /** + * Download or refresh the copy of <code>url</code> in <code>file</code>. + * + * @param url the url of the document + * @param file the file to save the document in + * @return <code>true</code> if the existing file was up-to-date, <code>false</code> otherwise + */ + private boolean downloadOrRefreshCopy(final String url, final File file) { + final String absoluteURL = makeAbsoluteURL(url); - if (returnErrorImage) { - imagePre = BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded); - } else { - imagePre = getTransparent1x1Image(); + if (absoluteURL != null) { + try { + final HttpResponse httpResponse = Network.getRequest(absoluteURL, null, file); + if (httpResponse != null) { + final int statusCode = httpResponse.getStatusLine().getStatusCode(); + if (statusCode == 200) { + LocalStorage.saveEntityToFile(httpResponse, file); + } else if (statusCode == 304) { + if (!file.setLastModified(System.currentTimeMillis())) { + makeFreshCopy(file); + } + return true; + } + } + } catch (Exception e) { + Log.e("HtmlImage.downloadOrRefreshCopy", e); } } + return false; + } - return imagePre != null ? ImageUtils.scaleBitmapToFitDisplay(imagePre) : null; + private static void saveBase64ToFile(final String url, final File file) { + // TODO: when we use SDK level 8 or above, we can use the streaming version of the base64 + // Android utilities. + OutputStream out = null; + try { + out = new FileOutputStream(file); + out.write(Base64.decode(StringUtils.substringAfter(url, ";base64,"), Base64.DEFAULT)); + } catch (final IOException e) { + Log.e("HtmlImage.saveBase64ToFile: cannot write file for decoded inline image", e); + } finally { + IOUtils.closeQuietly(out); + } } /** @@ -180,25 +289,35 @@ public class HtmlImage implements Html.ImageGetter { } } - private Bitmap getTransparent1x1Image() { - return BitmapFactory.decodeResource(resources, R.drawable.image_no_placement); + private BitmapDrawable getTransparent1x1Image(final Resources res) { + return new BitmapDrawable(res, BitmapFactory.decodeResource(resources, R.drawable.image_no_placement)); } - private Bitmap loadImageFromStorage(final String url, final String pseudoGeocode, final boolean forceKeep) { + /** + * Load an image from primary or secondary storage. + * + * @param url the image URL + * @param pseudoGeocode the geocode or the shared name + * @param forceKeep keep the image if it is there, without checking its freshness + * @return <code>true</code> if the image was there and is fresh enough, <code>false</code> otherwise + */ + @NonNull + private Pair<Bitmap, Boolean> loadImageFromStorage(final String url, final String pseudoGeocode, final boolean forceKeep) { try { final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, false); - final Bitmap image = loadCachedImage(file, forceKeep); - if (image != null) { + final Pair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep); + if (image.getRight() || image.getLeft() != null) { return image; } final File fileSec = LocalStorage.getStorageSecFile(pseudoGeocode, url, true); return loadCachedImage(fileSec, forceKeep); } catch (Exception e) { - Log.w("HtmlImage.getDrawable (reading cache)", e); + Log.w("HtmlImage.loadImageFromStorage", e); } - return null; + return new ImmutablePair<Bitmap, Boolean>(null, false); } + @Nullable private String makeAbsoluteURL(final String url) { // Check if uri is absolute or not, if not attach the connector hostname // FIXME: that should also include the scheme @@ -222,21 +341,39 @@ public class HtmlImage implements Html.ImageGetter { return null; } - private Bitmap loadCachedImage(final File file, final boolean forceKeep) { + /** + * Load a previously saved image. + * + * @param file the file on disk + * @param forceKeep keep the image if it is there, without checking its freshness + * @return a pair with <code>true</code> if the image was there and is fresh enough or <code>false</code> otherwise, + * and the image (possibly <code>null</code> if the first component is <code>false</code> and the image + * could not be loaded, or if the first component is <code>true</code> and <code>onlySave</code> is also + * <code>true</code>) + */ + @NonNull + private Pair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) { if (file.exists()) { - if (listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (new Date().getTime() - (24 * 60 * 60 * 1000)) || forceKeep) { - setSampleSize(file); - final Bitmap image = BitmapFactory.decodeFile(file.getPath(), bfOptions); - if (image == null) { - Log.e("Cannot decode bitmap from " + file.getPath()); - } - return image; + final boolean freshEnough = listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (new Date().getTime() - (24 * 60 * 60 * 1000)) || forceKeep; + if (onlySave) { + return new ImmutablePair<Bitmap, Boolean>(null, true); + } + final BitmapFactory.Options bfOptions = new BitmapFactory.Options(); + bfOptions.inTempStorage = new byte[16 * 1024]; + bfOptions.inPreferredConfig = Bitmap.Config.RGB_565; + setSampleSize(file, bfOptions); + final Bitmap image = BitmapFactory.decodeFile(file.getPath(), bfOptions); + if (image == null) { + Log.e("Cannot decode bitmap from " + file.getPath()); + return new ImmutablePair<Bitmap, Boolean>(null, false); } + return new ImmutablePair<Bitmap, Boolean>(image, + freshEnough); } - return null; + return new ImmutablePair<Bitmap, Boolean>(null, false); } - private void setSampleSize(final File file) { + private void setSampleSize(final File file, final BitmapFactory.Options bfOptions) { //Decode image size only BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; diff --git a/main/src/cgeo/geocaching/network/Network.java b/main/src/cgeo/geocaching/network/Network.java index e891d3b..d8638db 100644 --- a/main/src/cgeo/geocaching/network/Network.java +++ b/main/src/cgeo/geocaching/network/Network.java @@ -5,18 +5,10 @@ import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.TextUtils; -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HeaderElement; import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpException; -import ch.boye.httpclientandroidlib.HttpRequest; -import ch.boye.httpclientandroidlib.HttpRequestInterceptor; import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.HttpResponseInterceptor; import ch.boye.httpclientandroidlib.NameValuePair; -import ch.boye.httpclientandroidlib.ProtocolException; import ch.boye.httpclientandroidlib.client.HttpClient; -import ch.boye.httpclientandroidlib.client.entity.GzipDecompressingEntity; import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity; import ch.boye.httpclientandroidlib.client.methods.HttpGet; import ch.boye.httpclientandroidlib.client.methods.HttpPost; @@ -26,15 +18,14 @@ import ch.boye.httpclientandroidlib.entity.StringEntity; import ch.boye.httpclientandroidlib.entity.mime.MultipartEntity; import ch.boye.httpclientandroidlib.entity.mime.content.FileBody; import ch.boye.httpclientandroidlib.entity.mime.content.StringBody; +import ch.boye.httpclientandroidlib.impl.client.DecompressingHttpClient; import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.impl.client.DefaultRedirectStrategy; +import ch.boye.httpclientandroidlib.impl.client.LaxRedirectStrategy; import ch.boye.httpclientandroidlib.params.BasicHttpParams; import ch.boye.httpclientandroidlib.params.CoreConnectionPNames; import ch.boye.httpclientandroidlib.params.CoreProtocolPNames; import ch.boye.httpclientandroidlib.params.HttpParams; -import ch.boye.httpclientandroidlib.protocol.HttpContext; import ch.boye.httpclientandroidlib.util.EntityUtils; - import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.Nullable; @@ -48,6 +39,7 @@ import android.net.Uri; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -86,60 +78,8 @@ public abstract class Network { final DefaultHttpClient client = new DefaultHttpClient(); client.setCookieStore(Cookies.cookieStore); client.setParams(clientParams); - - client.setRedirectStrategy(new DefaultRedirectStrategy() { - @Override - public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) { - boolean isRedirect = false; - try { - isRedirect = super.isRedirected(request, response, context); - } catch (final ProtocolException e) { - Log.e("httpclient.isRedirected: unable to check for redirection", e); - } - if (!isRedirect) { - final int responseCode = response.getStatusLine().getStatusCode(); - if (responseCode == 301 || responseCode == 302) { - return true; - } - } - return isRedirect; - } - }); - - client.addRequestInterceptor(new HttpRequestInterceptor() { - - @Override - public void process( - final HttpRequest request, - final HttpContext context) throws HttpException, IOException { - if (!request.containsHeader("Accept-Encoding")) { - request.addHeader("Accept-Encoding", "gzip"); - } - } - }); - client.addResponseInterceptor(new HttpResponseInterceptor() { - - @Override - public void process( - final HttpResponse response, - final HttpContext context) throws HttpException, IOException { - final HttpEntity entity = response.getEntity(); - if (entity != null) { - final Header contentEncoding = entity.getContentEncoding(); - if (contentEncoding != null) { - for (final HeaderElement codec : contentEncoding.getElements()) { - if (codec.getName().equalsIgnoreCase("gzip")) { - response.setEntity(new GzipDecompressingEntity(response.getEntity())); - return; - } - } - } - } - } - - }); - - return client; + client.setRedirectStrategy(new LaxRedirectStrategy()); + return new DecompressingHttpClient(client); } /** @@ -426,6 +366,30 @@ public abstract class Network { return null; } + /** + * Get the input stream corresponding to a HTTP response if it exists. + * + * @param response a HTTP response, which can be null + * @return the input stream if the HTTP request is successful, <code>null</code> otherwise + */ + @Nullable + public static InputStream getResponseStream(@Nullable final HttpResponse response) { + if (!isSuccess(response)) { + return null; + } + assert(response != null); + final HttpEntity entity = response.getEntity(); + if (entity == null) { + return null; + } + try { + return entity.getContent(); + } catch (final IOException e) { + Log.e("Network.getResponseStream", e); + return null; + } + } + @Nullable private static String getResponseDataNoError(final HttpResponse response, boolean replaceWhitespace) { try { diff --git a/main/src/cgeo/geocaching/network/OAuth.java b/main/src/cgeo/geocaching/network/OAuth.java index c033660..fa376af 100644 --- a/main/src/cgeo/geocaching/network/OAuth.java +++ b/main/src/cgeo/geocaching/network/OAuth.java @@ -37,13 +37,14 @@ public class OAuth { } final String keysPacked = consumerSecret + "&" + StringUtils.defaultString(tokenSecret); // both even if empty some of them! - final String requestPacked = method + "&" + OAuth.percentEncode((https ? "https" : "http") + "://" + host + path) + "&" + OAuth.percentEncode(StringUtils.join(paramsEncoded.toArray(), '&')); + final @NonNull String joinedParams = StringUtils.join(paramsEncoded.toArray(), '&'); + final String requestPacked = method + "&" + OAuth.percentEncode((https ? "https" : "http") + "://" + host + path) + "&" + OAuth.percentEncode(joinedParams); params.put("oauth_signature", CryptUtils.base64Encode(CryptUtils.hashHmac(requestPacked, keysPacked))); } /** * percent encode following http://tools.ietf.org/html/rfc5849#section-3.6 - * + * * @param url * @return */ diff --git a/main/src/cgeo/geocaching/network/OAuthAuthorizationActivity.java b/main/src/cgeo/geocaching/network/OAuthAuthorizationActivity.java index 888cf77..a5a2383 100644 --- a/main/src/cgeo/geocaching/network/OAuthAuthorizationActivity.java +++ b/main/src/cgeo/geocaching/network/OAuthAuthorizationActivity.java @@ -7,6 +7,7 @@ import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; +import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.ParseException; import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity; import ch.boye.httpclientandroidlib.util.EntityUtils; @@ -34,6 +35,10 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { public static final int NOT_AUTHENTICATED = 0; public static final int AUTHENTICATED = 1; + private static final int STATUS_ERROR = 0; + private static final int STATUS_SUCCESS = 1; + private static final int STATUS_ERROR_EXT_MSG = 2; + @NonNull final private String host; @NonNull final private String pathRequest; @NonNull final private String pathAuthorize; @@ -62,8 +67,13 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { startButton.setOnClickListener(new StartListener()); startButton.setEnabled(true); - if (msg.what == 1) { + if (msg.what == STATUS_SUCCESS) { startButton.setText(getAuthAgain()); + } else if (msg.what == STATUS_ERROR_EXT_MSG) { + String errMsg = getErrAuthInitialize(); + errMsg += msg.obj != null ? "\n" + msg.obj.toString() : ""; + showToast(errMsg); + startButton.setText(getAuthStart()); } else { showToast(getErrAuthInitialize()); startButton.setText(getAuthStart()); @@ -161,37 +171,49 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { params.put("oauth_callback", callback); final String method = "GET"; OAuth.signOAuth(host, pathRequest, method, https, params, null, null, consumerKey, consumerSecret); - final String line = Network.getResponseData(Network.getRequest(getUrlPrefix() + host + pathRequest, params)); + final HttpResponse response = Network.getRequest(getUrlPrefix() + host + pathRequest, params); - int status = 0; - if (StringUtils.isNotBlank(line)) { - assert line != null; - final MatcherWrapper paramsMatcher1 = new MatcherWrapper(paramsPattern1, line); - if (paramsMatcher1.find()) { - OAtoken = paramsMatcher1.group(1); - } - final MatcherWrapper paramsMatcher2 = new MatcherWrapper(paramsPattern2, line); - if (paramsMatcher2.find()) { - OAtokenSecret = paramsMatcher2.group(1); - } + if (Network.isSuccess(response)) { + final String line = Network.getResponseData(response); - if (StringUtils.isNotBlank(OAtoken) && StringUtils.isNotBlank(OAtokenSecret)) { - setTempTokens(OAtoken, OAtokenSecret); - try { - final Parameters paramsBrowser = new Parameters(); - paramsBrowser.put("oauth_token", OAtoken); - final String encodedParams = EntityUtils.toString(new UrlEncodedFormEntity(paramsBrowser)); - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getUrlPrefix() + host + pathAuthorize + "?" + encodedParams))); - status = 1; - } catch (ParseException e) { - Log.e("OAuthAuthorizationActivity.requestToken", e); - } catch (IOException e) { - Log.e("OAuthAuthorizationActivity.requestToken", e); + int status = STATUS_ERROR; + if (StringUtils.isNotBlank(line)) { + assert line != null; + final MatcherWrapper paramsMatcher1 = new MatcherWrapper(paramsPattern1, line); + if (paramsMatcher1.find()) { + OAtoken = paramsMatcher1.group(1); + } + final MatcherWrapper paramsMatcher2 = new MatcherWrapper(paramsPattern2, line); + if (paramsMatcher2.find()) { + OAtokenSecret = paramsMatcher2.group(1); + } + + if (StringUtils.isNotBlank(OAtoken) && StringUtils.isNotBlank(OAtokenSecret)) { + setTempTokens(OAtoken, OAtokenSecret); + try { + final Parameters paramsBrowser = new Parameters(); + paramsBrowser.put("oauth_token", OAtoken); + final String encodedParams = EntityUtils.toString(new UrlEncodedFormEntity(paramsBrowser)); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getUrlPrefix() + host + pathAuthorize + "?" + encodedParams))); + status = STATUS_SUCCESS; + } catch (ParseException e) { + Log.e("OAuthAuthorizationActivity.requestToken", e); + } catch (IOException e) { + Log.e("OAuthAuthorizationActivity.requestToken", e); + } } } - } - requestTokenHandler.sendEmptyMessage(status); + requestTokenHandler.sendEmptyMessage(status); + } else { + final String extErrMsg = getExtendedErrorMsg(response); + if (StringUtils.isNotBlank(extErrMsg)) { + final Message msg = requestTokenHandler.obtainMessage(STATUS_ERROR_EXT_MSG, extErrMsg); + requestTokenHandler.sendMessage(msg); + } else { + requestTokenHandler.sendEmptyMessage(STATUS_ERROR); + } + } } private void changeToken(final String verifier) { @@ -306,6 +328,18 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { return res.getString(R.string.err_auth_process); } + /** + * Allows deriving classes to check the response for error messages specific to their OAuth implementation + * + * @param response + * The error response of the token request + * @return String with a more detailed error message (user-facing, localized), can be empty + */ + @SuppressWarnings("static-method") + protected String getExtendedErrorMsg(HttpResponse response) { + return StringUtils.EMPTY; + } + protected String getAuthDialogWait() { return res.getString(R.string.auth_dialog_waiting, getAuthTitle()); } diff --git a/main/src/cgeo/geocaching/network/StatusUpdater.java b/main/src/cgeo/geocaching/network/StatusUpdater.java index cb4c7f4..4055f01 100644 --- a/main/src/cgeo/geocaching/network/StatusUpdater.java +++ b/main/src/cgeo/geocaching/network/StatusUpdater.java @@ -1,21 +1,22 @@ package cgeo.geocaching.network; import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.utils.MemorySubject; -import cgeo.geocaching.utils.PeriodicHandler; -import cgeo.geocaching.utils.PeriodicHandler.PeriodicHandlerListener; import cgeo.geocaching.utils.Version; import org.json.JSONException; import org.json.JSONObject; +import rx.Scheduler; +import rx.schedulers.Schedulers; +import rx.subjects.BehaviorSubject; +import rx.functions.Action1; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; -import android.os.Looper; import java.util.Locale; +import java.util.concurrent.TimeUnit; -public class StatusUpdater extends MemorySubject<StatusUpdater.Status> implements Runnable, PeriodicHandlerListener { +public class StatusUpdater { static public class Status { final public String message; @@ -30,24 +31,40 @@ public class StatusUpdater extends MemorySubject<StatusUpdater.Status> implement this.url = url; } + Status(final JSONObject response) { + message = get(response, "message"); + messageId = get(response, "message_id"); + icon = get(response, "icon"); + url = get(response, "url"); + } + final static public Status closeoutStatus = new Status("", "status_closeout_warning", "attribute_abandonedbuilding", "http://faq.cgeo.org/#7_69"); - final static public Status defaultStatus() { + final static public Status defaultStatus(final Status upToDate) { + if (upToDate != null && upToDate.message != null) { + return upToDate; + } return VERSION.SDK_INT < VERSION_CODES.ECLAIR_MR1 ? closeoutStatus : null; } } - @Override - public void onPeriodic() { - final JSONObject response = - Network.requestJSON("http://status.cgeo.org/api/status.json", - new Parameters("version_code", String.valueOf(Version.getVersionCode(CgeoApplication.getInstance())), - "version_name", Version.getVersionName(CgeoApplication.getInstance()), - "locale", Locale.getDefault().toString())); - if (response != null) { - notifyObservers(new Status(get(response, "message"), get(response, "message_id"), get(response, "icon"), get(response, "url"))); - } + final static public BehaviorSubject<Status> latestStatus = BehaviorSubject.create(Status.defaultStatus(null)); + + static { + Schedulers.io().schedulePeriodically(new Action1<Scheduler.Inner>() { + @Override + public void call(final Scheduler.Inner inner) { + final JSONObject response = + Network.requestJSON("http://status.cgeo.org/api/status.json", + new Parameters("version_code", String.valueOf(Version.getVersionCode(CgeoApplication.getInstance())), + "version_name", Version.getVersionName(CgeoApplication.getInstance()), + "locale", Locale.getDefault().toString())); + if (response != null) { + latestStatus.onNext(Status.defaultStatus(new Status(response))); + } + } + }, 0, 1800, TimeUnit.SECONDS); } private static String get(final JSONObject json, final String key) { @@ -58,11 +75,4 @@ public class StatusUpdater extends MemorySubject<StatusUpdater.Status> implement } } - @Override - public void run() { - Looper.prepare(); - new PeriodicHandler(1800000L, this).start(); - Looper.loop(); - } - } diff --git a/main/src/cgeo/geocaching/search/AutoCompleteAdapter.java b/main/src/cgeo/geocaching/search/AutoCompleteAdapter.java new file mode 100644 index 0000000..15a45c6 --- /dev/null +++ b/main/src/cgeo/geocaching/search/AutoCompleteAdapter.java @@ -0,0 +1,71 @@ +package cgeo.geocaching.search; + +import org.apache.commons.lang3.StringUtils; + +import rx.functions.Func1; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.Filter; + +/** + * The standard auto completion only matches user input at word boundaries. Therefore searching "est" will not match + * "test". This adapter matches everywhere. + * + */ +public class AutoCompleteAdapter extends ArrayAdapter<String> { + + private String[] suggestions; + private final Func1<String, String[]> suggestionFunction; + + public AutoCompleteAdapter(Context context, int textViewResourceId, final Func1<String, String[]> suggestionFunction) { + super(context, textViewResourceId); + this.suggestionFunction = suggestionFunction; + } + + @Override + public int getCount() { + return suggestions.length; + } + + @Override + public String getItem(int index) { + return suggestions[index]; + } + + @Override + public Filter getFilter() { + Filter filter = new Filter() { + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults filterResults = new FilterResults(); + if (constraint == null) { + return filterResults; + } + String trimmed = StringUtils.trim(constraint.toString()); + if (StringUtils.length(trimmed) >= 2) { + String[] newResults = suggestionFunction.call(trimmed); + + // Assign the data to the FilterResults, but do not yet store in the global member. + // Otherwise we might invalidate the adapter and cause an IllegalStateException. + filterResults.values = newResults; + filterResults.count = newResults.length; + } + return filterResults; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults filterResults) { + if (filterResults != null && filterResults.count > 0) { + suggestions = (String[]) filterResults.values; + notifyDataSetChanged(); + } + else { + notifyDataSetInvalidated(); + } + } + }; + return filter; + } +}
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/search/SuggestionProvider.java b/main/src/cgeo/geocaching/search/SuggestionProvider.java new file mode 100644 index 0000000..c0a7728 --- /dev/null +++ b/main/src/cgeo/geocaching/search/SuggestionProvider.java @@ -0,0 +1,57 @@ +package cgeo.geocaching.search; + +import cgeo.geocaching.DataStore; + +import org.apache.commons.lang3.StringUtils; + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +public class SuggestionProvider extends ContentProvider { + + private static Cursor lastCursor; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public String getType(final Uri arg0) { + return SearchManager.SUGGEST_MIME_TYPE; + } + + @Override + public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) { + final String searchTerm = uri.getLastPathSegment(); + // can be empty when deleting the query + if (StringUtils.equals(searchTerm, SearchManager.SUGGEST_URI_PATH_QUERY)) { + return lastCursor; + } + return getSuggestions(searchTerm); + } + + private static Cursor getSuggestions(final String searchTerm) { + lastCursor = DataStore.findSuggestions(searchTerm); + return lastCursor; + } + + @Override + public int delete(final Uri uri, final String selection, final String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri insert(final Uri uri, final ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + +} diff --git a/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java b/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java index d3aae5c..1efbc96 100644 --- a/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java +++ b/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java @@ -5,28 +5,23 @@ import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.network.Cookies; import cgeo.geocaching.ui.dialog.Dialogs; -import cgeo.geocaching.utils.Log; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; +import rx.Observable; +import rx.android.observables.AndroidObservable; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.schedulers.Schedulers; -import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Message; import android.preference.Preference; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -public abstract class AbstractCheckCredentialsPreference extends Preference { - - public AbstractCheckCredentialsPreference(Context context) { - super(context); - } +public abstract class AbstractCheckCredentialsPreference extends AbstractClickablePreference { public AbstractCheckCredentialsPreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -37,57 +32,25 @@ public abstract class AbstractCheckCredentialsPreference extends Preference { } @Override - protected View onCreateView(ViewGroup parent) { - setOnPreferenceClickListener(new LoginCheckClickListener()); - return super.onCreateView(parent); + protected OnPreferenceClickListener getOnPreferenceClickListener(final SettingsActivity activity) { + return new LoginCheckClickListener(activity); } protected abstract ImmutablePair<String, String> getCredentials(); - protected abstract Object login(); + protected abstract ImmutablePair<StatusCode, Drawable> login(); private class LoginCheckClickListener implements OnPreferenceClickListener { - private Resources res; - private SettingsActivity activity; - - private ProgressDialog loginDialog; - @SuppressLint("HandlerLeak") - private Handler logInHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - try { - if (loginDialog != null && loginDialog.isShowing()) { - loginDialog.dismiss(); - } + final private SettingsActivity activity; - if (msg.obj == null || (msg.obj instanceof Drawable)) { - Dialogs.message(activity, R.string.init_login_popup, R.string.init_login_popup_ok, (Drawable) msg.obj); - } else { - Dialogs.message(activity, R.string.init_login_popup, - res.getString(R.string.init_login_popup_failed_reason) - + " " - + ((StatusCode) msg.obj).getErrorString(res) - + "."); - } - } catch (Exception e) { - ActivityMixin.showToast(activity, R.string.err_login_failed); - Log.e("SettingsActivity.logInHandler", e); - } finally { - if (loginDialog != null && loginDialog.isShowing()) { - loginDialog.dismiss(); - } - // enable/disable basic member preferences - activity.initBasicMemberPreferences(); - } - } - }; + LoginCheckClickListener(final SettingsActivity activity) { + this.activity = activity; + } @Override public boolean onPreferenceClick(Preference preference) { - this.activity = (SettingsActivity) AbstractCheckCredentialsPreference.this.getContext(); - this.res = activity.getResources(); - - ImmutablePair<String, String> credentials = getCredentials(); + final Resources res = activity.getResources(); + final ImmutablePair<String, String> credentials = getCredentials(); // check credentials for validity if (StringUtils.isBlank(credentials.getLeft()) @@ -96,19 +59,33 @@ public abstract class AbstractCheckCredentialsPreference extends Preference { return false; } - loginDialog = ProgressDialog.show(activity, + final ProgressDialog loginDialog = ProgressDialog.show(activity, res.getString(R.string.init_login_popup), res.getString(R.string.init_login_popup_working), true); loginDialog.setCancelable(false); Cookies.clearCookies(); - (new Thread() { + AndroidObservable.fromActivity(activity, Observable.defer(new Func0<Observable<ImmutablePair<StatusCode, Drawable>>>() { @Override - public void run() { - Object payload = login(); - logInHandler.obtainMessage(0, payload).sendToTarget(); + public Observable<ImmutablePair<StatusCode, Drawable>> call() { + return Observable.from(login()); + } + }).subscribeOn(Schedulers.io())).subscribe(new Action1<ImmutablePair<StatusCode, Drawable>>() { + @Override + public void call(final ImmutablePair<StatusCode, Drawable> loginInfo) { + loginDialog.dismiss(); + if (loginInfo.getLeft() == StatusCode.NO_ERROR) { + Dialogs.message(activity, R.string.init_login_popup, R.string.init_login_popup_ok, loginInfo.getRight()); + } else { + Dialogs.message(activity, R.string.init_login_popup, + res.getString(R.string.init_login_popup_failed_reason) + + " " + + loginInfo.getLeft().getErrorString(res) + + "."); + } + activity.initBasicMemberPreferences(); } - }).start(); + }); return false; // no shared preference has to be changed } diff --git a/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java b/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java new file mode 100644 index 0000000..f4080cd --- /dev/null +++ b/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java @@ -0,0 +1,30 @@ +package cgeo.geocaching.settings; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +abstract class AbstractClickablePreference extends Preference { + + final SettingsActivity activity; + + public AbstractClickablePreference(Context context, AttributeSet attrs) { + super(context, attrs); + activity = (SettingsActivity) context; + } + + public AbstractClickablePreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + activity = (SettingsActivity) context; + } + + @Override + protected View onCreateView(ViewGroup parent) { + setOnPreferenceClickListener(getOnPreferenceClickListener(activity)); + return super.onCreateView(parent); + } + + abstract protected OnPreferenceClickListener getOnPreferenceClickListener(final SettingsActivity activity); +} diff --git a/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java b/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java index 46a3661..c1cf740 100644 --- a/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java +++ b/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java @@ -7,14 +7,11 @@ import cgeo.geocaching.enumerations.StatusCode; import org.apache.commons.lang3.tuple.ImmutablePair; import android.content.Context; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; public class CheckECCredentialsPreference extends AbstractCheckCredentialsPreference { - public CheckECCredentialsPreference(Context context) { - super(context); - } - public CheckECCredentialsPreference(Context context, AttributeSet attrs) { super(context, attrs); } @@ -29,12 +26,7 @@ public class CheckECCredentialsPreference extends AbstractCheckCredentialsPrefer } @Override - protected Object login() { - final StatusCode loginResult = ECLogin.getInstance().login(); - Object payload = loginResult; - if (loginResult == StatusCode.NO_ERROR) { - payload = null; - } - return payload; + protected ImmutablePair<StatusCode, Drawable> login() { + return new ImmutablePair<StatusCode, Drawable>(ECLogin.getInstance().login(), null); } } diff --git a/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java b/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java index 12c8b24..8257fdd 100644 --- a/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java +++ b/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java @@ -6,14 +6,11 @@ import cgeo.geocaching.enumerations.StatusCode; import org.apache.commons.lang3.tuple.ImmutablePair; import android.content.Context; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; public class CheckGcCredentialsPreference extends AbstractCheckCredentialsPreference { - public CheckGcCredentialsPreference(Context context) { - super(context); - } - public CheckGcCredentialsPreference(Context context, AttributeSet attrs) { super(context, attrs); } @@ -28,13 +25,14 @@ public class CheckGcCredentialsPreference extends AbstractCheckCredentialsPrefer } @Override - protected Object login() { + protected ImmutablePair<StatusCode, Drawable> login() { final StatusCode loginResult = GCLogin.getInstance().login(); - Object payload = loginResult; - if (loginResult == StatusCode.NO_ERROR) { - GCLogin.detectGcCustomDate(); - payload = GCLogin.getInstance().downloadAvatarAndGetMemberStatus(); + switch (loginResult) { + case NO_ERROR: + GCLogin.detectGcCustomDate(); + return new ImmutablePair<StatusCode, Drawable>(StatusCode.NO_ERROR, GCLogin.getInstance().downloadAvatarAndGetMemberStatus()); + default: + return new ImmutablePair<StatusCode, Drawable>(loginResult, null); } - return payload; } } diff --git a/main/src/cgeo/geocaching/settings/OAuthPreference.java b/main/src/cgeo/geocaching/settings/OAuthPreference.java index 3550947..df77197 100644 --- a/main/src/cgeo/geocaching/settings/OAuthPreference.java +++ b/main/src/cgeo/geocaching/settings/OAuthPreference.java @@ -10,10 +10,8 @@ import android.content.Context; import android.content.Intent; import android.preference.Preference; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -public class OAuthPreference extends Preference { +public class OAuthPreference extends AbstractClickablePreference { private static final int NO_KEY = -1; @@ -23,8 +21,8 @@ public class OAuthPreference extends Preference { OCPL(R.string.pref_fakekey_ocpl_authorization, OCPLAuthorizationActivity.class), TWITTER(R.string.pref_fakekey_twitter_authorization, TwitterAuthorizationActivity.class); - public int prefKeyId; - public Class<?> authActivity; + public final int prefKeyId; + public final Class<?> authActivity; OAuthActivityMapping(int prefKeyId, Class<?> clazz) { this.prefKeyId = prefKeyId; @@ -44,11 +42,6 @@ public class OAuthPreference extends Preference { return OAuthActivityMapping.NONE; } - public OAuthPreference(Context context) { - super(context); - this.oAuthMapping = getAuthorization(); - } - public OAuthPreference(Context context, AttributeSet attrs) { super(context, attrs); this.oAuthMapping = getAuthorization(); @@ -60,10 +53,9 @@ public class OAuthPreference extends Preference { } @Override - protected View onCreateView(ViewGroup parent) { - final SettingsActivity activity = (SettingsActivity) getContext(); - - setOnPreferenceClickListener(new OnPreferenceClickListener() { + protected OnPreferenceClickListener getOnPreferenceClickListener(final SettingsActivity activity) { + activity.setOcAuthTitle(oAuthMapping.prefKeyId); + return new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { if (oAuthMapping.authActivity != null) { @@ -74,9 +66,7 @@ public class OAuthPreference extends Preference { } return false; // no shared preference has to be changed } - }); + }; - activity.setOcAuthTitle(oAuthMapping.prefKeyId); - return super.onCreateView(parent); } } diff --git a/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java b/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java index 3e838ab..a1ab215 100644 --- a/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java +++ b/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java @@ -8,26 +8,19 @@ import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.lang3.StringUtils; +import rx.Observable; +import rx.android.observables.AndroidObservable; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.schedulers.Schedulers; import android.app.ProgressDialog; import android.content.Context; -import android.os.Handler; -import android.os.Message; import android.preference.Preference; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; - -public class RegisterSend2CgeoPreference extends Preference { - ProgressDialog progressDialog; - SettingsActivity activity; - - public RegisterSend2CgeoPreference(Context context) { - super(context); - } +public class RegisterSend2CgeoPreference extends AbstractClickablePreference { public RegisterSend2CgeoPreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -37,42 +30,9 @@ public class RegisterSend2CgeoPreference extends Preference { super(context, attrs, defStyle); } - private Handler webAuthHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // satisfy static code analysis - if (activity == null) { - return; - } - - try { - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - - if (msg.what > 0) { - Dialogs.message(activity, R.string.init_sendToCgeo, - activity.getString(R.string.init_sendToCgeo_register_ok) - .replace("####", String.valueOf(msg.what))); - } else { - Dialogs.message(activity, R.string.init_sendToCgeo, R.string.init_sendToCgeo_register_fail); - } - } catch (Exception e) { - ActivityMixin.showToast(activity, R.string.init_sendToCgeo_register_fail); - Log.e("SettingsActivity.webHandler", e); - } - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } - }; - @Override - protected View onCreateView(ViewGroup parent) { - activity = (SettingsActivity) getContext(); - - setOnPreferenceClickListener(new OnPreferenceClickListener() { + protected OnPreferenceClickListener getOnPreferenceClickListener(final SettingsActivity activity) { + return new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { // satisfy static code analysis @@ -88,43 +48,51 @@ public class RegisterSend2CgeoPreference extends Preference { return false; } - progressDialog = ProgressDialog.show(activity, + final ProgressDialog progressDialog = ProgressDialog.show(activity, activity.getString(R.string.init_sendToCgeo), activity.getString(R.string.init_sendToCgeo_registering), true); progressDialog.setCancelable(false); - (new Thread() { - - @Override - public void run() { - int pin = 0; - - final String nam = StringUtils.defaultString(deviceName); - final String cod = StringUtils.defaultString(deviceCode); - - final Parameters params = new Parameters("name", nam, "code", cod); - HttpResponse response = Network.getRequest("http://send2.cgeo.org/auth.html", params); - - if (response != null && response.getStatusLine().getStatusCode() == 200) { - //response was OK - String[] strings = StringUtils.split(Network.getResponseData(response), ','); - try { - pin = Integer.parseInt(strings[1].trim()); - } catch (Exception e) { - Log.e("webDialog", e); + AndroidObservable.fromActivity(activity, + Observable.defer(new Func0<Observable<Integer>>() { + @Override + public Observable<Integer> call() { + final String nam = StringUtils.defaultString(deviceName); + final String cod = StringUtils.defaultString(deviceCode); + + final Parameters params = new Parameters("name", nam, "code", cod); + HttpResponse response = Network.getRequest("http://send2.cgeo.org/auth.html", params); + + if (response != null && response.getStatusLine().getStatusCode() == 200) { + //response was OK + final String[] strings = StringUtils.split(Network.getResponseData(response), ','); + Settings.setWebNameCode(nam, strings[0]); + try { + return Observable.from(Integer.parseInt(strings[1].trim())); + } catch (final Exception e) { + Log.e("RegisterSend2CgeoPreference", e); + } + } + + return Observable.empty(); } - String code = strings[0]; - Settings.setWebNameCode(nam, code); + }).firstOrDefault(0).subscribeOn(Schedulers.io())).subscribe(new Action1<Integer>() { + @Override + public void call(final Integer pin) { + progressDialog.dismiss(); + if (pin > 0) { + Dialogs.message(activity, R.string.init_sendToCgeo, + activity.getString(R.string.init_sendToCgeo_register_ok) + .replace("####", String.valueOf(pin))); + } else { + Dialogs.message(activity, R.string.init_sendToCgeo, R.string.init_sendToCgeo_register_fail); } - - webAuthHandler.sendEmptyMessage(pin); } - }).start(); + }); return true; } - }); - return super.onCreateView(parent); + }; } } diff --git a/main/src/cgeo/geocaching/settings/Settings.java b/main/src/cgeo/geocaching/settings/Settings.java index 0732866..6c3c984 100644 --- a/main/src/cgeo/geocaching/settings/Settings.java +++ b/main/src/cgeo/geocaching/settings/Settings.java @@ -54,8 +54,8 @@ public class Settings { private final static int unitsMetric = 1; // twitter api keys - private final static String keyConsumerPublic = CryptUtils.rot13("ESnsCvAv3kEupF1GCR3jGj"); - private final static String keyConsumerSecret = CryptUtils.rot13("7vQWceACV9umEjJucmlpFe9FCMZSeqIqfkQ2BnhV9x"); + private final static @NonNull String keyConsumerPublic = CryptUtils.rot13("ESnsCvAv3kEupF1GCR3jGj"); + private final static @NonNull String keyConsumerSecret = CryptUtils.rot13("7vQWceACV9umEjJucmlpFe9FCMZSeqIqfkQ2BnhV9x"); public enum CoordInputFormatEnum { Plain, @@ -313,16 +313,16 @@ public class Settings { return getBoolean(R.string.pref_connectorOXActive, false); } - public static boolean isPremiumMember() { + public static boolean isGCPremiumMember() { // Basic Member, Premium Member, ??? - return GCConstants.MEMBER_STATUS_PM.equalsIgnoreCase(Settings.getMemberStatus()); + return GCConstants.MEMBER_STATUS_PM.equalsIgnoreCase(Settings.getGCMemberStatus()); } - public static String getMemberStatus() { + public static String getGCMemberStatus() { return getString(R.string.pref_memberstatus, ""); } - public static boolean setMemberStatus(final String memberStatus) { + public static boolean setGCMemberStatus(final String memberStatus) { if (StringUtils.isBlank(memberStatus)) { return remove(R.string.pref_memberstatus); } @@ -478,7 +478,7 @@ public class Settings { } public static boolean getLoadDirImg() { - return !isPremiumMember() && getBoolean(R.string.pref_loaddirectionimg, true); + return !isGCPremiumMember() && getBoolean(R.string.pref_loaddirectionimg, true); } public static void setGcCustomDate(final String format) { @@ -506,7 +506,7 @@ public class Settings { } public static boolean isShowCaptcha() { - return !isPremiumMember() && getBoolean(R.string.pref_showcaptcha, false); + return !isGCPremiumMember() && getBoolean(R.string.pref_showcaptcha, false); } public static boolean isExcludeDisabledCaches() { @@ -694,10 +694,12 @@ public class Settings { return getBoolean(R.string.pref_skin, false); } + @NonNull public static String getKeyConsumerPublic() { return keyConsumerPublic; } + @NonNull public static String getKeyConsumerSecret() { return keyConsumerSecret; } diff --git a/main/src/cgeo/geocaching/settings/SettingsActivity.java b/main/src/cgeo/geocaching/settings/SettingsActivity.java index 58acfc1..bcf6715 100644 --- a/main/src/cgeo/geocaching/settings/SettingsActivity.java +++ b/main/src/cgeo/geocaching/settings/SettingsActivity.java @@ -1,6 +1,7 @@ package cgeo.geocaching.settings; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.DataStore; import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.SelectMapfileActivity; @@ -19,8 +20,10 @@ import cgeo.geocaching.utils.Log; import org.apache.commons.lang3.StringUtils; import org.openintents.intents.FileManagerIntents; +import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -52,8 +55,7 @@ import java.util.Locale; */ public class SettingsActivity extends PreferenceActivity { - private static final String INTENT_GOTO = "GOTO"; - private static final int INTENT_GOTO_SERVICES = 1; + private static final String INTENT_OPEN_SCREEN = "OPEN_SCREEN"; /** * Enumeration for directory choosers. This is how we can retrieve information about the @@ -89,18 +91,21 @@ public class SettingsActivity extends PreferenceActivity { initPreferences(); Intent intent = getIntent(); - int gotoPage = intent.getIntExtra(INTENT_GOTO, 0); - if (gotoPage == INTENT_GOTO_SERVICES) { - // start with services screen - PreferenceScreen main = (PreferenceScreen) getPreference(R.string.pref_fakekey_main_screen); - try { - if (main != null) { - int index = getPreference(R.string.pref_fakekey_services_screen).getOrder(); - main.onItemClick(null, null, index, 0); - } - } catch (RuntimeException e) { - Log.e("could not open services preferences", e); - } + openInitialScreen(intent.getIntExtra(INTENT_OPEN_SCREEN, 0)); + } + + private void openInitialScreen(int initialScreen) { + if (initialScreen == 0) { + return; + } + PreferenceScreen screen = (PreferenceScreen) getPreference(initialScreen); + if (screen == null) { + return; + } + try { + setPreferenceScreen(screen); + } catch (RuntimeException e) { + Log.e("could not open preferences " + initialScreen, e); } } @@ -121,6 +126,7 @@ public class SettingsActivity extends PreferenceActivity { initSend2CgeoPreferences(); initServicePreferences(); initNavigationMenuPreferences(); + initMaintenanceButtons(); for (int k : new int[] { R.string.pref_username, R.string.pref_password, R.string.pref_pass_vote, R.string.pref_signature, @@ -141,9 +147,9 @@ public class SettingsActivity extends PreferenceActivity { getPreference(appEnum.preferenceKey).setEnabled(true); } } - getPreference(R.string.pref_fakekey_basicmembers_screen) - .setEnabled(!Settings.isPremiumMember()); - redrawScreen(R.string.pref_fakekey_navigation_menu_screen); + getPreference(R.string.preference_screen_basicmembers) + .setEnabled(!Settings.isGCPremiumMember()); + redrawScreen(R.string.preference_screen_navigation_menu); } private void initServicePreferences() { @@ -310,6 +316,35 @@ public class SettingsActivity extends PreferenceActivity { }); } + public void initMaintenanceButtons() { + Preference dirMaintenance = getPreference(R.string.pref_fakekey_preference_maintenance_directories); + dirMaintenance.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(final Preference preference) { + // disable the button, as the cleanup runs in background and should not be invoked a second time + preference.setEnabled(false); + + Resources res = getResources(); + final SettingsActivity activity = SettingsActivity.this; + final ProgressDialog dialog = ProgressDialog.show(activity, res.getString(R.string.init_maintenance), res.getString(R.string.init_maintenance_directories), true, false); + new Thread() { + @Override + public void run() { + DataStore.removeObsoleteCacheDirectories(); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + dialog.dismiss(); + } + }); + } + }.start(); + + return true; + } + }); + } + private void initDbLocationPreference() { Preference p = getPreference(R.string.pref_dbonsdcard); p.setPersistent(false); @@ -336,21 +371,29 @@ public class SettingsActivity extends PreferenceActivity { } void initBasicMemberPreferences() { - getPreference(R.string.pref_fakekey_basicmembers_screen) - .setEnabled(!Settings.isPremiumMember()); + getPreference(R.string.preference_screen_basicmembers) + .setEnabled(!Settings.isGCPremiumMember()); getPreference(R.string.pref_loaddirectionimg) - .setEnabled(!Settings.isPremiumMember()); + .setEnabled(!Settings.isGCPremiumMember()); getPreference(R.string.pref_showcaptcha) - .setEnabled(!Settings.isPremiumMember()); + .setEnabled(!Settings.isGCPremiumMember()); - redrawScreen(R.string.pref_fakekey_services_screen); + redrawScreen(R.string.preference_screen_services); } - void redrawScreen(int key) { - PreferenceScreen screen = (PreferenceScreen) getPreference(key); - if (screen == null) { + /** + * Refresh a preference screen. Has no effect when called for a preference, that is not actually a preference + * screen. + * + * @param key + * Key of a preference screen. + */ + void redrawScreen(final int key) { + final Preference preference = getPreference(key); + if (!(preference instanceof PreferenceScreen)) { return; } + final PreferenceScreen screen = (PreferenceScreen) preference; ListAdapter adapter = screen.getRootAdapter(); if (adapter instanceof BaseAdapter) { ((BaseAdapter) adapter).notifyDataSetChanged(); @@ -399,9 +442,9 @@ public class SettingsActivity extends PreferenceActivity { : R.string.settings_authorize)); } - public static void jumpToServicesPage(final Context fromActivity) { + public static void openForScreen(final int preferenceScreenKey, final Context fromActivity) { final Intent intent = new Intent(fromActivity, SettingsActivity.class); - intent.putExtra(INTENT_GOTO, INTENT_GOTO_SERVICES); + intent.putExtra(INTENT_OPEN_SCREEN, preferenceScreenKey); fromActivity.startActivity(intent); } @@ -445,15 +488,15 @@ public class SettingsActivity extends PreferenceActivity { break; case R.string.pref_fakekey_ocde_authorization: setOCDEAuthTitle(); - redrawScreen(R.string.pref_fakekey_services_screen); + redrawScreen(R.string.preference_screen_ocde); break; case R.string.pref_fakekey_ocpl_authorization: setOCPLAuthTitle(); - redrawScreen(R.string.pref_fakekey_services_screen); + redrawScreen(R.string.preference_screen_ocpl); break; case R.string.pref_fakekey_twitter_authorization: setTwitterAuthTitle(); - redrawScreen(R.string.pref_fakekey_services_screen); + redrawScreen(R.string.preference_screen_twitter); break; default: throw new IllegalArgumentException(); @@ -582,6 +625,13 @@ public class SettingsActivity extends PreferenceActivity { preferenceActivity.addPreferencesFromResource(preferencesResId); } + @SuppressWarnings("deprecation") + @Override + public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + // TODO replace with fragment based code + super.setPreferenceScreen(preferenceScreen); + } + private static boolean isPreference(final Preference preference, int preferenceKeyId) { return getKey(preferenceKeyId).equals(preference.getKey()); } diff --git a/main/src/cgeo/geocaching/settings/TemplateTextPreference.java b/main/src/cgeo/geocaching/settings/TemplateTextPreference.java index a703231..667b02b 100644 --- a/main/src/cgeo/geocaching/settings/TemplateTextPreference.java +++ b/main/src/cgeo/geocaching/settings/TemplateTextPreference.java @@ -2,6 +2,7 @@ package cgeo.geocaching.settings; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; +import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.LogTemplateProvider; import cgeo.geocaching.utils.LogTemplateProvider.LogTemplate; @@ -49,6 +50,7 @@ public class TemplateTextPreference extends DialogPreference { editText = (EditText) view.findViewById(R.id.signature_dialog_text); editText.setText(getPersistedString(initialValue != null ? initialValue : StringUtils.EMPTY)); + Dialogs.moveCursorToEnd(editText); Button button = (Button) view.findViewById(R.id.signature_templates); button.setOnClickListener(new View.OnClickListener() { diff --git a/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java b/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java index a1c04a4..2b171b4 100644 --- a/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java +++ b/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java @@ -3,6 +3,7 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; import cgeo.geocaching.utils.Log; +import org.apache.commons.lang3.StringUtils; /** * abstract super implementation for all cache comparators @@ -13,25 +14,35 @@ public abstract class AbstractCacheComparator implements CacheComparator { @Override public final int compare(final Geocache cache1, final Geocache cache2) { try { - // first check that we have all necessary data for the comparison - if (!canCompare(cache1, cache2)) { - return 0; + final boolean canCompare1 = canCompare(cache1); + final boolean canCompare2 = canCompare(cache2); + if (!canCompare1) { + return canCompare2 ? 1 : fallbackToGeocode(cache1, cache2); } - return compareCaches(cache1, cache2); - } catch (Exception e) { + return canCompare2 ? compareCaches(cache1, cache2) : -1; + } catch (final Exception e) { Log.e("AbstractCacheComparator.compare", e); + // This may violate the Comparator interface if the exception is not systematic. + return fallbackToGeocode(cache1, cache2); } - return 0; + } + + private static int fallbackToGeocode(final Geocache cache1, final Geocache cache2) { + return StringUtils.defaultString(cache1.getGeocode()).compareToIgnoreCase(StringUtils.defaultString(cache2.getGeocode())); } /** - * Check necessary preconditions (like missing fields) before running the comparison itself - * - * @param cache1 - * @param cache2 - * @return + * Check necessary preconditions (like missing fields) before running the comparison itself. + * Caches not filling the conditions will be placed last, sorted by Geocode. + * + * The default returns <code>true</code> and can be overridden if needed in child classes. + * + * @param cache + * @return <code>true</code> if the cache holds the necessary data to be compared meaningfully */ - protected abstract boolean canCompare(final Geocache cache1, final Geocache cache2); + protected boolean canCompare(final Geocache cache) { + return true; + } /** * Compares two caches. Logging and exception handling is implemented outside this method already. diff --git a/main/src/cgeo/geocaching/sorting/CacheComparator.java b/main/src/cgeo/geocaching/sorting/CacheComparator.java index 7932729..b06a4b0 100644 --- a/main/src/cgeo/geocaching/sorting/CacheComparator.java +++ b/main/src/cgeo/geocaching/sorting/CacheComparator.java @@ -1,9 +1,9 @@ package cgeo.geocaching.sorting; -import java.util.Comparator; - import cgeo.geocaching.Geocache; +import java.util.Comparator; + public interface CacheComparator extends Comparator<Geocache> { } diff --git a/main/src/cgeo/geocaching/sorting/ComparatorUserInterface.java b/main/src/cgeo/geocaching/sorting/ComparatorUserInterface.java index 99a535a..7f10353 100644 --- a/main/src/cgeo/geocaching/sorting/ComparatorUserInterface.java +++ b/main/src/cgeo/geocaching/sorting/ComparatorUserInterface.java @@ -2,7 +2,8 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.R; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RunnableWithArgument; + +import rx.functions.Action1; import android.app.Activity; import android.app.AlertDialog; @@ -70,7 +71,7 @@ public class ComparatorUserInterface { registry.add(new ComparatorEntry(res.getString(resourceId), comparatorClass)); } - public void selectComparator(final CacheComparator current, final RunnableWithArgument<CacheComparator> runAfterwards) { + public void selectComparator(final CacheComparator current, final Action1<CacheComparator> runAfterwards) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.caches_sort_title); @@ -85,11 +86,11 @@ public class ComparatorUserInterface { ComparatorEntry entry = registry.get(itemIndex); try { if (entry.cacheComparator == null) { - runAfterwards.run(null); + runAfterwards.call(null); } else { CacheComparator comparator = entry.cacheComparator.newInstance(); - runAfterwards.run(comparator); + runAfterwards.call(comparator); } } catch (InstantiationException e) { Log.e("selectComparator", e); diff --git a/main/src/cgeo/geocaching/sorting/DateComparator.java b/main/src/cgeo/geocaching/sorting/DateComparator.java index 091f6a4..9df70f9 100644 --- a/main/src/cgeo/geocaching/sorting/DateComparator.java +++ b/main/src/cgeo/geocaching/sorting/DateComparator.java @@ -1,7 +1,7 @@ package cgeo.geocaching.sorting; -import cgeo.geocaching.Geocache; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.Geocache; import java.util.ArrayList; import java.util.Date; @@ -12,11 +12,6 @@ import java.util.Date; public class DateComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return true; - } - - @Override protected int compareCaches(Geocache cache1, Geocache cache2) { final Date date1 = cache1.getHiddenDate(); final Date date2 = cache2.getHiddenDate(); diff --git a/main/src/cgeo/geocaching/sorting/DifficultyComparator.java b/main/src/cgeo/geocaching/sorting/DifficultyComparator.java index 73d12fa..459f38d 100644 --- a/main/src/cgeo/geocaching/sorting/DifficultyComparator.java +++ b/main/src/cgeo/geocaching/sorting/DifficultyComparator.java @@ -9,8 +9,8 @@ import cgeo.geocaching.Geocache; public class DifficultyComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return cache1.getDifficulty() != 0.0 && cache2.getDifficulty() != 0.0; + protected boolean canCompare(Geocache cache) { + return cache.getDifficulty() != 0.0; } @Override diff --git a/main/src/cgeo/geocaching/sorting/DistanceComparator.java b/main/src/cgeo/geocaching/sorting/DistanceComparator.java index 731e356..541ce48 100644 --- a/main/src/cgeo/geocaching/sorting/DistanceComparator.java +++ b/main/src/cgeo/geocaching/sorting/DistanceComparator.java @@ -36,11 +36,6 @@ public class DistanceComparator extends AbstractCacheComparator { } @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { calculateAllDistances(); final Float distance1 = cache1.getDistance(); diff --git a/main/src/cgeo/geocaching/sorting/FindsComparator.java b/main/src/cgeo/geocaching/sorting/FindsComparator.java index c889776..7f2ef50 100644 --- a/main/src/cgeo/geocaching/sorting/FindsComparator.java +++ b/main/src/cgeo/geocaching/sorting/FindsComparator.java @@ -5,8 +5,8 @@ import cgeo.geocaching.Geocache; public class FindsComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return cache1.getLogCounts() != null && cache2.getLogCounts() != null; + protected boolean canCompare(Geocache cache) { + return cache.getLogCounts() != null; } @Override diff --git a/main/src/cgeo/geocaching/sorting/GeocodeComparator.java b/main/src/cgeo/geocaching/sorting/GeocodeComparator.java index fff26c6..e700f13 100644 --- a/main/src/cgeo/geocaching/sorting/GeocodeComparator.java +++ b/main/src/cgeo/geocaching/sorting/GeocodeComparator.java @@ -2,23 +2,20 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; -import org.apache.commons.lang3.StringUtils; - /** * sorts caches by geo code, therefore effectively sorting by cache age - * + * */ public class GeocodeComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return StringUtils.isNotBlank(cache1.getGeocode()) - && StringUtils.isNotBlank(cache2.getGeocode()); + protected boolean canCompare(final Geocache cache) { + // This will fall back to geocode comparisons. + return false; } @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { - final int lengthDiff = cache1.getGeocode().length() - cache2.getGeocode().length(); - return lengthDiff != 0 ? lengthDiff : cache1.getGeocode().compareToIgnoreCase(cache2.getGeocode()); + throw new RuntimeException("should never be called"); } } diff --git a/main/src/cgeo/geocaching/sorting/InventoryComparator.java b/main/src/cgeo/geocaching/sorting/InventoryComparator.java index 73ea2c5..9d19b64 100644 --- a/main/src/cgeo/geocaching/sorting/InventoryComparator.java +++ b/main/src/cgeo/geocaching/sorting/InventoryComparator.java @@ -8,11 +8,6 @@ import cgeo.geocaching.Geocache; public class InventoryComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { return cache2.getInventoryItems() - cache1.getInventoryItems(); } diff --git a/main/src/cgeo/geocaching/sorting/NameComparator.java b/main/src/cgeo/geocaching/sorting/NameComparator.java index b432ad0..2941b1c 100644 --- a/main/src/cgeo/geocaching/sorting/NameComparator.java +++ b/main/src/cgeo/geocaching/sorting/NameComparator.java @@ -11,8 +11,8 @@ import org.apache.commons.lang3.StringUtils; public class NameComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return StringUtils.isNotBlank(cache1.getName()) && StringUtils.isNotBlank(cache2.getName()); + protected boolean canCompare(Geocache cache) { + return StringUtils.isNotBlank(cache.getName()); } @Override diff --git a/main/src/cgeo/geocaching/sorting/PopularityComparator.java b/main/src/cgeo/geocaching/sorting/PopularityComparator.java index e256654..2dbee68 100644 --- a/main/src/cgeo/geocaching/sorting/PopularityComparator.java +++ b/main/src/cgeo/geocaching/sorting/PopularityComparator.java @@ -9,11 +9,6 @@ import cgeo.geocaching.Geocache; public class PopularityComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { return cache2.getFavoritePoints() - cache1.getFavoritePoints(); } diff --git a/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java b/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java index f438762..1ed8e68 100644 --- a/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java +++ b/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java @@ -11,11 +11,6 @@ import cgeo.geocaching.Geocache; public class PopularityRatioComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { float ratio1 = 0.0f; diff --git a/main/src/cgeo/geocaching/sorting/RatingComparator.java b/main/src/cgeo/geocaching/sorting/RatingComparator.java index 72cf6c8..6f2c615 100644 --- a/main/src/cgeo/geocaching/sorting/RatingComparator.java +++ b/main/src/cgeo/geocaching/sorting/RatingComparator.java @@ -9,11 +9,6 @@ import cgeo.geocaching.Geocache; public class RatingComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { final float rating1 = cache1.getRating(); final float rating2 = cache2.getRating(); diff --git a/main/src/cgeo/geocaching/sorting/SizeComparator.java b/main/src/cgeo/geocaching/sorting/SizeComparator.java index d128822..c8de586 100644 --- a/main/src/cgeo/geocaching/sorting/SizeComparator.java +++ b/main/src/cgeo/geocaching/sorting/SizeComparator.java @@ -9,8 +9,8 @@ import cgeo.geocaching.Geocache; public class SizeComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return cache1.getSize() != null && cache2.getSize() != null; + protected boolean canCompare(Geocache cache) { + return cache.getSize() != null; } @Override diff --git a/main/src/cgeo/geocaching/sorting/StateComparator.java b/main/src/cgeo/geocaching/sorting/StateComparator.java index b99c3c0..9488bd9 100644 --- a/main/src/cgeo/geocaching/sorting/StateComparator.java +++ b/main/src/cgeo/geocaching/sorting/StateComparator.java @@ -9,11 +9,6 @@ import cgeo.geocaching.Geocache; public class StateComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { return getState(cache1) - getState(cache2); } diff --git a/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java b/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java index 78ba742..b718d3b 100644 --- a/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java +++ b/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java @@ -5,11 +5,6 @@ import cgeo.geocaching.Geocache; public class StorageTimeComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return true; - } - - @Override protected int compareCaches(Geocache cache1, Geocache cache2) { if (cache1.getUpdated() < cache2.getUpdated()) { return -1; diff --git a/main/src/cgeo/geocaching/sorting/TerrainComparator.java b/main/src/cgeo/geocaching/sorting/TerrainComparator.java index be1e9bb..9bbb5f7 100644 --- a/main/src/cgeo/geocaching/sorting/TerrainComparator.java +++ b/main/src/cgeo/geocaching/sorting/TerrainComparator.java @@ -9,8 +9,8 @@ import cgeo.geocaching.Geocache; public class TerrainComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return cache1.getTerrain() != 0.0 && cache2.getTerrain() != 0.0; + protected boolean canCompare(final Geocache cache) { + return cache.getTerrain() != 0.0; } @Override diff --git a/main/src/cgeo/geocaching/sorting/VisitComparator.java b/main/src/cgeo/geocaching/sorting/VisitComparator.java index 27d3170..1589a4c 100644 --- a/main/src/cgeo/geocaching/sorting/VisitComparator.java +++ b/main/src/cgeo/geocaching/sorting/VisitComparator.java @@ -9,11 +9,6 @@ import cgeo.geocaching.Geocache; public class VisitComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(final Geocache cache1, final Geocache cache2) { - return true; - } - - @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { return Long.valueOf(cache2.getVisitedDate()).compareTo(cache1.getVisitedDate()); } diff --git a/main/src/cgeo/geocaching/sorting/VoteComparator.java b/main/src/cgeo/geocaching/sorting/VoteComparator.java index dc0304b..cd4ad7e 100644 --- a/main/src/cgeo/geocaching/sorting/VoteComparator.java +++ b/main/src/cgeo/geocaching/sorting/VoteComparator.java @@ -8,11 +8,6 @@ import cgeo.geocaching.Geocache; public class VoteComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache1, Geocache cache2) { - return true; - } - - @Override protected int compareCaches(Geocache cache1, Geocache cache2) { // if there is no vote available, put that cache at the end of the list return Float.compare(cache2.getMyVote(), cache1.getMyVote()); diff --git a/main/src/cgeo/geocaching/speech/SpeechService.java b/main/src/cgeo/geocaching/speech/SpeechService.java index 2a72bbf..8c650c3 100644 --- a/main/src/cgeo/geocaching/speech/SpeechService.java +++ b/main/src/cgeo/geocaching/speech/SpeechService.java @@ -47,7 +47,7 @@ public class SpeechService extends Service implements OnInitListener { GeoDirHandler geoHandler = new GeoDirHandler() { @Override - protected void updateDirection(float newDirection) { + public void updateDirection(float newDirection) { if (CgeoApplication.getInstance().currentGeo().getSpeed() <= 5) { direction = DirectionProvider.getDirectionNow(startingActivity, newDirection); directionInitialized = true; @@ -56,7 +56,7 @@ public class SpeechService extends Service implements OnInitListener { } @Override - protected void updateGeoData(cgeo.geocaching.IGeoData newGeo) { + public void updateGeoData(cgeo.geocaching.IGeoData newGeo) { position = newGeo.getCoords(); positionInitialized = true; if (!Settings.isUseCompass() || newGeo.getSpeed() > 5) { diff --git a/main/src/cgeo/geocaching/speech/TextFactory.java b/main/src/cgeo/geocaching/speech/TextFactory.java index 2a3b6d7..eb780c6 100644 --- a/main/src/cgeo/geocaching/speech/TextFactory.java +++ b/main/src/cgeo/geocaching/speech/TextFactory.java @@ -2,9 +2,9 @@ package cgeo.geocaching.speech; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; -import cgeo.geocaching.settings.Settings; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.geopoint.IConversion; +import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.AngleUtils; import java.util.Locale; diff --git a/main/src/cgeo/geocaching/twitter/Twitter.java b/main/src/cgeo/geocaching/twitter/Twitter.java index 51cf6e2..c89c0b6 100644 --- a/main/src/cgeo/geocaching/twitter/Twitter.java +++ b/main/src/cgeo/geocaching/twitter/Twitter.java @@ -17,7 +17,6 @@ import cgeo.geocaching.utils.LogTemplateProvider; import cgeo.geocaching.utils.LogTemplateProvider.LogContext; import ch.boye.httpclientandroidlib.HttpResponse; - import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; diff --git a/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java b/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java index ed5d182..0c67384 100644 --- a/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java +++ b/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java @@ -3,7 +3,6 @@ package cgeo.geocaching.ui; import cgeo.geocaching.activity.AbstractViewPagerActivity.PageViewCreator; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; diff --git a/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java b/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java index db82e5c..d4c2e10 100644 --- a/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java +++ b/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java @@ -8,7 +8,7 @@ import android.widget.TextView; /** * <code>LinkMovementMethod</code> with built-in suppression of errors for links, where the URL cannot be handled * correctly by Android. - * + * */ public class AnchorAwareLinkMovementMethod extends LinkMovementMethod { diff --git a/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java b/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java index 7fe77c4..5d8ebef 100644 --- a/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java +++ b/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java @@ -39,6 +39,11 @@ public final class CacheDetailsCreator { parentView.removeAllViews(); } + /** + * @param nameId + * @param value + * @return the view containing the displayed string (i.e. the right side one from the pair of "label": "value") + */ public TextView add(final int nameId, final CharSequence value) { final RelativeLayout layout = (RelativeLayout) activity.getLayoutInflater().inflate(R.layout.cache_information_item, null); final TextView nameView = (TextView) layout.findViewById(R.id.name); @@ -188,14 +193,24 @@ public final class CacheDetailsCreator { if (!cache.isEventCache()) { return; } + addHiddenDate(cache); + } + + public TextView addHiddenDate(final @NonNull Geocache cache) { final Date hiddenDate = cache.getHiddenDate(); if (hiddenDate == null) { - return; + return null; } final long time = hiddenDate.getTime(); if (time > 0) { - final String dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + Formatter.formatFullDate(time); - add(R.string.cache_event, dateString); + String dateString = Formatter.formatFullDate(time); + if (cache.isEventCache()) { + dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString; + } + final TextView view = add(cache.isEventCache() ? R.string.cache_event : R.string.cache_hidden, dateString); + view.setId(R.id.date); + return view; } + return null; } } diff --git a/main/src/cgeo/geocaching/ui/EditNoteDialog.java b/main/src/cgeo/geocaching/ui/EditNoteDialog.java index 2af1cb8..63f06fc 100644 --- a/main/src/cgeo/geocaching/ui/EditNoteDialog.java +++ b/main/src/cgeo/geocaching/ui/EditNoteDialog.java @@ -1,12 +1,17 @@ package cgeo.geocaching.ui; import cgeo.geocaching.R; +import cgeo.geocaching.activity.Keyboard; +import cgeo.geocaching.ui.dialog.Dialogs; + +import org.eclipse.jdt.annotation.NonNull; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; import android.view.ContextThemeWrapper; import android.view.View; import android.widget.EditText; @@ -35,15 +40,17 @@ public class EditNoteDialog extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - View view = View.inflate(new ContextThemeWrapper(getActivity(), R.style.dark), R.layout.fragment_edit_note, null); + final @NonNull FragmentActivity activity = getActivity(); + View view = View.inflate(new ContextThemeWrapper(activity, R.style.dark), R.layout.fragment_edit_note, null); mEditText = (EditText) view.findViewById(R.id.note); String initialNote = getArguments().getString(ARGUMENT_INITIAL_NOTE); if (initialNote != null) { mEditText.setText(initialNote); + Dialogs.moveCursorToEnd(mEditText); getArguments().remove(ARGUMENT_INITIAL_NOTE); } - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.cache_personal_note); builder.setView(view); builder.setPositiveButton(android.R.string.ok, @@ -61,6 +68,8 @@ public class EditNoteDialog extends DialogFragment { dialog.dismiss(); } }); - return builder.create(); + final AlertDialog dialog = builder.create(); + new Keyboard(activity).showDelayed(mEditText); + return dialog; } } diff --git a/main/src/cgeo/geocaching/ui/ImagesList.java b/main/src/cgeo/geocaching/ui/ImagesList.java index 4eaf06d..dcce969 100644 --- a/main/src/cgeo/geocaching/ui/ImagesList.java +++ b/main/src/cgeo/geocaching/ui/ImagesList.java @@ -9,6 +9,12 @@ import cgeo.geocaching.utils.Log; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import rx.Subscription; +import rx.android.observables.AndroidObservable; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; import android.app.Activity; import android.content.Intent; @@ -18,14 +24,12 @@ import android.graphics.Bitmap.CompressFormat; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; -import android.os.AsyncTask; import android.text.Html; import android.util.SparseArray; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup.LayoutParams; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -44,8 +48,7 @@ public class ImagesList { public enum ImageType { LogImages(R.string.cache_log_images_title), - SpoilerImages(R.string.cache_spoiler_images_title), - AllImages(R.string.cache_images_title); + SpoilerImages(R.string.cache_spoiler_images_title); private final int titleResId; @@ -75,12 +78,31 @@ public class ImagesList { inflater = activity.getLayoutInflater(); } - public void loadImages(final View parentView, final List<Image> images, final boolean offline) { + /** + * Load images into a view. + * + * @param parentView a view to load the images into + * @param images the images to load + * @param offline <tt>true</tt> if the images must be stored for offline use + * @return a subscription which, when unsubscribed, interrupts the loading and clears up resources + */ + public Subscription loadImages(final View parentView, final List<Image> images, final boolean offline) { + // Start with a fresh subscription because of this method can be called several times if the + // englobing activity is stopped/restarted. + final CompositeSubscription subscriptions = new CompositeSubscription(Subscriptions.create(new Action0() { + @Override + public void call() { + removeAllViews(); + } + })); imagesView = (LinearLayout) parentView.findViewById(R.id.spoiler_list); + final HtmlImage imgGetter = new HtmlImage(geocode, true, offline ? StoredList.STANDARD_LIST_ID : StoredList.TEMPORARY_LIST_ID, false); + for (final Image img : images) { - LinearLayout rowView = (LinearLayout) inflater.inflate(R.layout.cache_image_item, null); + final LinearLayout rowView = (LinearLayout) inflater.inflate(R.layout.cache_image_item, null); + assert(rowView != null); if (StringUtils.isNotBlank(img.getTitle())) { ((TextView) rowView.findViewById(R.id.title)).setText(Html.fromHtml(img.getTitle())); @@ -93,66 +115,58 @@ public class ImagesList { descView.setVisibility(View.VISIBLE); } - new AsyncImgLoader(rowView, img, offline).execute(); + final ImageView imageView = (ImageView) inflater.inflate(R.layout.image_item, null); + assert(imageView != null); + subscriptions.add(AndroidObservable.fromActivity(activity, imgGetter.fetchDrawable(img.getUrl())) + .subscribe(new Action1<BitmapDrawable>() { + @Override + public void call(final BitmapDrawable image) { + display(imageView, image, img, rowView); + } + })); + rowView.addView(imageView); imagesView.addView(rowView); } + + return subscriptions; } - private class AsyncImgLoader extends AsyncTask<Void, Void, BitmapDrawable> { + private void display(final ImageView imageView, final BitmapDrawable image, final Image img, final LinearLayout view) { + if (image != null) { + bitmaps.add(image.getBitmap()); - final private LinearLayout view; - final private Image img; - final boolean offline; + final Rect bounds = image.getBounds(); - public AsyncImgLoader(final LinearLayout view, final Image img, final boolean offline) { - this.view = view; - this.img = img; - this.offline = offline; - } + imageView.setImageResource(R.drawable.image_not_loaded); + imageView.setClickable(true); + imageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + viewImageInStandardApp(image); + } + }); + activity.registerForContextMenu(imageView); + imageView.setImageDrawable(image); + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + imageView.setLayoutParams(new LinearLayout.LayoutParams(bounds.width(), bounds.height())); - @Override - protected BitmapDrawable doInBackground(Void... params) { - final HtmlImage imgGetter = new HtmlImage(geocode, true, offline ? StoredList.STANDARD_LIST_ID : StoredList.TEMPORARY_LIST_ID, false); - return imgGetter.getDrawable(img.getUrl()); - } + view.findViewById(R.id.progress_bar).setVisibility(View.GONE); - @Override - protected void onPostExecute(final BitmapDrawable image) { - if (image != null) { - bitmaps.add(image.getBitmap()); - final ImageView imageView = (ImageView) inflater.inflate(R.layout.image_item, null); - - final Rect bounds = image.getBounds(); - - imageView.setImageResource(R.drawable.image_not_loaded); - imageView.setClickable(true); - imageView.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View arg0) { - viewImageInStandardApp(image); - } - }); - activity.registerForContextMenu(imageView); - imageView.setImageDrawable(image); - imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - imageView.setLayoutParams(new LayoutParams(bounds.width(), bounds.height())); - - view.findViewById(R.id.progress_bar).setVisibility(View.GONE); - view.addView(imageView); - - imageView.setId(image.hashCode()); - images.put(imageView.getId(), img); - } + imageView.setId(image.hashCode()); + images.put(imageView.getId(), img); + + view.invalidate(); } } - public void removeAllViews() { - imagesView.removeAllViews(); + private void removeAllViews() { for (final Bitmap b : bitmaps) { b.recycle(); } bitmaps.clear(); + images.clear(); + + imagesView.removeAllViews(); } public void onCreateContextMenu(ContextMenu menu, View v) { diff --git a/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java b/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java index 93f50e1..651ff6e 100644 --- a/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java +++ b/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java @@ -54,7 +54,7 @@ public class CoordinatesInputDialog extends NoTitleDialog { } else if (geo != null && geo.getCoords() != null) { this.gp = geo.getCoords(); } else { - this.gp = new Geopoint(0.0, 0.0); + this.gp = Geopoint.ZERO; } } @@ -396,7 +396,7 @@ public class CoordinatesInputDialog extends NoTitleDialog { if (geo != null && geo.getCoords() != null) { gp = geo.getCoords(); } else { - gp = new Geopoint(0.0, 0.0); + gp = Geopoint.ZERO; } } } diff --git a/main/src/cgeo/geocaching/ui/dialog/Dialogs.java b/main/src/cgeo/geocaching/ui/dialog/Dialogs.java index 865ba70..cb8926a 100644 --- a/main/src/cgeo/geocaching/ui/dialog/Dialogs.java +++ b/main/src/cgeo/geocaching/ui/dialog/Dialogs.java @@ -1,20 +1,24 @@ package cgeo.geocaching.ui.dialog; import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.utils.RunnableWithArgument; +import cgeo.geocaching.R; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.Nullable; +import rx.functions.Action1; + import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; +import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; +import android.view.ContextThemeWrapper; import android.view.WindowManager; import android.widget.EditText; @@ -298,7 +302,7 @@ public final class Dialogs { /** * Show a message dialog for input from the user. The okay button is only enabled on non empty input. - * + * * @param context * activity owning the dialog * @param title @@ -310,19 +314,20 @@ public final class Dialogs { * @param okayListener * listener to be run on okay */ - public static void input(final Activity context, final int title, final String defaultValue, final int buttonTitle, final RunnableWithArgument<String> okayListener) { - final EditText input = new EditText(context); + public static void input(final Activity context, final int title, final String defaultValue, final int buttonTitle, final Action1<String> okayListener) { + final Context themedContext = new ContextThemeWrapper(context, R.style.dark); + final EditText input = new EditText(themedContext); input.setInputType(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_CLASS_TEXT); input.setText(defaultValue); - final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final AlertDialog.Builder builder = new AlertDialog.Builder(themedContext); builder.setTitle(title); builder.setView(input); builder.setPositiveButton(buttonTitle, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - okayListener.run(input.getText().toString()); + okayListener.call(input.getText().toString()); } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @@ -357,8 +362,16 @@ public final class Dialogs { dialog.show(); enableDialogButtonIfNotEmpty(dialog, defaultValue); - // position cursor after text - input.setSelection(input.getText().length()); + moveCursorToEnd(input); + } + + /** + * Move the cursor to the end of the input field. + * + * @param input + */ + public static void moveCursorToEnd(final EditText input) { + input.setSelection(input.getText().length(), input.getText().length()); } private static void enableDialogButtonIfNotEmpty(final AlertDialog dialog, final String input) { diff --git a/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java b/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java index 6ad59ec..c29f549 100644 --- a/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java +++ b/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java @@ -1,15 +1,14 @@ package cgeo.geocaching.ui.dialog; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.settings.Settings; -import cgeo.geocaching.CgeoApplication; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.view.ContextThemeWrapper; import android.view.View; -import android.widget.CheckBox; public class LiveMapInfoDialogBuilder { @@ -20,12 +19,7 @@ public class LiveMapInfoDialogBuilder { final View layout = View.inflate(new ContextThemeWrapper(activity, R.style.dark), R.layout.livemapinfo, null); builder.setView(layout); - final CheckBox checkBoxHide = (CheckBox) layout.findViewById(R.id.live_map_hint_hide); - final int showCount = Settings.getLiveMapHintShowCount(); - if (showCount > 2) { - checkBoxHide.setVisibility(View.VISIBLE); - } Settings.setLiveMapHintShowCount(showCount + 1); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @@ -33,10 +27,7 @@ public class LiveMapInfoDialogBuilder { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); - CgeoApplication.getInstance().setLiveMapHintShown(); - if (checkBoxHide.getVisibility() == View.VISIBLE && checkBoxHide.isChecked()) { - Settings.setHideLiveHint(true); - } + CgeoApplication.getInstance().setLiveMapHintShownInThisSession(); } }); return builder.create(); diff --git a/main/src/cgeo/geocaching/utils/CancellableHandler.java b/main/src/cgeo/geocaching/utils/CancellableHandler.java index cb4b9db..01fb568 100644 --- a/main/src/cgeo/geocaching/utils/CancellableHandler.java +++ b/main/src/cgeo/geocaching/utils/CancellableHandler.java @@ -2,6 +2,9 @@ package cgeo.geocaching.utils; import cgeo.geocaching.CgeoApplication; +import rx.Subscription; +import rx.subscriptions.CompositeSubscription; + import android.os.Handler; import android.os.Message; @@ -13,6 +16,7 @@ public abstract class CancellableHandler extends Handler { protected static final int UPDATE_LOAD_PROGRESS_DETAIL = 42186; private volatile boolean cancelled = false; + private static CompositeSubscription subscriptions = new CompositeSubscription(); private static class CancelHolder { final Object payload; @@ -30,6 +34,7 @@ public abstract class CancellableHandler extends Handler { if (message.obj instanceof CancelHolder) { cancelled = true; + subscriptions.unsubscribe(); handleCancel(((CancelHolder) message.obj).payload); } else { handleRegularMessage(message); @@ -37,6 +42,17 @@ public abstract class CancellableHandler extends Handler { } /** + * Add a subscription to the list of subscriptions to be subscribed at cancellation time. + */ + final public void unsubscribeIfCancelled(final Subscription subscription) { + subscriptions.add(subscription); + if (cancelled) { + // Protect against race conditions + subscriptions.unsubscribe(); + } + } + + /** * Handle a non-cancel message.<br> * Subclasses must implement this to handle messages. * diff --git a/main/src/cgeo/geocaching/utils/CryptUtils.java b/main/src/cgeo/geocaching/utils/CryptUtils.java index 5273fa5..80d841f 100644 --- a/main/src/cgeo/geocaching/utils/CryptUtils.java +++ b/main/src/cgeo/geocaching/utils/CryptUtils.java @@ -3,6 +3,7 @@ package cgeo.geocaching.utils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -66,6 +67,8 @@ public final class CryptUtils { } } + @SuppressWarnings("null") + @NonNull public static String rot13(String text) { if (text == null) { return StringUtils.EMPTY; diff --git a/main/src/cgeo/geocaching/utils/FileUtils.java b/main/src/cgeo/geocaching/utils/FileUtils.java index f650216..54553c2 100644 --- a/main/src/cgeo/geocaching/utils/FileUtils.java +++ b/main/src/cgeo/geocaching/utils/FileUtils.java @@ -1,12 +1,20 @@ package cgeo.geocaching.utils; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import android.os.Handler; import android.os.Message; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.util.List; /** @@ -126,4 +134,23 @@ public final class FileUtils { } return success; } + + public static boolean writeFileUTF16(File file, String content) { + // TODO: replace by some apache.commons IOUtils or FileUtils code + Writer fileWriter = null; + BufferedOutputStream buffer = null; + try { + final OutputStream os = new FileOutputStream(file); + buffer = new BufferedOutputStream(os); + fileWriter = new OutputStreamWriter(buffer, CharEncoding.UTF_16); + fileWriter.write(content.toString()); + } catch (final IOException e) { + Log.e("FieldnoteExport.ExportTask export", e); + return false; + } finally { + IOUtils.closeQuietly(fileWriter); + IOUtils.closeQuietly(buffer); + } + return true; + } } diff --git a/main/src/cgeo/geocaching/utils/GeoDirHandler.java b/main/src/cgeo/geocaching/utils/GeoDirHandler.java index c85648b..7050e61 100644 --- a/main/src/cgeo/geocaching/utils/GeoDirHandler.java +++ b/main/src/cgeo/geocaching/utils/GeoDirHandler.java @@ -4,12 +4,16 @@ import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.IGeoData; import cgeo.geocaching.settings.Settings; -import android.os.Handler; -import android.os.Message; +import rx.Scheduler; +import rx.Subscription; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; +import rx.functions.Action1; + +import java.util.concurrent.TimeUnit; /** - * GeoData and Direction handler. Manipulating geodata and direction information - * through a GeoDirHandler ensures that all listeners are registered from a {@link android.os.Looper} thread. + * GeoData and Direction handler. * <p> * To use this class, override at least one of {@link #updateDirection(float)} or {@link #updateGeoData(IGeoData)}. You * need to start the handler using one of @@ -21,47 +25,11 @@ import android.os.Message; * A good place might be the {@code onResume} method of the Activity. Stop the Handler accordingly in {@code onPause}. * </p> */ -public abstract class GeoDirHandler extends Handler implements IObserver<Object> { - - private static final int OBSERVABLE = 1 << 1; - private static final int START_GEO = 1 << 2; - private static final int START_DIR = 1 << 3; - private static final int STOP_GEO = 1 << 4; - private static final int STOP_DIR = 1 << 5; - +public abstract class GeoDirHandler { private static final CgeoApplication app = CgeoApplication.getInstance(); - @Override - final public void handleMessage(final Message message) { - if ((message.what & START_GEO) != 0) { - app.addGeoObserver(this); - } - - if ((message.what & START_DIR) != 0) { - app.addDirectionObserver(this); - } - - if ((message.what & STOP_GEO) != 0) { - app.deleteGeoObserver(this); - } - - if ((message.what & STOP_DIR) != 0) { - app.deleteDirectionObserver(this); - } - - if ((message.what & OBSERVABLE) != 0) { - if (message.obj instanceof IGeoData) { - updateGeoData((IGeoData) message.obj); - } else { - updateDirection((Float) message.obj); - } - } - } - - @Override - final public void update(final Object o) { - obtainMessage(OBSERVABLE, o).sendToTarget(); - } + private Subscription dirSubscription = null; + private Subscription geoSubscription = null; /** * Update method called when new IGeoData is available. @@ -69,7 +37,7 @@ public abstract class GeoDirHandler extends Handler implements IObserver<Object> * @param data * the new data */ - protected void updateGeoData(final IGeoData data) { + public void updateGeoData(final IGeoData data) { // Override this in children } @@ -79,24 +47,38 @@ public abstract class GeoDirHandler extends Handler implements IObserver<Object> * @param direction * the new direction */ - protected void updateDirection(final float direction) { + public void updateDirection(final float direction) { // Override this in children } /** * Register the current GeoDirHandler for GeoData information. */ - public void startGeo() { - sendEmptyMessage(START_GEO); + public synchronized void startGeo() { + geoSubscription = app.currentGeoObject() + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1<IGeoData>() { + @Override + public void call(final IGeoData geoData) { + updateGeoData(geoData); + } + }); } /** * Register the current GeoDirHandler for direction information if the preferences * allow it. */ - public void startDir() { + public synchronized void startDir() { if (Settings.isUseCompass()) { - sendEmptyMessage(START_DIR); + dirSubscription = app.currentDirObject() + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1<Float>() { + @Override + public void call(final Float direction) { + updateDirection(direction); + } + }); } } @@ -105,27 +87,43 @@ public abstract class GeoDirHandler extends Handler implements IObserver<Object> * preferences allow it). */ public void startGeoAndDir() { - sendEmptyMessage(START_GEO | (Settings.isUseCompass() ? START_DIR : 0)); + startGeo(); + startDir(); } /** * Unregister the current GeoDirHandler for GeoData information. */ - public void stopGeo() { - sendEmptyMessage(STOP_GEO); + public synchronized void stopGeo() { + // Delay the unsubscription by 2.5 seconds, so that another activity has + // the time to subscribe and the GPS receiver will not be turned down. + if (geoSubscription != null) { + final Subscription subscription = geoSubscription; + geoSubscription = null; + Schedulers.newThread().schedule(new Action1<Scheduler.Inner>() { + @Override + public void call(final Scheduler.Inner inner) { + subscription.unsubscribe(); + } + }, 2500, TimeUnit.MILLISECONDS); + } } /** * Unregister the current GeoDirHandler for direction information. */ - public void stopDir() { - sendEmptyMessage(STOP_DIR); + public synchronized void stopDir() { + if (dirSubscription != null) { + dirSubscription.unsubscribe(); + dirSubscription = null; + } } /** * Unregister the current GeoDirHandler for GeoData and direction information. */ public void stopGeoAndDir() { - sendEmptyMessage(STOP_GEO | STOP_DIR); + stopGeo(); + stopDir(); } } diff --git a/main/src/cgeo/geocaching/utils/IObserver.java b/main/src/cgeo/geocaching/utils/IObserver.java deleted file mode 100644 index bfcc798..0000000 --- a/main/src/cgeo/geocaching/utils/IObserver.java +++ /dev/null @@ -1,22 +0,0 @@ -package cgeo.geocaching.utils; - -/** - * Observer interface. - * <p/> - * An observer will receive updates about the observed object (implementing the {@link ISubject} interface) through its - * {@link #update(Object)} method. - * - * @param <T> - * the kind of data to observe - */ -public interface IObserver<T> { - - /** - * Called when an observed object has updated its data. - * - * @param data - * the updated data - */ - void update(final T data); - -} diff --git a/main/src/cgeo/geocaching/utils/ISubject.java b/main/src/cgeo/geocaching/utils/ISubject.java deleted file mode 100644 index c325db0..0000000 --- a/main/src/cgeo/geocaching/utils/ISubject.java +++ /dev/null @@ -1,57 +0,0 @@ -package cgeo.geocaching.utils; - -/** - * Interface for subjects objects. Those can be observed by objects implementing the {@link IObserver} interface. - * - * @param <T> - * the kind of data to observe - */ - -public interface ISubject<T> { - - /** - * Add an observer to the observers list. - * <p/> - * Observers will be notified with no particular order. - * - * @param observer - * the observer to add - * @return true if the observer has been added, false if it was present already - */ - public boolean addObserver(final IObserver<? super T> observer); - - /** - * Delete an observer from the observers list. - * - * @param observer - * the observer to remove - * @return true if the observer has been removed, false if it was not in the list of observers - */ - public boolean deleteObserver(final IObserver<? super T> observer); - - /** - * Number of observers currently observing the object. - * - * @return the number of observers - */ - public int sizeObservers(); - - /** - * Notify all the observers that new data is available. - * <p/> - * The {@link IObserver#update(Object)} method of each observer will be called with no particular order. - * - * @param data - * the updated data - * @return true if at least one observer was notified, false if there were no observers - */ - public boolean notifyObservers(final T data); - - /** - * Clear the observers list. - * - * @return true if there were observers before calling this method, false if the observers list was empty - */ - public boolean clearObservers(); - -} diff --git a/main/src/cgeo/geocaching/utils/ImageUtils.java b/main/src/cgeo/geocaching/utils/ImageUtils.java index 73f322c..9f47ead 100644 --- a/main/src/cgeo/geocaching/utils/ImageUtils.java +++ b/main/src/cgeo/geocaching/utils/ImageUtils.java @@ -78,7 +78,7 @@ public final class ImageUtils { * @return BitmapDrawable The scaled image */ @NonNull - public static BitmapDrawable scaleBitmapTo(@NonNull final Bitmap image, final int maxWidth, final int maxHeight) { + private static BitmapDrawable scaleBitmapTo(@NonNull final Bitmap image, final int maxWidth, final int maxHeight) { final CgeoApplication app = CgeoApplication.getInstance(); Bitmap result = image; int width = image.getWidth(); diff --git a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java index 708dff0..0c83076 100644 --- a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java +++ b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java @@ -1,5 +1,7 @@ package cgeo.geocaching.utils; +import org.eclipse.jdt.annotation.NonNull; + import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; @@ -42,6 +44,8 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E> * * @see HashSet */ + @SuppressWarnings("null") + @NonNull @Override public Iterator<E> iterator() { return map.keySet().iterator(); diff --git a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java index 76fa0f7..5fa0982 100644 --- a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java +++ b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java @@ -47,10 +47,6 @@ public final class LogTemplateProvider { this.logEntry = logEntry; } - public LogContext(final boolean offline) { - this(null, null, offline); - } - public LogContext(final Geocache cache, LogEntry logEntry, final boolean offline) { this.cache = cache; this.offline = offline; diff --git a/main/src/cgeo/geocaching/utils/MemorySubject.java b/main/src/cgeo/geocaching/utils/MemorySubject.java deleted file mode 100644 index c424528..0000000 --- a/main/src/cgeo/geocaching/utils/MemorySubject.java +++ /dev/null @@ -1,45 +0,0 @@ -package cgeo.geocaching.utils; - -/** - * Synchronized implementation of the {@link ISubject} interface with an added pull interface. - * - * @param <T> - * the kind of data to observe - */ -public class MemorySubject<T> extends Subject<T> { - - /** - * The latest version of the observed data. - * <p/> - * A child class implementation may want to set this field from its constructors, in case early observers request - * the data before it got a chance to get updated. Otherwise, <code>null</code> will be returned until updated - * data is available. - */ - private T memory; - - @Override - public synchronized boolean addObserver(final IObserver<? super T> observer) { - final boolean added = super.addObserver(observer); - if (added && memory != null) { - observer.update(memory); - } - return added; - } - - @Override - public synchronized boolean notifyObservers(final T data) { - memory = data; - return super.notifyObservers(data); - } - - /** - * Get the memorized version of the data. - * - * @return the initial data set by the subject (which may be <code>null</code>), - * or the updated data if it is available - */ - public synchronized T getMemory() { - return memory; - } - -} diff --git a/main/src/cgeo/geocaching/utils/RunnableWithArgument.java b/main/src/cgeo/geocaching/utils/RunnableWithArgument.java deleted file mode 100644 index 6137efd..0000000 --- a/main/src/cgeo/geocaching/utils/RunnableWithArgument.java +++ /dev/null @@ -1,7 +0,0 @@ -package cgeo.geocaching.utils; - -public interface RunnableWithArgument<T> { - - abstract void run(final T argument); - -} diff --git a/main/src/cgeo/geocaching/utils/Subject.java b/main/src/cgeo/geocaching/utils/Subject.java deleted file mode 100644 index b1754cc..0000000 --- a/main/src/cgeo/geocaching/utils/Subject.java +++ /dev/null @@ -1,76 +0,0 @@ -package cgeo.geocaching.utils; - -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * Synchronized implementation of the {@link ISubject} interface. - * - * @param <T> - * the kind of data to observe - */ -public class Subject<T> implements ISubject<T> { - - /** - * Collection of observers. - */ - protected final Set<IObserver<? super T>> observers = new LinkedHashSet<IObserver<? super T>>(); - - @Override - public synchronized boolean addObserver(final IObserver<? super T> observer) { - final boolean added = observers.add(observer); - if (added && observers.size() == 1) { - onFirstObserver(); - } - return added; - } - - @Override - public synchronized boolean deleteObserver(final IObserver<? super T> observer) { - final boolean removed = observers.remove(observer); - if (removed && observers.isEmpty()) { - onLastObserver(); - } - return removed; - } - - @Override - public synchronized boolean notifyObservers(final T arg) { - final boolean nonEmpty = !observers.isEmpty(); - for (final IObserver<? super T> observer : observers) { - observer.update(arg); - } - return nonEmpty; - } - - @Override - public synchronized int sizeObservers() { - return observers.size(); - } - - @Override - public synchronized boolean clearObservers() { - final boolean nonEmpty = !observers.isEmpty(); - for (final IObserver<? super T> observer : observers) { - deleteObserver(observer); - } - return nonEmpty; - } - - /** - * Method called when the collection of observers goes from empty to non-empty. - * <p/> - * The default implementation does nothing and may be overwritten by child classes. - */ - protected void onFirstObserver() { - } - - /** - * Method called when the collection of observers goes from non-empty to empty. - * <p/> - * The default implementation does nothing and may be overwritten by child classes. - */ - protected void onLastObserver() { - } - -} diff --git a/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java b/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java index 2368469..7848d1a 100644 --- a/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java +++ b/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java @@ -4,6 +4,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.TimeZone; public class SynchronizedDateFormat { private final SimpleDateFormat format; @@ -12,7 +13,17 @@ public class SynchronizedDateFormat { format = new SimpleDateFormat(pattern, locale); } + public SynchronizedDateFormat(String pattern, TimeZone timeZone, Locale locale) { + format = new SimpleDateFormat(pattern, locale); + format.setTimeZone(timeZone); + } + public synchronized Date parse(final String input) throws ParseException { return format.parse(input); } + + public synchronized String format(final Date date) { + return format.format(date); + } + } diff --git a/main/src/cgeo/geocaching/utils/TextUtils.java b/main/src/cgeo/geocaching/utils/TextUtils.java index efbb2d7..c4e1128 100644 --- a/main/src/cgeo/geocaching/utils/TextUtils.java +++ b/main/src/cgeo/geocaching/utils/TextUtils.java @@ -4,7 +4,6 @@ package cgeo.geocaching.utils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - import org.eclipse.jdt.annotation.Nullable; import java.util.regex.Matcher; |
