diff options
Diffstat (limited to 'main/src')
75 files changed, 1943 insertions, 1258 deletions
diff --git a/main/src/cgeo/geocaching/AbstractLoggingActivity.java b/main/src/cgeo/geocaching/AbstractLoggingActivity.java index 4641d3a..15e8848 100644 --- a/main/src/cgeo/geocaching/AbstractLoggingActivity.java +++ b/main/src/cgeo/geocaching/AbstractLoggingActivity.java @@ -19,11 +19,6 @@ import android.widget.EditText; public abstract class AbstractLoggingActivity extends AbstractActionBarActivity { - /** - * sub classes can disable the send button - */ - private boolean enableSend = true; - @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.abstract_logging_activity, menu); @@ -54,7 +49,6 @@ public abstract class AbstractLoggingActivity extends AbstractActionBarActivity } menu.findItem(R.id.menu_smilies).setVisible(smileyVisible); - menu.findItem(R.id.menu_send).setVisible(enableSend); return true; } @@ -85,11 +79,6 @@ public abstract class AbstractLoggingActivity extends AbstractActionBarActivity ActivityMixin.insertAtPosition(log, newText, moveCursor); } - protected final void setLoggingEnabled(final boolean enabled) { - enableSend = enabled; - invalidateOptionsMenuCompatible(); - } - protected void requestKeyboardForLogging() { new Keyboard(this).show(findViewById(R.id.log)); } diff --git a/main/src/cgeo/geocaching/CacheDetailActivity.java b/main/src/cgeo/geocaching/CacheDetailActivity.java index dca0a43..a59d9b9 100644 --- a/main/src/cgeo/geocaching/CacheDetailActivity.java +++ b/main/src/cgeo/geocaching/CacheDetailActivity.java @@ -5,6 +5,7 @@ import butterknife.InjectView; import cgeo.calendar.CalendarAddon; import cgeo.geocaching.activity.AbstractActivity; +import cgeo.geocaching.activity.AbstractActivity.ActivitySharingInterface; import cgeo.geocaching.activity.AbstractViewPagerActivity; import cgeo.geocaching.activity.INavigationSource; import cgeo.geocaching.activity.Progress; @@ -17,6 +18,7 @@ import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.GCConstants; import cgeo.geocaching.enumerations.CacheAttribute; +import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.WaypointType; @@ -132,7 +134,8 @@ import java.util.regex.Pattern; * * e.g. details, description, logs, waypoints, inventory... */ -public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailActivity.Page> implements CacheMenuHandler.ActivityInterface, INavigationSource { +public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailActivity.Page> + implements CacheMenuHandler.ActivityInterface, INavigationSource, ActivitySharingInterface, EditNoteDialogListener { private static final int MESSAGE_FAILED = -1; private static final int MESSAGE_SUCCEEDED = 1; @@ -237,7 +240,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc finish(); return; } - } else if (uriHost.contains("opencaching.de")) { + } else if (uriHost.contains("opencaching.de") || uriHost.contains("opencaching.fr")) { if (StringUtils.startsWith(uriPath, "/oc")) { geocode = uriPath.substring(1).toUpperCase(Locale.US); } else { @@ -264,9 +267,8 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return; } - // if we open this cache from a search, let's properly initialize the title bar, even if we don't have cache details - cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_ONLY); - updateTitleBar(geocode); + // If we open this cache from a search, let's properly initialize the title bar, even if we don't have cache details + updateTitleBar(geocode, name, null); final LoadCacheHandler loadCacheHandler = new LoadCacheHandler(this, progress); @@ -313,6 +315,14 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc }); locationUpdater = new CacheDetailsGeoDirHandler(this); + + // If we have a newer Android device setup Android Beam for easy cache sharing + initializeAndroidBeam(this); + } + + @Override + public String getAndroidBeamUri() { + return cache != null ? cache.getCgeoUrl() : null; } @Override @@ -603,17 +613,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // allow cache to notify CacheDetailActivity when it changes so it can be reloaded cache.setChangeNotificationHandler(new ChangeNotificationHandler(this, progress)); - updateTitleBar(null); - - // if we have a newer Android device setup Android Beam for easy cache sharing - initializeAndroidBeam( - new ActivitySharingInterface() { - @Override - public String getUri() { - return cache.getCgeoUrl(); - } - } - ); + updateTitleBar(cache.getGeocode(), cache.getName(), cache.getType()); // reset imagesList so Images view page will be redrawn imagesList = null; @@ -626,19 +626,16 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc Settings.addCacheToHistory(cache.getGeocode()); } - private void updateTitleBar(@Nullable final String geocode) { - if (cache == null) { - setTitle(StringUtils.isBlank(geocode) ? res.getString(R.string.cache) : geocode); - // avoid showing the traditional cache icon from the standard action bar (it may later change to the actual type icon) - getSupportActionBar().setIcon(android.R.color.transparent); + private void updateTitleBar(@Nullable final String geocode, @Nullable final String name, @Nullable final CacheType type) { + if (StringUtils.isNotBlank(name)) { + setTitle(StringUtils.isNotBlank(geocode) ? name + " (" + geocode + ")" : name); + } else { + setTitle(StringUtils.isNotBlank(geocode) ? geocode : res.getString(R.string.cache)); } - else { - if (StringUtils.isNotBlank(cache.getName())) { - setTitle(cache.getName() + " (" + cache.getGeocode() + ')'); - } else { - setTitle(cache.getGeocode()); - } - getSupportActionBar().setIcon(getResources().getDrawable(cache.getType().markerId)); + if (type != null) { + getSupportActionBar().setIcon(getResources().getDrawable(type.markerId)); + } else { + getSupportActionBar().setIcon(android.R.color.transparent); } } @@ -2248,7 +2245,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (UPDATE_LOAD_PROGRESS_DETAIL == msg.what && msg.obj instanceof String) { updateStatusMsg(R.string.cache_dialog_offline_save_message, (String) msg.obj); } else { - notifyDatasetChanged(activityRef); + notifyDataSetChanged(activityRef); } } } @@ -2264,7 +2261,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (UPDATE_LOAD_PROGRESS_DETAIL == msg.what && msg.obj instanceof String) { updateStatusMsg(R.string.cache_dialog_refresh_message, (String) msg.obj); } else { - notifyDatasetChanged(activityRef); + notifyDataSetChanged(activityRef); } } } @@ -2277,7 +2274,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public void handleMessage(final Message msg) { - notifyDatasetChanged(activityRef); + notifyDataSetChanged(activityRef); } } @@ -2292,12 +2289,12 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (msg.what == MESSAGE_FAILED) { super.handleMessage(msg); } else { - notifyDatasetChanged(activityRef); + notifyDataSetChanged(activityRef); } } } - private static void notifyDatasetChanged(final WeakReference<AbstractActivity> activityRef) { + private static void notifyDataSetChanged(final WeakReference<AbstractActivity> activityRef) { final CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get()); if (activity != null) { activity.notifyDataSetChanged(); @@ -2316,23 +2313,22 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc public static void editPersonalNote(final Geocache cache, final CacheDetailActivity activity) { if (cache.isOffline()) { - final EditNoteDialogListener editNoteDialogListener = new EditNoteDialogListener() { - @Override - public void onFinishEditNoteDialog(final String note) { - cache.setPersonalNote(note); - cache.parseWaypointsFromNote(); - final TextView personalNoteView = ButterKnife.findById(activity, R.id.personalnote); - setPersonalNote(personalNoteView, note); - DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); - activity.notifyDataSetChanged(); - } - }; final FragmentManager fm = activity.getSupportFragmentManager(); - final EditNoteDialog dialog = EditNoteDialog.newInstance(cache.getPersonalNote(), editNoteDialogListener); + final EditNoteDialog dialog = EditNoteDialog.newInstance(cache.getPersonalNote()); dialog.show(fm, "fragment_edit_note"); } } + @Override + public void onFinishEditNoteDialog(final String note) { + cache.setPersonalNote(note); + cache.parseWaypointsFromNote(); + final TextView personalNoteView = ButterKnife.findById(this, R.id.personalnote); + setPersonalNote(personalNoteView, note); + DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); + notifyDataSetChanged(); + } + private static void setPersonalNote(final TextView personalNoteView, final String personalNote) { personalNoteView.setText(personalNote, TextView.BufferType.SPANNABLE); if (StringUtils.isNotBlank(personalNote)) { diff --git a/main/src/cgeo/geocaching/CacheListActivity.java b/main/src/cgeo/geocaching/CacheListActivity.java index 82998cc..661755d 100644 --- a/main/src/cgeo/geocaching/CacheListActivity.java +++ b/main/src/cgeo/geocaching/CacheListActivity.java @@ -7,6 +7,7 @@ import cgeo.geocaching.activity.AbstractListActivity; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.activity.FilteredActivity; import cgeo.geocaching.activity.Progress; +import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; import cgeo.geocaching.apps.cachelist.CacheListAppFactory; import cgeo.geocaching.compatibility.Compatibility; @@ -40,7 +41,6 @@ import cgeo.geocaching.maps.CGeoMap; import cgeo.geocaching.network.Cookies; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; -import cgeo.geocaching.sensors.DirectionProvider; import cgeo.geocaching.sensors.GeoDirHandler; import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.settings.Settings; @@ -51,6 +51,7 @@ import cgeo.geocaching.ui.CacheListAdapter; import cgeo.geocaching.ui.LoggingUI; import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; +import cgeo.geocaching.utils.AngleUtils; import cgeo.geocaching.utils.AsyncTaskWithProgress; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.DateUtils; @@ -58,6 +59,9 @@ import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; +import com.github.amlcurran.showcaseview.targets.ActionViewTarget; +import com.github.amlcurran.showcaseview.targets.ActionViewTarget.Type; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; @@ -136,7 +140,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA @Override public void updateDirection(final float direction) { if (Settings.isLiveList()) { - adapter.setActualHeading(DirectionProvider.getDirectionNow(direction)); + adapter.setActualHeading(AngleUtils.getDirectionNow(direction)); } } @@ -388,8 +392,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA // get parameters Bundle extras = getIntent().getExtras(); if (extras != null) { - final Object typeObject = extras.get(Intents.EXTRA_LIST_TYPE); - type = (typeObject instanceof CacheListType) ? (CacheListType) typeObject : CacheListType.OFFLINE; + type = Intents.getListType(getIntent()); coords = extras.getParcelable(Intents.EXTRA_COORDS); } else { @@ -401,6 +404,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA coords = Geopoint.ZERO; } } + if (type == CacheListType.NEAREST) { + coords = CgeoApplication.getInstance().currentGeo().getCoords(); + } setTitle(title); @@ -427,9 +433,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA if (isInvokedFromAttachment()) { importGpxAttachement(); } - - - + else { + presentShowcase(); + } } /** @@ -765,7 +771,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return new SearchResult(geocodes); } - public void deletePastEvents() { + private void deletePastEvents() { final List<Geocache> deletion = new ArrayList<>(); for (final Geocache cache : adapter.getCheckedOrAllCaches()) { if (DateUtils.isPastEvent(cache)) { @@ -775,9 +781,15 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA new DropDetailsTask().execute(deletion.toArray(new Geocache[deletion.size()])); } - public void clearOfflineLogs() { - progress.show(this, null, res.getString(R.string.caches_clear_offlinelogs_progress), true, clearOfflineLogsHandler.cancelMessage()); - new ClearOfflineLogsThread(clearOfflineLogsHandler).start(); + private void clearOfflineLogs() { + Dialogs.confirmYesNo(this, R.string.caches_clear_offlinelogs, R.string.caches_clear_offlinelogs_message, new OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + progress.show(CacheListActivity.this, null, res.getString(R.string.caches_clear_offlinelogs_progress), true, clearOfflineLogsHandler.cancelMessage()); + new ClearOfflineLogsThread(clearOfflineLogsHandler).start(); + } + }); } /** @@ -1121,7 +1133,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA }); } - public void removeFromHistory() { + private void removeFromHistory() { final List<Geocache> caches = adapter.getCheckedOrAllCaches(); final String[] geocodes = new String[caches.size()]; for (int i = 0; i < geocodes.length; i++) { @@ -1132,7 +1144,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA getSupportLoaderManager().initLoader(CacheListLoaderType.REMOVE_FROM_HISTORY.getLoaderId(), b, this); } - public void importWeb() { + private void importWeb() { // menu is also shown with no device connected if (!Settings.isRegisteredForSend2cgeo()) { Dialogs.confirm(this, R.string.web_import_title, R.string.init_sendToCgeo_description, new OnClickListener() { @@ -1154,7 +1166,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA threadWeb.start(); } - public void dropStored() { + private void dropStored() { final int titleId = (adapter.getCheckedCount() > 0) ? R.string.caches_remove_selected : R.string.caches_remove_all; final int messageId = (adapter.getCheckedCount() > 0) ? R.string.caches_remove_selected_confirm : R.string.caches_remove_all_confirm; final String message = getString(messageId, adapter.getCheckedOrAllCount()); @@ -1350,13 +1362,13 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA }; } - public void switchListById(final int id) { + private void switchListById(final int id) { if (id < 0) { return; } if (id == PseudoList.HISTORY_LIST.id) { - CacheListActivity.startActivityHistory(this); + startActivity(CacheListActivity.getHistoryIntent(this)); finish(); return; } @@ -1381,6 +1393,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA showProgress(true); showFooterLoadingCaches(); DataStore.moveToList(adapter.getCheckedCaches(), listId); + adapter.setSelectMode(false); currentLoader = (OfflineGeocacheListLoader) getSupportLoaderManager().restartLoader(CacheListType.OFFLINE.getLoaderId(), OfflineGeocacheListLoader.getBundleForList(listId), this); @@ -1446,7 +1459,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA public static void startActivityOffline(final Context context) { final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.OFFLINE); + Intents.putListType(cachesIntent, CacheListType.OFFLINE); context.startActivity(cachesIntent); } @@ -1455,7 +1468,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.OWNER); + Intents.putListType(cachesIntent, CacheListType.OWNER); cachesIntent.putExtra(Intents.EXTRA_USERNAME, userName); context.startActivity(cachesIntent); } @@ -1473,7 +1486,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.FINDER); + Intents.putListType(cachesIntent, CacheListType.FINDER); cachesIntent.putExtra(Intents.EXTRA_USERNAME, userName); context.startActivity(cachesIntent); } @@ -1494,25 +1507,17 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } } - public static void startActivityNearest(final AbstractActivity context, final Geopoint coordsNow) { - if (!isValidCoords(context, coordsNow)) { - return; - } - final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.NEAREST); - cachesIntent.putExtra(Intents.EXTRA_COORDS, coordsNow); - context.startActivity(cachesIntent); + public static Intent getNearestIntent(final Activity context) { + return Intents.putListType(new Intent(context, CacheListActivity.class), CacheListType.NEAREST); } - public static void startActivityHistory(final Context context) { - final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.HISTORY); - context.startActivity(cachesIntent); + public static Intent getHistoryIntent(final Context context) { + return Intents.putListType(new Intent(context, CacheListActivity.class), CacheListType.HISTORY); } public static void startActivityAddress(final Context context, final Geopoint coords, final String address) { final Intent addressIntent = new Intent(context, CacheListActivity.class); - addressIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.ADDRESS); + Intents.putListType(addressIntent, CacheListType.ADDRESS); addressIntent.putExtra(Intents.EXTRA_COORDS, coords); addressIntent.putExtra(Intents.EXTRA_ADDRESS, address); context.startActivity(addressIntent); @@ -1523,7 +1528,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.COORDINATE); + Intents.putListType(cachesIntent, CacheListType.COORDINATE); cachesIntent.putExtra(Intents.EXTRA_COORDS, coords); context.startActivity(cachesIntent); } @@ -1542,15 +1547,15 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.KEYWORD); + Intents.putListType(cachesIntent, CacheListType.KEYWORD); cachesIntent.putExtra(Intents.EXTRA_KEYWORD, keyword); context.startActivity(cachesIntent); } public static void startActivityMap(final Context context, final SearchResult search) { final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.MAP); cachesIntent.putExtra(Intents.EXTRA_SEARCH, search); + Intents.putListType(cachesIntent, CacheListType.MAP); context.startActivity(cachesIntent); } @@ -1561,7 +1566,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } final Intent cachesIntent = new Intent(context, CacheListActivity.class); - cachesIntent.putExtra(Intents.EXTRA_LIST_TYPE, CacheListType.POCKET); + Intents.putListType(cachesIntent, CacheListType.POCKET); cachesIntent.putExtra(Intents.EXTRA_NAME, pq.getName()); cachesIntent.putExtra(Intents.EXTRA_POCKET_GUID, guid); context.startActivity(cachesIntent); @@ -1741,4 +1746,14 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } return numbers.isEmpty() ? null : StringUtils.join(numbers, '/'); } + + @Override + public ShowcaseViewBuilder getShowcase() { + if (mCacheListSpinnerAdapter != null) { + return new ShowcaseViewBuilder(this) + .setTarget(new ActionViewTarget(this, Type.SPINNER)) + .setContent(R.string.showcase_cachelist_title, R.string.showcase_cachelist_text); + } + return null; + } } diff --git a/main/src/cgeo/geocaching/CgeoApplication.java b/main/src/cgeo/geocaching/CgeoApplication.java index ae5a565..34dab09 100644 --- a/main/src/cgeo/geocaching/CgeoApplication.java +++ b/main/src/cgeo/geocaching/CgeoApplication.java @@ -1,12 +1,13 @@ package cgeo.geocaching; import cgeo.geocaching.playservices.LocationProvider; -import cgeo.geocaching.sensors.DirectionProvider; import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDataProvider; import cgeo.geocaching.sensors.GpsStatusProvider; import cgeo.geocaching.sensors.GpsStatusProvider.Status; import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.OrientationProvider; +import cgeo.geocaching.sensors.RotationProvider; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.OOMDumpingUncaughtExceptionHandler; @@ -15,8 +16,11 @@ import cgeo.geocaching.utils.RxUtils; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; +import org.eclipse.jdt.annotation.NonNull; + import rx.Observable; import rx.functions.Action1; +import rx.functions.Func1; import android.app.Application; import android.view.ViewConfiguration; @@ -33,13 +37,15 @@ public class CgeoApplication extends Application { private Observable<IGeoData> geoDataObservableLowPower; private Observable<Float> directionObservable; private Observable<Status> gpsStatusObservable; - private volatile IGeoData currentGeo = GeoData.dummyLocation(); + @NonNull private volatile IGeoData currentGeo = GeoData.DUMMY_LOCATION; + private volatile boolean hasValidLocation = false; private volatile float currentDirection = 0.0f; private boolean isGooglePlayServicesAvailable = false; - private final Action1<IGeoData> REMEMBER_GEODATA = new Action1<IGeoData>() { + private final Action1<IGeoData> rememberGeodataAction = new Action1<IGeoData>() { @Override public void call(final IGeoData geoData) { currentGeo = geoData; + hasValidLocation = true; } }; @@ -78,38 +84,60 @@ public class CgeoApplication extends Application { menuKeyField.setBoolean(config, false); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException ignore) { } + + // Set language to English if the user decided so. + Settings.setLanguage(Settings.isUseEnglish()); + // ensure initialization of lists DataStore.getLists(); + // Check if Google Play services is available if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) { isGooglePlayServicesAvailable = true; } Log.i("Google Play services are " + (isGooglePlayServicesAvailable ? "" : "not ") + "available"); setupGeoDataObservables(Settings.useGooglePlayServices(), Settings.useLowPowerMode()); - geoDataObservableLowPower.subscribeOn(RxUtils.looperCallbacksScheduler).first().subscribe(REMEMBER_GEODATA); - directionObservable = DirectionProvider.create(this).replay(1).refCount().doOnNext(new Action1<Float>() { - @Override - public void call(final Float direction) { - currentDirection = direction; - } - }); - gpsStatusObservable = GpsStatusProvider.create(this).startWith(GpsStatusProvider.NO_GPS).share(); + setupDirectionObservable(Settings.useLowPowerMode()); + gpsStatusObservable = GpsStatusProvider.create(this).replay(1).refCount(); + + // Attempt to acquire an initial location before any real activity happens. + geoDataObservableLowPower.subscribeOn(RxUtils.looperCallbacksScheduler).first().subscribe(rememberGeodataAction); } public void setupGeoDataObservables(final boolean useGooglePlayServices, final boolean useLowPowerLocation) { if (useGooglePlayServices) { - geoDataObservable = LocationProvider.getMostPrecise(this, true).replay(1).refCount().doOnNext(REMEMBER_GEODATA); + geoDataObservable = LocationProvider.getMostPrecise(this).doOnNext(rememberGeodataAction); if (useLowPowerLocation) { - geoDataObservableLowPower = LocationProvider.getLowPower(this, true).replay(1).refCount().doOnNext(REMEMBER_GEODATA); + geoDataObservableLowPower = LocationProvider.getLowPower(this, true).doOnNext(rememberGeodataAction); } else { geoDataObservableLowPower = geoDataObservable; } } else { - geoDataObservable = GeoDataProvider.create(this).replay(1).refCount().doOnNext(REMEMBER_GEODATA); + geoDataObservable = GeoDataProvider.create(this).replay(1).refCount().doOnNext(rememberGeodataAction); geoDataObservableLowPower = geoDataObservable; } } + public void setupDirectionObservable(final boolean useLowPower) { + directionObservable = RotationProvider.create(this, useLowPower).onErrorResumeNext(new Func1<Throwable, Observable<? extends Float>>() { + @Override + public Observable<? extends Float> call(final Throwable throwable) { + return OrientationProvider.create(CgeoApplication.this); + } + }).onErrorResumeNext(new Func1<Throwable, Observable<? extends Float>>() { + @Override + public Observable<? extends Float> call(final Throwable throwable) { + Log.e("Device orientation will not be available as no suitable sensors were found"); + return Observable.<Float>never().startWith(0.0f); + } + }).replay(1).refCount().doOnNext(new Action1<Float>() { + @Override + public void call(final Float direction) { + currentDirection = direction; + } + }); + } + @Override public void onLowMemory() { Log.i("Cleaning applications cache."); @@ -131,10 +159,15 @@ public class CgeoApplication extends Application { return gpsStatusObservable; } + @NonNull public IGeoData currentGeo() { return currentGeo; } + public boolean hasValidLocation() { + return hasValidLocation; + } + public Float distanceNonBlocking(final ICoordinates target) { if (target.getCoords() == null) { return null; diff --git a/main/src/cgeo/geocaching/CompassActivity.java b/main/src/cgeo/geocaching/CompassActivity.java index 7b5ae35..00fa790 100644 --- a/main/src/cgeo/geocaching/CompassActivity.java +++ b/main/src/cgeo/geocaching/CompassActivity.java @@ -8,7 +8,6 @@ import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.geopoint.Units; import cgeo.geocaching.maps.CGeoMap; -import cgeo.geocaching.sensors.DirectionProvider; import cgeo.geocaching.sensors.GeoDirHandler; import cgeo.geocaching.sensors.GpsStatusProvider.Status; import cgeo.geocaching.sensors.IGeoData; @@ -16,6 +15,7 @@ import cgeo.geocaching.settings.Settings; import cgeo.geocaching.speech.SpeechService; import cgeo.geocaching.ui.CompassView; import cgeo.geocaching.ui.LoggingUI; +import cgeo.geocaching.utils.AngleUtils; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; @@ -38,14 +38,10 @@ import android.view.SubMenu; import android.view.View; import android.widget.TextView; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; public class CompassActivity extends AbstractActionBarActivity { - private static final int COORDINATES_OFFSET = 10; - @InjectView(R.id.nav_type) protected TextView navType; @InjectView(R.id.nav_accuracy) protected TextView navAccuracy; @InjectView(R.id.nav_satellites) protected TextView navSatellites; @@ -60,7 +56,6 @@ public class CompassActivity extends AbstractActionBarActivity { private static final String EXTRAS_NAME = "name"; private static final String EXTRAS_GEOCODE = "geocode"; private static final String EXTRAS_CACHE_INFO = "cacheinfo"; - private static final List<IWaypoint> coordinates = new ArrayList<>(); /** * Destination of the compass, or null (if the compass is used for a waypoint only). @@ -124,6 +119,7 @@ public class CompassActivity extends AbstractActionBarActivity { public void onResume() { super.onResume(geoDirHandler.start(GeoDirHandler.UPDATE_GEODIR), app.gpsStatusObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(gpsStatusHandler)); + forceRefresh(); } @Override @@ -143,34 +139,42 @@ public class CompassActivity extends AbstractActionBarActivity { setTitle(); setDestCoords(); setCacheInfo(); + forceRefresh(); + } + private void forceRefresh() { // Force a refresh of location and direction when data is available. final CgeoApplication app = CgeoApplication.getInstance(); final IGeoData geo = app.currentGeo(); - if (geo != null) { - geoDirHandler.updateGeoDir(geo, app.currentDirection()); - } + geoDirHandler.updateGeoDir(geo, app.currentDirection()); } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.compass_activity_options, menu); menu.findItem(R.id.menu_compass_sensor).setVisible(hasMagneticFieldSensor); - final SubMenu subMenu = menu.findItem(R.id.menu_select_destination).getSubMenu(); - if (coordinates.size() > 1) { - for (int i = 0; i < coordinates.size(); i++) { - final IWaypoint coordinate = coordinates.get(i); - subMenu.add(0, COORDINATES_OFFSET + i, 0, coordinate.getName() + " (" + coordinate.getCoordType() + ")"); - } - } else { - menu.findItem(R.id.menu_select_destination).setVisible(false); - } if (cache != null) { LoggingUI.addMenuItems(this, menu, cache); } + addWaypointItems(menu); return true; } + private void addWaypointItems(final Menu menu) { + if (cache != null) { + final List<Waypoint> waypoints = cache.getWaypoints(); + boolean visible = false; + final SubMenu subMenu = menu.findItem(R.id.menu_select_destination).getSubMenu(); + for (final Waypoint waypoint : waypoints) { + if (waypoint.getCoords() != null) { + subMenu.add(0, waypoint.getId(), 0, waypoint.getName()); + visible = true; + } + } + menu.findItem(R.id.menu_select_destination).setVisible(visible); + } + } + @Override public boolean onPrepareOptionsMenu(final Menu menu) { super.onPrepareOptionsMenu(menu); @@ -212,18 +216,19 @@ public class CompassActivity extends AbstractActionBarActivity { if (LoggingUI.onMenuItemSelected(item, this, cache)) { return true; } - final int coordinatesIndex = id - COORDINATES_OFFSET; - if (coordinatesIndex >= 0 && coordinatesIndex < coordinates.size()) { - final IWaypoint coordinate = coordinates.get(coordinatesIndex); - title = coordinate.getName(); - dstCoords = coordinate.getCoords(); - setTitle(); - setDestCoords(); - setCacheInfo(); - updateDistanceInfo(app.currentGeo()); - - Log.d("destination set: " + title + " (" + dstCoords + ")"); - return true; + if (cache != null) { + final Waypoint waypoint = cache.getWaypointById(id); + if (waypoint != null) { + title = waypoint.getName(); + dstCoords = waypoint.getCoords(); + setTitle(); + setDestCoords(); + setCacheInfo(); + updateDistanceInfo(app.currentGeo()); + + Log.d("destination set: " + title + " (" + dstCoords + ")"); + return true; + } } } return super.onOptionsItemSelected(item); @@ -297,7 +302,7 @@ public class CompassActivity extends AbstractActionBarActivity { navLocation.setText(res.getString(R.string.loc_trying)); } - updateNorthHeading(DirectionProvider.getDirectionNow(dir)); + updateNorthHeading(AngleUtils.getDirectionNow(dir)); } catch (final RuntimeException e) { Log.w("Failed to LocationUpdater location."); } @@ -310,17 +315,8 @@ public class CompassActivity extends AbstractActionBarActivity { } } - public static void startActivity(final Context context, final String geocode, final String displayedName, final Geopoint coords, final Collection<IWaypoint> coordinatesWithType, + public static void startActivity(final Context context, final String geocode, final String displayedName, final Geopoint coords, final String info) { - coordinates.clear(); - if (coordinatesWithType != null) { - for (final IWaypoint coordinate : coordinatesWithType) { - if (coordinate != null) { - coordinates.add(coordinate); - } - } - } - final Intent navigateIntent = new Intent(context, CompassActivity.class); navigateIntent.putExtra(EXTRAS_COORDS, coords); navigateIntent.putExtra(EXTRAS_GEOCODE, geocode); @@ -331,12 +327,12 @@ public class CompassActivity extends AbstractActionBarActivity { context.startActivity(navigateIntent); } - public static void startActivity(final Context context, final String geocode, final String displayedName, final Geopoint coords, final Collection<IWaypoint> coordinatesWithType) { - CompassActivity.startActivity(context, geocode, displayedName, coords, coordinatesWithType, null); + public static void startActivity(final Context context, final String geocode, final String displayedName, final Geopoint coords) { + startActivity(context, geocode, displayedName, coords, null); } - public static void startActivity(final Context context, final Geocache cache) { - startActivity(context, cache.getGeocode(), cache.getName(), cache.getCoords(), null, + public static void startActivityCache(final Context context, final Geocache cache) { + startActivity(context, cache.getGeocode(), cache.getName(), cache.getCoords(), Formatter.formatCacheInfoShort(cache)); } diff --git a/main/src/cgeo/geocaching/CreateShortcutActivity.java b/main/src/cgeo/geocaching/CreateShortcutActivity.java index ffcf81b..cf6d486 100644 --- a/main/src/cgeo/geocaching/CreateShortcutActivity.java +++ b/main/src/cgeo/geocaching/CreateShortcutActivity.java @@ -1,19 +1,54 @@ package cgeo.geocaching; import cgeo.geocaching.activity.AbstractActionBarActivity; -import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.maps.MapActivity; +import cgeo.geocaching.ui.dialog.Dialogs; +import cgeo.geocaching.ui.dialog.Dialogs.ItemWithIcon; +import cgeo.geocaching.utils.ImageUtils; import rx.functions.Action1; import android.content.Intent; import android.content.Intent.ShortcutIconResource; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.os.Bundle; +import java.util.ArrayList; +import java.util.List; + public class CreateShortcutActivity extends AbstractActionBarActivity { + private static class Shortcut implements ItemWithIcon { + + private final int titleResourceId; + private final int drawableResourceId; + private final Intent intent; + + /** + * shortcut with a separate icon + */ + public Shortcut(final int titleResourceId, final int drawableResourceId, final Intent intent) { + this.titleResourceId = titleResourceId; + this.drawableResourceId = drawableResourceId; + this.intent = intent; + } + + @Override + public int getIcon() { + return drawableResourceId; + } + + @Override + public String toString() { + return CgeoApplication.getInstance().getString(titleResourceId); + } + } + @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // init @@ -23,35 +58,82 @@ public class CreateShortcutActivity extends AbstractActionBarActivity { } private void promptForShortcut() { + final List<Shortcut> shortcuts = new ArrayList<>(); + + shortcuts.add(new Shortcut(R.string.live_map_button, R.drawable.main_live, new Intent(this, MapActivity.class))); + shortcuts.add(new Shortcut(R.string.caches_nearby_button, R.drawable.main_nearby, CacheListActivity.getNearestIntent(this))); + + // TODO: make logging activities ask for cache/trackable when being invoked externally + // shortcuts.add(new Shortcut(R.string.cache_menu_visit, new Intent(this, LogCacheActivity.class))); + // shortcuts.add(new Shortcut(R.string.trackable_log_touch, new Intent(this, LogTrackableActivity.class))); + + final Shortcut offlineShortcut = new Shortcut(R.string.stored_caches_button, R.drawable.main_stored, null); + shortcuts.add(offlineShortcut); + shortcuts.add(new Shortcut(R.string.advanced_search_button, R.drawable.main_search, new Intent(this, SearchActivity.class))); + shortcuts.add(new Shortcut(R.string.any_button, R.drawable.main_any, new Intent(this, NavigateAnyPointActivity.class))); + shortcuts.add(new Shortcut(R.string.menu_history, R.drawable.main_stored, CacheListActivity.getHistoryIntent(this))); + + Dialogs.select(this, getString(R.string.create_shortcut), shortcuts, new Action1<Shortcut>() { + + @Override + public void call(final Shortcut shortcut) { + if (shortcut == offlineShortcut) { + promptForListShortcut(); + } + else { + createShortcutAndFinish(shortcut.toString(), shortcut.intent, shortcut.drawableResourceId); + } + } + }); + } + + protected void promptForListShortcut() { new StoredList.UserInterface(this).promptForListSelection(R.string.create_shortcut, new Action1<Integer>() { @Override public void call(final Integer listId) { - final Intent shortcut = createShortcut(listId); - setResult(RESULT_OK, shortcut); - - // finish activity to return the shortcut - finish(); + createOfflineListShortcut(listId.intValue()); } - }, false, PseudoList.HISTORY_LIST.id); + }, true, -1); } - protected Intent createShortcut(int listId) { + protected void createOfflineListShortcut(final int listId) { final StoredList list = DataStore.getList(listId); if (list == null) { - return null; + return; } // target to be executed by the shortcut final Intent targetIntent = new Intent(this, CacheListActivity.class); targetIntent.putExtra(Intents.EXTRA_LIST_ID, list.id); - final ShortcutIconResource iconResource = Intent.ShortcutIconResource.fromContext(this, R.drawable.cgeo); // shortcut to be returned + createShortcutAndFinish(list.title, targetIntent, R.drawable.main_stored); + } + + private void createShortcutAndFinish(final String title, final Intent targetIntent, final int iconResourceId) { final Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, targetIntent); - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, list.title); - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); - return intent; + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); + if (iconResourceId == R.drawable.cgeo) { + final ShortcutIconResource iconResource = Intent.ShortcutIconResource.fromContext(this, iconResourceId); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); + } + else { + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createOverlay(iconResourceId)); + } + + setResult(RESULT_OK, intent); + + // finish activity to return the shortcut + finish(); + } + + private Bitmap createOverlay(final int drawableResourceId) { + final LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { + res.getDrawable(drawableResourceId), res.getDrawable(R.drawable.cgeo) }); + layerDrawable.setLayerInset(0, 0, 0, 10, 10); + layerDrawable.setLayerInset(1, 50, 50, 0, 0); + return ImageUtils.convertToBitmap(layerDrawable); } } diff --git a/main/src/cgeo/geocaching/DataStore.java b/main/src/cgeo/geocaching/DataStore.java index 4f4eaec..9ee588b 100644 --- a/main/src/cgeo/geocaching/DataStore.java +++ b/main/src/cgeo/geocaching/DataStore.java @@ -112,35 +112,32 @@ public class DataStore { "cg_caches.direction," + // 16 "cg_caches.distance," + // 17 "cg_caches.terrain," + // 18 - "cg_caches.latlon," + // 19 - "cg_caches.location," + // 20 - "cg_caches.personal_note," + // 21 - "cg_caches.shortdesc," + // 22 - "cg_caches.favourite_cnt," + // 23 - "cg_caches.rating," + // 24 - "cg_caches.votes," + // 25 - "cg_caches.myvote," + // 26 - "cg_caches.disabled," + // 27 - "cg_caches.archived," + // 28 - "cg_caches.members," + // 29 - "cg_caches.found," + // 30 - "cg_caches.favourite," + // 31 - "cg_caches.inventoryunknown," + // 32 - "cg_caches.onWatchlist," + // 33 - "cg_caches.reliable_latlon," + // 34 - "cg_caches.coordsChanged," + // 35 - "cg_caches.latitude," + // 36 - "cg_caches.longitude," + // 37 - "cg_caches.finalDefined," + // 38 - "cg_caches._id," + // 39 - "cg_caches.inventorycoins," + // 40 - "cg_caches.inventorytags," + // 41 - "cg_caches.logPasswordRequired"; // 42 - - //TODO: remove "latlon" field from cache table + "cg_caches.location," + // 19 + "cg_caches.personal_note," + // 20 + "cg_caches.shortdesc," + // 21 + "cg_caches.favourite_cnt," + // 22 + "cg_caches.rating," + // 23 + "cg_caches.votes," + // 24 + "cg_caches.myvote," + // 25 + "cg_caches.disabled," + // 26 + "cg_caches.archived," + // 27 + "cg_caches.members," + // 28 + "cg_caches.found," + // 29 + "cg_caches.favourite," + // 30 + "cg_caches.inventoryunknown," + // 31 + "cg_caches.onWatchlist," + // 32 + "cg_caches.reliable_latlon," + // 33 + "cg_caches.coordsChanged," + // 34 + "cg_caches.latitude," + // 35 + "cg_caches.longitude," + // 36 + "cg_caches.finalDefined," + // 37 + "cg_caches._id," + // 38 + "cg_caches.inventorycoins," + // 39 + "cg_caches.inventorytags," + // 40 + "cg_caches.logPasswordRequired"; // 41 /** The list of fields needed for mapping. */ - private static final String[] WAYPOINT_COLUMNS = new String[] { "_id", "geocode", "updated", "type", "prefix", "lookup", "name", "latlon", "latitude", "longitude", "note", "own", "visited" }; + private static final String[] WAYPOINT_COLUMNS = new String[] { "_id", "geocode", "updated", "type", "prefix", "lookup", "name", "latitude", "longitude", "note", "own", "visited" }; /** Number of days (as ms) after temporarily saved caches are deleted */ private final static long DAYS_AFTER_CACHE_IS_DELETED = 3 * 24 * 60 * 60 * 1000; @@ -184,7 +181,6 @@ public class DataStore { + "size text, " + "difficulty float, " + "terrain float, " - + "latlon text, " + "location text, " + "direction double, " + "distance double, " @@ -215,9 +211,7 @@ public class DataStore { + "create table " + dbTableLists + " (" + "_id integer primary key autoincrement, " + "title text not null, " - + "updated long not null, " - + "latitude double, " - + "longitude double " + + "updated long not null" + "); "; private static final String dbCreateAttributes = "" + "create table " + dbTableAttributes + " (" @@ -236,7 +230,6 @@ public class DataStore { + "prefix text, " + "lookup text, " + "name text, " - + "latlon text, " + "latitude double, " + "longitude double, " + "note text, " @@ -639,7 +632,6 @@ public class DataStore { + "size text, " + "difficulty float, " + "terrain float, " - + "latlon text, " + "location text, " + "direction double, " + "distance double, " @@ -666,7 +658,7 @@ public class DataStore { db.execSQL(dbCreateCachesTemp); db.execSQL("insert into " + dbTableCachesTemp + " select _id,updated,detailed,detailedupdate,visiteddate,geocode,reason,cacheid,guid,type,name,own,owner,owner_real," + - "hidden,hint,size,difficulty,terrain,latlon,location,direction,distance,latitude,longitude, 0," + + "hidden,hint,size,difficulty,terrain,location,direction,distance,latitude,longitude, 0," + "personal_note,shortdesc,description,favourite_cnt,rating,votes,myvote,disabled,archived,members,found,favourite,inventorycoins," + "inventorytags,inventoryunknown,onWatchlist from " + dbTableCaches); db.execSQL("drop table " + dbTableCaches); @@ -682,13 +674,12 @@ public class DataStore { + "prefix text, " + "lookup text, " + "name text, " - + "latlon text, " + "latitude double, " + "longitude double, " + "note text " + "); "; db.execSQL(dbCreateWaypointsTemp); - db.execSQL("insert into " + dbTableWaypointsTemp + " select _id, geocode, updated, type, prefix, lookup, name, latlon, latitude, longitude, note from " + dbTableWaypoints); + db.execSQL("insert into " + dbTableWaypointsTemp + " select _id, geocode, updated, type, prefix, lookup, name, latitude, longitude, note from " + dbTableWaypoints); db.execSQL("drop table " + dbTableWaypoints); db.execSQL("alter table " + dbTableWaypointsTemp + " rename to " + dbTableWaypoints); @@ -1274,7 +1265,6 @@ public class DataStore { values.put("prefix", oneWaypoint.getPrefix()); values.put("lookup", oneWaypoint.getLookup()); values.put("name", oneWaypoint.getName()); - values.put("latlon", oneWaypoint.getLatlon()); putCoords(values, oneWaypoint.getCoords()); values.put("note", oneWaypoint.getNote()); values.put("own", oneWaypoint.isUserDefined() ? 1 : 0); @@ -1353,7 +1343,6 @@ public class DataStore { values.put("prefix", waypoint.getPrefix()); values.put("lookup", waypoint.getLookup()); values.put("name", waypoint.getName()); - values.put("latlon", waypoint.getLatlon()); putCoords(values, waypoint.getCoords()); values.put("note", waypoint.getNote()); values.put("own", waypoint.isUserDefined() ? 1 : 0); @@ -1542,7 +1531,7 @@ public class DataStore { return new HashSet<>(); } - final Set<Geocache> result = new HashSet<>(); + final Set<Geocache> result = new HashSet<>(geocodes.size()); final Set<String> remaining = new HashSet<>(geocodes); if (loadFlags.contains(LoadFlag.CACHE_BEFORE)) { @@ -1620,7 +1609,7 @@ public class DataStore { int logIndex = -1; while (cursor.moveToNext()) { - final Geocache cache = DataStore.createCacheFromDatabaseContent(cursor); + final Geocache cache = createCacheFromDatabaseContent(cursor); if (loadFlags.contains(LoadFlag.ATTRIBUTES)) { cache.setAttributes(loadAttributes(cache.getGeocode())); @@ -1730,25 +1719,25 @@ public class DataStore { } cache.setTerrain(cursor.getFloat(18)); // do not set cache.location - cache.setCoords(getCoords(cursor, 36, 37)); - cache.setPersonalNote(cursor.getString(21)); + cache.setPersonalNote(cursor.getString(20)); // do not set cache.shortdesc // do not set cache.description - cache.setFavoritePoints(cursor.getInt(23)); - cache.setRating(cursor.getFloat(24)); - cache.setVotes(cursor.getInt(25)); - cache.setMyVote(cursor.getFloat(26)); - cache.setDisabled(cursor.getInt(27) == 1); - cache.setArchived(cursor.getInt(28) == 1); - cache.setPremiumMembersOnly(cursor.getInt(29) == 1); - cache.setFound(cursor.getInt(30) == 1); - cache.setFavorite(cursor.getInt(31) == 1); - cache.setInventoryItems(cursor.getInt(32)); - cache.setOnWatchlist(cursor.getInt(33) == 1); - cache.setReliableLatLon(cursor.getInt(34) > 0); - cache.setUserModifiedCoords(cursor.getInt(35) > 0); - cache.setFinalDefined(cursor.getInt(38) > 0); - cache.setLogPasswordRequired(cursor.getInt(42) > 0); + cache.setFavoritePoints(cursor.getInt(22)); + cache.setRating(cursor.getFloat(23)); + cache.setVotes(cursor.getInt(24)); + cache.setMyVote(cursor.getFloat(25)); + cache.setDisabled(cursor.getInt(26) == 1); + cache.setArchived(cursor.getInt(27) == 1); + cache.setPremiumMembersOnly(cursor.getInt(28) == 1); + cache.setFound(cursor.getInt(29) == 1); + cache.setFavorite(cursor.getInt(30) == 1); + cache.setInventoryItems(cursor.getInt(31)); + cache.setOnWatchlist(cursor.getInt(32) == 1); + cache.setReliableLatLon(cursor.getInt(33) > 0); + cache.setUserModifiedCoords(cursor.getInt(34) > 0); + cache.setCoords(getCoords(cursor, 35, 36)); + cache.setFinalDefined(cursor.getInt(37) > 0); + cache.setLogPasswordRequired(cursor.getInt(41) > 0); Log.d("Loading " + cache.toString() + " (" + cache.getListId() + ") from DB"); @@ -1830,7 +1819,6 @@ public class DataStore { waypoint.setGeocode(cursor.getString(cursor.getColumnIndex("geocode"))); waypoint.setPrefix(cursor.getString(cursor.getColumnIndex("prefix"))); waypoint.setLookup(cursor.getString(cursor.getColumnIndex("lookup"))); - waypoint.setLatlon(cursor.getString(cursor.getColumnIndex("latlon"))); waypoint.setCoords(getCoords(cursor, cursor.getColumnIndex("latitude"), cursor.getColumnIndex("longitude"))); waypoint.setNote(cursor.getString(cursor.getColumnIndex("note"))); @@ -2846,7 +2834,7 @@ public class DataStore { final StringBuilder whereExpr = new StringBuilder("geocode in ("); final Iterator<String> iterator = geocodes.iterator(); while (true) { - whereExpr.append(DatabaseUtils.sqlEscapeString(StringUtils.upperCase(iterator.next()))); + DatabaseUtils.appendEscapedSQLString(whereExpr, StringUtils.upperCase(iterator.next())); if (!iterator.hasNext()) { break; } @@ -3112,18 +3100,24 @@ public class DataStore { cursor.getString(1), tbcode, Intents.ACTION_TRACKABLE, - tbcode + tbcode, + String.valueOf(R.drawable.trackable_all) }); } cursor.close(); } public static String[] getSuggestions(final String table, final String column, final String input) { - final Cursor cursor = database.rawQuery("SELECT DISTINCT " + column - + " FROM " + table - + " WHERE " + column + " LIKE ?" - + " ORDER BY " + column + " COLLATE NOCASE ASC;", new String[] { getSuggestionArgument(input) }); - return cursorToColl(cursor, new LinkedList<String>(), GET_STRING_0).toArray(new String[cursor.getCount()]); + try { + final Cursor cursor = database.rawQuery("SELECT DISTINCT " + column + + " FROM " + table + + " WHERE " + column + " LIKE ?" + + " ORDER BY " + column + " COLLATE NOCASE ASC;", new String[] { getSuggestionArgument(input) }); + return cursorToColl(cursor, new LinkedList<String>(), GET_STRING_0).toArray(new String[cursor.getCount()]); + } catch (final RuntimeException e) { + Log.e("cannot get suggestions from " + table + "->" + column + " for input '" + input + "'", e); + return new String[0]; + } } public static String[] getSuggestionsOwnerName(final String input) { diff --git a/main/src/cgeo/geocaching/EditWaypointActivity.java b/main/src/cgeo/geocaching/EditWaypointActivity.java index 73cb477..5dccad8 100644 --- a/main/src/cgeo/geocaching/EditWaypointActivity.java +++ b/main/src/cgeo/geocaching/EditWaypointActivity.java @@ -89,10 +89,10 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C */ private Geocache cache; - private Handler loadWaypointHandler = new Handler() { + private final Handler loadWaypointHandler = new Handler() { @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { try { if (waypoint == null) { Log.d("No waypoint loaded to edit. id= " + id); @@ -126,7 +126,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C if (own) { initializeWaypointTypeSelector(); } - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.e("EditWaypointActivity.loadWaypointHandler", e); } finally { if (waitDialog != null) { @@ -138,7 +138,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C }; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.editwaypoint_activity); if (StringUtils.isBlank(geocode) && id <= 0) { @@ -159,11 +159,11 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C addWaypoint.setOnClickListener(new SaveWaypointListener()); - List<String> wayPointNames = new ArrayList<>(); - for (WaypointType wpt : WaypointType.ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL) { + final List<String> wayPointNames = new ArrayList<>(); + for (final WaypointType wpt : WaypointType.ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL) { wayPointNames.add(wpt.getL10n()); } - ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, wayPointNames); + final ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, wayPointNames); waypointName.setAdapter(adapter); if (savedInstanceState != null) { @@ -189,7 +189,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C disableSuggestions(distanceView); } - private void setCoordsModificationVisibility(IConnector con, Geocache cache) { + private void setCoordsModificationVisibility(final IConnector con, final Geocache cache) { if (cache != null && (cache.getType() == CacheType.MYSTERY || cache.getType() == CacheType.MULTI)) { coordinatesGroup.setVisibility(View.VISIBLE); modifyBoth.setVisibility(con.supportsOwnCoordinates() ? View.VISIBLE : View.GONE); @@ -205,18 +205,23 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C } private void initializeWaypointTypeSelector() { - ArrayAdapter<WaypointType> wpAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, POSSIBLE_WAYPOINT_TYPES.toArray(new WaypointType[POSSIBLE_WAYPOINT_TYPES.size()])); + final ArrayAdapter<WaypointType> wpAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, POSSIBLE_WAYPOINT_TYPES.toArray(new WaypointType[POSSIBLE_WAYPOINT_TYPES.size()])); wpAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); waypointTypeSelector.setAdapter(wpAdapter); waypointTypeSelector.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) { + public void onItemSelected(final AdapterView<?> parent, final View v, final int pos, final long id) { + final String oldDefaultName = waypointTypeSelectorPosition >= 0 ? getDefaultWaypointName(POSSIBLE_WAYPOINT_TYPES.get(waypointTypeSelectorPosition)) : StringUtils.EMPTY; waypointTypeSelectorPosition = pos; + final String currentName = waypointName.getText().toString().trim(); + if (StringUtils.isBlank(currentName) || oldDefaultName.equals(currentName)) { + waypointName.setText(getDefaultWaypointName(getSelectedWaypointType())); + } } @Override - public void onNothingSelected(AdapterView<?> parent) { + public void onNothingSelected(final AdapterView<?> parent) { } }); @@ -277,7 +282,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C waypoint = DataStore.loadWaypoint(id); loadWaypointHandler.sendMessage(Message.obtain()); - } catch (Exception e) { + } catch (final Exception e) { Log.e("EditWaypointActivity.loadWaypoint.run", e); } } @@ -286,15 +291,15 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C private class CoordDialogListener implements View.OnClickListener { @Override - public void onClick(View arg0) { + public void onClick(final View arg0) { Geopoint gp = null; try { gp = new Geopoint(buttonLat.getText().toString(), buttonLon.getText().toString()); - } catch (Geopoint.ParseException e) { + } catch (final Geopoint.ParseException e) { // button text is blank when creating new waypoint } - Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); - CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(cache, gp, app.currentGeo()); + final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); + final CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(cache, gp, app.currentGeo()); coordsDialog.setCancelable(true); coordsDialog.show(getSupportFragmentManager(),"wpeditdialog"); } @@ -303,11 +308,43 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C } @Override - public void updateCoordinates(Geopoint gp) { + public void updateCoordinates(final Geopoint gp) { buttonLat.setText(gp.format(GeopointFormatter.Format.LAT_DECMINUTE)); buttonLon.setText(gp.format(GeopointFormatter.Format.LON_DECMINUTE)); } + /** + * Suffix the waypoint type with a running number to get a default name. + * + * @param type + * type to create a new default name for + * + * @return + */ + private String getDefaultWaypointName(final WaypointType type) { + final ArrayList<String> wpNames = new ArrayList<>(); + for (final Waypoint waypoint : cache.getWaypoints()) { + wpNames.add(waypoint.getName()); + } + // try final and trailhead without index + if (type == WaypointType.FINAL || type == WaypointType.TRAILHEAD) { + if (!wpNames.contains(type.getL10n())) { + return type.getL10n(); + } + } + // for other types add an index by default + int index = 1; + while (wpNames.contains(type.getL10n() + " " + index)) { + index++; + } + return type.getL10n() + " " + index; + } + + private WaypointType getSelectedWaypointType() { + final int selectedTypeIndex = waypointTypeSelector.getSelectedItemPosition(); + return selectedTypeIndex >= 0 ? POSSIBLE_WAYPOINT_TYPES.get(selectedTypeIndex) : waypoint.getWaypointType(); + } + public static final int SUCCESS = 0; public static final int UPLOAD_START = 1; public static final int UPLOAD_ERROR = 2; @@ -318,7 +355,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C private class SaveWaypointListener implements View.OnClickListener { @Override - public void onClick(View arg0) { + public void onClick(final View arg0) { final String bearingText = bearing.getText().toString(); // combine distance from EditText and distanceUnit saved from Spinner final String distanceText = distanceView.getText().toString() + distanceUnits.get(distanceUnitSelector.getSelectedItemPosition()); @@ -336,7 +373,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C if (StringUtils.isNotBlank(latText) && StringUtils.isNotBlank(lonText)) { try { coords = new Geopoint(latText, lonText); - } catch (Geopoint.ParseException e) { + } catch (final Geopoint.ParseException e) { showToast(res.getString(e.resource)); return; } @@ -354,7 +391,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C double bearing; try { bearing = Double.parseDouble(bearingText); - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { Dialogs.message(EditWaypointActivity.this, R.string.err_point_bear_and_dist_title, R.string.err_point_bear_and_dist); return; } @@ -363,7 +400,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C try { distance = DistanceParser.parseDistance(distanceText, !Settings.isUseImperialUnits()); - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { showToast(res.getString(R.string.err_parse_dist)); return; } @@ -371,19 +408,17 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C coords = coords.project(bearing, distance); } - // if no name is given, just give the waypoint its number as name final String givenName = waypointName.getText().toString().trim(); - final String name = StringUtils.isNotEmpty(givenName) ? givenName : res.getString(R.string.waypoint) + " " + (wpCount + 1); + final String name = StringUtils.defaultIfBlank(givenName, getDefaultWaypointName(getSelectedWaypointType())); final String noteText = note.getText().toString().trim(); final Geopoint coordsToSave = coords; - final int selectedTypeIndex = waypointTypeSelector.getSelectedItemPosition(); - final WaypointType type = selectedTypeIndex >= 0 ? POSSIBLE_WAYPOINT_TYPES.get(selectedTypeIndex) : waypoint.getWaypointType(); + final WaypointType type = getSelectedWaypointType(); final boolean visited = visitedCheckBox.isChecked(); final ProgressDialog progress = ProgressDialog.show(EditWaypointActivity.this, getString(R.string.cache), getString(R.string.waypoint_being_saved), true); final Handler finishHandler = new Handler() { @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { // TODO: The order of showToast, progress.dismiss and finish is different in these cases. Why? switch (msg.what) { case UPLOAD_SUCCESS: @@ -422,7 +457,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C class SaveWptTask extends AsyncTask<Void, Void, Void> { @Override - protected Void doInBackground(Void... params) { + protected Void doInBackground(final Void... params) { final Waypoint waypoint = new Waypoint(name, type, own); waypoint.setGeocode(geocode); waypoint.setPrefix(prefix); @@ -432,12 +467,12 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C waypoint.setVisited(visited); waypoint.setId(id); - Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); + final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); if (cache == null) { finishHandler.sendEmptyMessage(SAVE_ERROR); return null; } - Waypoint oldWaypoint = cache.getWaypointById(id); + final Waypoint oldWaypoint = cache.getWaypointById(id); if (cache.addOrChangeWaypoint(waypoint, true)) { DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); if (!StaticMapsProvider.hasAllStaticMapsForWaypoint(geocode, waypoint)) { @@ -460,7 +495,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C finishHandler.sendEmptyMessage(UPLOAD_START); if (cache.supportsOwnCoordinates()) { - boolean result = uploadModifiedCoords(cache, waypoint.getCoords()); + final boolean result = uploadModifiedCoords(cache, waypoint.getCoords()); finishHandler.sendEmptyMessage(result ? SUCCESS : UPLOAD_ERROR); } else { showToast(getString(R.string.waypoint_coordinates_couldnt_be_modified_on_website)); diff --git a/main/src/cgeo/geocaching/Geocache.java b/main/src/cgeo/geocaching/Geocache.java index 19082d9..8bf64dc 100644 --- a/main/src/cgeo/geocaching/Geocache.java +++ b/main/src/cgeo/geocaching/Geocache.java @@ -52,14 +52,12 @@ import rx.functions.Action0; import android.app.Activity; import android.content.Intent; import android.content.res.Resources; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.text.Html; -import android.text.Html.ImageGetter; import java.io.File; import java.util.ArrayList; @@ -70,7 +68,6 @@ import java.util.Date; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -161,10 +158,6 @@ public class Geocache implements ICache, IWaypoint { private Handler changeNotificationHandler = null; - // Images whose URL contains one of those patterns will not be available on the Images tab - // for opening into an external application. - private final static String[] NO_EXTERNAL = new String[]{"geocheck.org"}; - /** * Create a new cache. To be used everywhere except for the GPX parser */ @@ -453,11 +446,7 @@ public class Geocache implements ICache, IWaypoint { ActivityMixin.showToast(fromActivity, fromActivity.getResources().getString(R.string.err_cannot_log_visit)); return; } - final Intent logVisitIntent = new Intent(fromActivity, LogCacheActivity.class); - logVisitIntent.putExtra(LogCacheActivity.EXTRAS_ID, cacheId); - logVisitIntent.putExtra(LogCacheActivity.EXTRAS_GEOCODE, geocode); - - fromActivity.startActivity(logVisitIntent); + fromActivity.startActivity(LogCacheActivity.getLogCacheIntent(fromActivity, cacheId, geocode)); } public void logOffline(final Activity fromActivity, final LogType logType) { @@ -534,7 +523,7 @@ public class Geocache implements ICache, IWaypoint { } public boolean supportsFavoritePoints() { - return getConnector().supportsFavoritePoints(); + return getConnector().supportsFavoritePoints(this); } public boolean supportsLogging() { @@ -1521,7 +1510,6 @@ public class Geocache implements ICache, IWaypoint { @Override public void call() { refreshSynchronous(handler); - handler.sendEmptyMessage(CancellableHandler.DONE); } }); } @@ -1612,7 +1600,7 @@ public class Geocache implements ICache, IWaypoint { RxUtils.waitForCompletion(StaticMapsProvider.downloadMaps(cache), imgGetter.waitForEndObservable(handler)); if (handler != null) { - handler.sendMessage(Message.obtain()); + handler.sendEmptyMessage(CancellableHandler.DONE); } } catch (final Exception e) { Log.e("Geocache.storeCache", e); @@ -1702,23 +1690,6 @@ public class Geocache implements ICache, IWaypoint { } }; - private void addDescriptionImagesUrls(final Collection<Image> images) { - final Set<String> urls = new LinkedHashSet<>(); - for (final Image image : images) { - urls.add(image.getUrl()); - } - Html.fromHtml(getDescription(), new ImageGetter() { - @Override - public Drawable getDrawable(final String source) { - if (!urls.contains(source) && !ImageUtils.containsPattern(source, NO_EXTERNAL)) { - images.add(new Image(source, geocode)); - urls.add(source); - } - return null; - } - }, null); - } - public Collection<Image> getImages() { final LinkedList<Image> result = new LinkedList<>(); result.addAll(getSpoilers()); @@ -1726,11 +1697,7 @@ public class Geocache implements ICache, IWaypoint { for (final LogEntry log : getLogs()) { result.addAll(log.getLogImages()); } - final Set<String> urls = new HashSet<>(result.size()); - for (final Image image : result) { - urls.add(image.getUrl()); - } - addDescriptionImagesUrls(result); + ImageUtils.addImagesFromHtml(result, getDescription(), geocode); return result; } diff --git a/main/src/cgeo/geocaching/Image.java b/main/src/cgeo/geocaching/Image.java index 50ea80e..f592fc1 100644 --- a/main/src/cgeo/geocaching/Image.java +++ b/main/src/cgeo/geocaching/Image.java @@ -1,10 +1,13 @@ package cgeo.geocaching; +import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.utils.FileUtils; +import cgeo.geocaching.utils.Log; import org.apache.commons.lang3.StringUtils; -import android.content.Context; +import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; import android.os.Parcel; @@ -73,12 +76,17 @@ public class Image implements Parcelable { return description; } - public void openInBrowser(final Context fromActivity) { + public void openInBrowser(final Activity fromActivity) { if (StringUtils.isBlank(url)) { return; } final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - fromActivity.startActivity(browserIntent); + try { + fromActivity.startActivity(browserIntent); + } catch (final ActivityNotFoundException e) { + Log.e("Cannot find suitable activity", e); + ActivityMixin.showToast(fromActivity, R.string.err_application_no); + } } @Override diff --git a/main/src/cgeo/geocaching/ImageSelectActivity.java b/main/src/cgeo/geocaching/ImageSelectActivity.java index a64b4cf..3b4039e 100644 --- a/main/src/cgeo/geocaching/ImageSelectActivity.java +++ b/main/src/cgeo/geocaching/ImageSelectActivity.java @@ -48,11 +48,6 @@ public class ImageSelectActivity extends AbstractActionBarActivity { @InjectView(R.id.cancel) protected Button clearButton; @InjectView(R.id.image_preview) protected ImageView imagePreview; - static final String EXTRAS_CAPTION = "caption"; - static final String EXTRAS_DESCRIPTION = "description"; - static final String EXTRAS_URI_AS_STRING = "uri"; - static final String EXTRAS_SCALE = "scale"; - private static final String SAVED_STATE_IMAGE_CAPTION = "cgeo.geocaching.saved_state_image_caption"; private static final String SAVED_STATE_IMAGE_DESCRIPTION = "cgeo.geocaching.saved_state_image_description"; private static final String SAVED_STATE_IMAGE_URI = "cgeo.geocaching.saved_state_image_uri"; @@ -80,10 +75,10 @@ public class ImageSelectActivity extends AbstractActionBarActivity { // Get parameters from intent and basic cache information from database final Bundle extras = getIntent().getExtras(); if (extras != null) { - imageCaption = extras.getString(EXTRAS_CAPTION); - imageDescription = extras.getString(EXTRAS_DESCRIPTION); - imageUri = Uri.parse(extras.getString(EXTRAS_URI_AS_STRING)); - scaleChoiceIndex = extras.getInt(EXTRAS_SCALE, scaleChoiceIndex); + imageCaption = extras.getString(Intents.EXTRA_CAPTION); + imageDescription = extras.getString(Intents.EXTRA_DESCRIPTION); + imageUri = Uri.parse(extras.getString(Intents.EXTRA_URI_AS_STRING)); + scaleChoiceIndex = extras.getInt(Intents.EXTRA_SCALE, scaleChoiceIndex); } // Restore previous state @@ -97,7 +92,7 @@ public class ImageSelectActivity extends AbstractActionBarActivity { cameraButton.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View view) { + public void onClick(final View view) { selectImageFromCamera(); } }); @@ -105,7 +100,7 @@ public class ImageSelectActivity extends AbstractActionBarActivity { storedButton.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View view) { + public void onClick(final View view) { selectImageFromStorage(); } }); @@ -123,20 +118,20 @@ public class ImageSelectActivity extends AbstractActionBarActivity { scaleView.setSelection(scaleChoiceIndex); scaleView.setOnItemSelectedListener(new OnItemSelectedListener() { @Override - public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { + public void onItemSelected(final AdapterView<?> arg0, final View arg1, final int arg2, final long arg3) { scaleChoiceIndex = scaleView.getSelectedItemPosition(); Settings.setLogImageScale(scaleChoiceIndex); } @Override - public void onNothingSelected(AdapterView<?> arg0) { + public void onNothingSelected(final AdapterView<?> arg0) { } }); saveButton.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { saveImageInfo(true); } }); @@ -144,7 +139,7 @@ public class ImageSelectActivity extends AbstractActionBarActivity { clearButton.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { saveImageInfo(false); } }); @@ -162,17 +157,17 @@ public class ImageSelectActivity extends AbstractActionBarActivity { outState.putInt(SAVED_STATE_IMAGE_SCALE, scaleChoiceIndex); } - public void saveImageInfo(boolean saveInfo) { + public void saveImageInfo(final boolean saveInfo) { if (saveInfo) { final String filename = writeScaledImage(imageUri.getPath()); if (filename != null) { imageUri = Uri.parse(filename); final Intent intent = new Intent(); syncEditTexts(); - intent.putExtra(EXTRAS_CAPTION, imageCaption); - intent.putExtra(EXTRAS_DESCRIPTION, imageDescription); - intent.putExtra(EXTRAS_URI_AS_STRING, imageUri.toString()); - intent.putExtra(EXTRAS_SCALE, scaleChoiceIndex); + intent.putExtra(Intents.EXTRA_CAPTION, imageCaption); + intent.putExtra(Intents.EXTRA_DESCRIPTION, imageDescription); + intent.putExtra(Intents.EXTRA_URI_AS_STRING, imageUri.toString()); + intent.putExtra(Intents.EXTRA_SCALE, scaleChoiceIndex); setResult(RESULT_OK, intent); } else { showToast(res.getString(R.string.err_select_logimage_failed)); @@ -193,7 +188,7 @@ public class ImageSelectActivity extends AbstractActionBarActivity { private void selectImageFromCamera() { // create Intent to take a picture and return control to the calling application - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); imageUri = ImageUtils.getOutputImageFileUri(); // create a file to save the image if (imageUri == null) { @@ -207,14 +202,14 @@ public class ImageSelectActivity extends AbstractActionBarActivity { } private void selectImageFromStorage() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/jpeg"); startActivityForResult(Intent.createChooser(intent, "Select Image"), SELECT_STORED_IMAGE); } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { + protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { if (resultCode == RESULT_CANCELED) { // User cancelled the image capture showToast(getResources().getString(R.string.info_select_logimage_cancelled)); @@ -232,7 +227,7 @@ public class ImageSelectActivity extends AbstractActionBarActivity { if (data != null) { final Uri selectedImage = data.getData(); if (Build.VERSION.SDK_INT < VERSION_CODES.KITKAT) { - String[] filePathColumn = { MediaColumns.DATA }; + final String[] filePathColumn = { MediaColumns.DATA }; Cursor cursor = null; try { @@ -243,14 +238,14 @@ public class ImageSelectActivity extends AbstractActionBarActivity { } cursor.moveToFirst(); - int columnIndex = cursor.getColumnIndex(filePathColumn[0]); - String filePath = cursor.getString(columnIndex); + final int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + final String filePath = cursor.getString(columnIndex); if (StringUtils.isBlank(filePath)) { showFailure(); return; } imageUri = Uri.parse(filePath); - } catch (Exception e) { + } catch (final Exception e) { Log.e("ImageSelectActivity.onActivityResult", e); showFailure(); } finally { @@ -269,7 +264,7 @@ public class ImageSelectActivity extends AbstractActionBarActivity { output = new FileOutputStream(outputFile); LocalStorage.copy(input, output); imageUri = Uri.fromFile(outputFile); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { Log.e("ImageSelectActivity.onStartResult", e); } finally { IOUtils.closeQuietly(input); diff --git a/main/src/cgeo/geocaching/Intents.java b/main/src/cgeo/geocaching/Intents.java index a55c22a..e2b204e 100644 --- a/main/src/cgeo/geocaching/Intents.java +++ b/main/src/cgeo/geocaching/Intents.java @@ -1,5 +1,13 @@ package cgeo.geocaching; +import cgeo.geocaching.enumerations.CacheListType; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; + +import android.content.Intent; +import android.os.Bundle; + public class Intents { private Intents() { @@ -18,7 +26,22 @@ public class Intents { public static final String EXTRA_KEYWORD = PREFIX + "keyword"; public static final String EXTRA_KEYWORD_SEARCH = PREFIX + "keyword_search"; public static final String EXTRA_LIST_ID = PREFIX + "list_id"; - public static final String EXTRA_LIST_TYPE = PREFIX + "list_type"; + public static final String EXTRA_CAPTION = PREFIX + "caption"; + public static final String EXTRA_DESCRIPTION = PREFIX + "description"; + public static final String EXTRA_URI_AS_STRING = PREFIX + "uri"; + public static final String EXTRA_SCALE = PREFIX + "scale"; + + public static final String EXTRA_WPTTYPE = PREFIX + "wpttype"; + public static final String EXTRA_MAPSTATE = PREFIX + "mapstate"; + public static final String EXTRA_MAP_TITLE = PREFIX + "mapTitle"; + public static final String EXTRA_MAP_MODE = PREFIX + "mapMode"; + public static final String EXTRA_LIVE_ENABLED = PREFIX + "liveEnabled"; + + /** + * list type to be used with the cache list activity. Be aware to use the String representation of the corresponding + * enum. + */ + private static final String EXTRA_LIST_TYPE = PREFIX + "list_type"; public static final String EXTRA_MAP_FILE = PREFIX + "map_file"; public static final String EXTRA_NAME = PREFIX + "name"; public static final String EXTRA_SEARCH = PREFIX + "search"; @@ -49,4 +72,27 @@ public class Intents { public static final String EXTRA_OAUTH_TEMP_TOKEN_SECRET_PREF = PREFIX_OAUTH + "tempSecretPref"; public static final String EXTRA_OAUTH_TOKEN_PUBLIC_KEY = PREFIX_OAUTH + "publicTokenPref"; public static final String EXTRA_OAUTH_TOKEN_SECRET_KEY = PREFIX_OAUTH + "secretTokenPref"; + + public static Intent putListType(final Intent intent, final @NonNull CacheListType listType) { + intent.putExtra(Intents.EXTRA_LIST_TYPE, listType.name()); + return intent; + } + + public static @NonNull CacheListType getListType(final Intent intent) { + final Bundle extras = intent.getExtras(); + if (extras == null) { + return CacheListType.OFFLINE; + } + final String typeName = extras.getString(Intents.EXTRA_LIST_TYPE); + if (StringUtils.isBlank(typeName)) { + return CacheListType.OFFLINE; + } + CacheListType listType; + try { + listType = CacheListType.valueOf(typeName); + } catch (final IllegalArgumentException e) { + return CacheListType.OFFLINE; + } + return (listType != null) ? listType : CacheListType.OFFLINE; + } } diff --git a/main/src/cgeo/geocaching/LogCacheActivity.java b/main/src/cgeo/geocaching/LogCacheActivity.java index 75d4c2c..3558b3c 100644 --- a/main/src/cgeo/geocaching/LogCacheActivity.java +++ b/main/src/cgeo/geocaching/LogCacheActivity.java @@ -2,6 +2,7 @@ package cgeo.geocaching; import butterknife.ButterKnife; +import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.connector.ILoggingManager; import cgeo.geocaching.connector.ImageResult; import cgeo.geocaching.connector.LogResult; @@ -21,6 +22,8 @@ import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.LogTemplateProvider; import cgeo.geocaching.utils.LogTemplateProvider.LogContext; +import com.github.amlcurran.showcaseview.targets.ActionItemTarget; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -51,8 +54,6 @@ import java.util.Date; import java.util.List; public class LogCacheActivity extends AbstractLoggingActivity implements DateDialog.DateDialogParent { - static final String EXTRAS_GEOCODE = "geocode"; - static final String EXTRAS_ID = "id"; private static final String SAVED_STATE_RATING = "cgeo.geocaching.saved_state_rating"; private static final String SAVED_STATE_TYPE = "cgeo.geocaching.saved_state_type"; @@ -196,7 +197,6 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia private void enablePostButton(final boolean enabled) { sendButtonEnabled = enabled; - invalidateOptionsMenuCompatible(); } @Override @@ -206,9 +206,9 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia // Get parameters from intent and basic cache information from database final Bundle extras = getIntent().getExtras(); if (extras != null) { - geocode = extras.getString(EXTRAS_GEOCODE); + geocode = extras.getString(Intents.EXTRA_GEOCODE); if (StringUtils.isBlank(geocode)) { - final String cacheid = extras.getString(EXTRAS_ID); + final String cacheid = extras.getString(Intents.EXTRA_ID); if (StringUtils.isNotBlank(cacheid)) { geocode = DataStore.getGeocodeForGuid(cacheid); } @@ -422,7 +422,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia if (logResult.getPostLogResult() == StatusCode.NO_ERROR) { // update geocache in DB - if (typeSelected == LogType.FOUND_IT || typeSelected == LogType.ATTENDED || typeSelected == LogType.WEBCAM_PHOTO_TAKEN) { + if (typeSelected.isFoundLog()) { cache.setFound(true); cache.setVisitedDate(new Date().getTime()); } @@ -577,9 +577,9 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia private void selectImage() { final Intent selectImageIntent = new Intent(this, ImageSelectActivity.class); - selectImageIntent.putExtra(ImageSelectActivity.EXTRAS_CAPTION, imageCaption); - selectImageIntent.putExtra(ImageSelectActivity.EXTRAS_DESCRIPTION, imageDescription); - selectImageIntent.putExtra(ImageSelectActivity.EXTRAS_URI_AS_STRING, imageUri.toString()); + selectImageIntent.putExtra(Intents.EXTRA_CAPTION, imageCaption); + selectImageIntent.putExtra(Intents.EXTRA_DESCRIPTION, imageDescription); + selectImageIntent.putExtra(Intents.EXTRA_URI_AS_STRING, imageUri.toString()); startActivityForResult(selectImageIntent, SELECT_IMAGE); } @@ -588,9 +588,9 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { if (requestCode == SELECT_IMAGE) { if (resultCode == RESULT_OK) { - imageCaption = data.getStringExtra(ImageSelectActivity.EXTRAS_CAPTION); - imageDescription = data.getStringExtra(ImageSelectActivity.EXTRAS_DESCRIPTION); - imageUri = Uri.parse(data.getStringExtra(ImageSelectActivity.EXTRAS_URI_AS_STRING)); + imageCaption = data.getStringExtra(Intents.EXTRA_CAPTION); + imageDescription = data.getStringExtra(Intents.EXTRA_DESCRIPTION); + imageUri = Uri.parse(data.getStringExtra(Intents.EXTRA_URI_AS_STRING)); } else if (resultCode != RESULT_CANCELED) { // Image capture failed, advise user showToast(getResources().getString(R.string.err_select_logimage_failed)); @@ -602,7 +602,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_send: - sendLog(); + sendLogAndConfirm(); return true; case R.id.menu_image: selectImage(); @@ -621,25 +621,53 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia return super.onOptionsItemSelected(item); } - private void sendLog() { + private void sendLogAndConfirm() { if (!sendButtonEnabled) { Dialogs.message(this, R.string.log_post_not_possible); + return; + } + if (typeSelected.mustConfirmLog()) { + Dialogs.confirm(this, R.string.confirm_log_title, res.getString(R.string.confirm_log_message, typeSelected.getL10n()), new OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + sendLogInternal(); + } + }); } else { - final String message = res.getString(StringUtils.isBlank(imageUri.getPath()) ? - R.string.log_saving : - R.string.log_saving_and_uploading); - new Poster(this, message).execute(currentLogText(), currentLogPassword()); + sendLogInternal(); } } + private void sendLogInternal() { + final String message = res.getString(StringUtils.isBlank(imageUri.getPath()) ? + R.string.log_saving : + R.string.log_saving_and_uploading); + new Poster(this, message).execute(currentLogText(), currentLogPassword()); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); menu.findItem(R.id.menu_image).setVisible(cache.supportsLogImages()); menu.findItem(R.id.save).setVisible(true); menu.findItem(R.id.clear).setVisible(true); + presentShowcase(); return true; } + @Override + public ShowcaseViewBuilder getShowcase() { + return new ShowcaseViewBuilder(this) + .setTarget(new ActionItemTarget(this, R.id.menu_send)) + .setContent(R.string.showcase_logcache_title, R.string.showcase_logcache_text); + } + + public static Intent getLogCacheIntent(final Activity context, final String cacheId, final String geocode) { + final Intent logVisitIntent = new Intent(context, LogCacheActivity.class); + logVisitIntent.putExtra(Intents.EXTRA_ID, cacheId); + logVisitIntent.putExtra(Intents.EXTRA_GEOCODE, geocode); + return logVisitIntent; + } } diff --git a/main/src/cgeo/geocaching/LogTrackableActivity.java b/main/src/cgeo/geocaching/LogTrackableActivity.java index 5970210..5c6d0f5 100644 --- a/main/src/cgeo/geocaching/LogTrackableActivity.java +++ b/main/src/cgeo/geocaching/LogTrackableActivity.java @@ -53,6 +53,9 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat private String guid = null; private String geocode = null; private String[] viewstates = null; + /** + * As long as we still fetch the current state of the trackable from the Internet, the user cannot yet send a log. + */ private boolean gettingViewstate = true; private Calendar date = Calendar.getInstance(); private LogType typeSelected = LogType.getById(Settings.getTrackableAction()); @@ -88,7 +91,6 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat } gettingViewstate = false; // we're done, user can post log - setLoggingEnabled(true); showProgress(false); } @@ -211,10 +213,7 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat } if (GCLogin.isEmpty(viewstates)) { - setLoggingEnabled(false); new LoadDataThread().start(); - } else { - setLoggingEnabled(true); } disableSuggestions(trackingEditText); } diff --git a/main/src/cgeo/geocaching/MainActivity.java b/main/src/cgeo/geocaching/MainActivity.java index 3723116..40504fb 100644 --- a/main/src/cgeo/geocaching/MainActivity.java +++ b/main/src/cgeo/geocaching/MainActivity.java @@ -4,6 +4,7 @@ import butterknife.ButterKnife; import butterknife.InjectView; import cgeo.geocaching.activity.AbstractActionBarActivity; +import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ILogin; import cgeo.geocaching.enumerations.CacheType; @@ -27,6 +28,7 @@ import cgeo.geocaching.utils.RxUtils; import cgeo.geocaching.utils.TextUtils; import cgeo.geocaching.utils.Version; +import com.github.amlcurran.showcaseview.targets.ActionViewTarget; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; @@ -211,6 +213,9 @@ public class MainActivity extends AbstractActionBarActivity { super.onResume(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA | GeoDirHandler.LOW_POWER), app.gpsStatusObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(satellitesHandler)); updateUserInfoHandler.sendEmptyMessage(-1); + if (app.hasValidLocation()) { + locationUpdater.updateGeoData(app.currentGeo()); + } startBackgroundLogin(); init(); } @@ -264,7 +269,7 @@ public class MainActivity extends AbstractActionBarActivity { final MenuItem searchItem = menu.findItem(R.id.menu_gosearch); final SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); - + presentShowcase(); return true; } @@ -293,7 +298,7 @@ public class MainActivity extends AbstractActionBarActivity { startActivity(new Intent(this, SettingsActivity.class)); return true; case R.id.menu_history: - CacheListActivity.startActivityHistory(this); + startActivity(CacheListActivity.getHistoryIntent(this)); return true; case R.id.menu_scan: startScannerApplication(); @@ -356,8 +361,6 @@ public class MainActivity extends AbstractActionBarActivity { initialized = true; - Settings.setLanguage(Settings.isUseEnglish()); - findOnMap.setClickable(true); findOnMap.setOnClickListener(new OnClickListener() { @Override @@ -558,7 +561,7 @@ public class MainActivity extends AbstractActionBarActivity { } } }); - AndroidObservable.bindActivity(MainActivity.this, address.onErrorResumeNext(Observable.from(geo.getCoords().toString()))) + AndroidObservable.bindActivity(MainActivity.this, address.onErrorResumeNext(Observable.just(geo.getCoords().toString()))) .subscribeOn(RxUtils.networkScheduler) .subscribe(new Action1<String>() { @Override @@ -579,7 +582,7 @@ public class MainActivity extends AbstractActionBarActivity { */ public void cgeoFindOnMap(final View v) { findOnMap.setPressed(true); - CGeoMap.startActivityLiveMap(this); + startActivity(CGeoMap.getLiveMapIntent(this)); } /** @@ -592,7 +595,7 @@ public class MainActivity extends AbstractActionBarActivity { } nearestView.setPressed(true); - CacheListActivity.startActivityNearest(this, app.currentGeo().getCoords()); + startActivity(CacheListActivity.getNearestIntent(this)); } /** @@ -735,4 +738,11 @@ public class MainActivity extends AbstractActionBarActivity { public void showAbout(final View view) { startActivity(new Intent(this, AboutActivity.class)); } + + @Override + public ShowcaseViewBuilder getShowcase() { + return new ShowcaseViewBuilder(this) + .setTarget(new ActionViewTarget(this, ActionViewTarget.Type.OVERFLOW)) + .setContent(R.string.showcase_main_title, R.string.showcase_main_text); + } } diff --git a/main/src/cgeo/geocaching/SearchActivity.java b/main/src/cgeo/geocaching/SearchActivity.java index 81dec98..edd611a 100644 --- a/main/src/cgeo/geocaching/SearchActivity.java +++ b/main/src/cgeo/geocaching/SearchActivity.java @@ -4,6 +4,7 @@ import butterknife.ButterKnife; import butterknife.InjectView; import cgeo.geocaching.activity.AbstractActionBarActivity; +import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.capability.ISearchByGeocode; @@ -177,14 +178,14 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin buttonLatitude.setOnClickListener(new OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { updateCoordinates(); } }); buttonLongitude.setOnClickListener(new OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { updateCoordinates(); } }); @@ -398,6 +399,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin @Override public final boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.search_activity_options, menu); + presentShowcase(); return true; } @@ -417,4 +419,13 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin putExtra(Intents.EXTRA_KEYWORD_SEARCH, false); fromActivity.startActivityForResult(searchIntent, MainActivity.SEARCH_REQUEST_CODE); } + + @Override + public ShowcaseViewBuilder getShowcase() { + // The showcase doesn't work well with the search activity, because on searching a geocode (or + // selecting a cache from the search field) we immediately close the activity. That in turn confuses the delayed + // creation of the showcase bitmap. To avoid someone running into this issue again, this method explicitly overrides + // the parent method with the same implementation. + return null; + } } diff --git a/main/src/cgeo/geocaching/SearchResult.java b/main/src/cgeo/geocaching/SearchResult.java index 74cc59d..6015872 100644 --- a/main/src/cgeo/geocaching/SearchResult.java +++ b/main/src/cgeo/geocaching/SearchResult.java @@ -320,7 +320,7 @@ public class SearchResult implements Parcelable { 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(); + return c.isActive() ? Observable.just(func.call(c)) : Observable.<SearchResult>empty(); } }); } diff --git a/main/src/cgeo/geocaching/StatusFragment.java b/main/src/cgeo/geocaching/StatusFragment.java index a228363..3fd4434 100644 --- a/main/src/cgeo/geocaching/StatusFragment.java +++ b/main/src/cgeo/geocaching/StatusFragment.java @@ -9,7 +9,6 @@ import cgeo.geocaching.utils.Log; import rx.Subscription; import rx.android.observables.AndroidObservable; import rx.functions.Action1; -import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; import android.content.Intent; @@ -34,7 +33,7 @@ public class StatusFragment extends Fragment { final ViewGroup statusGroup = (ViewGroup) inflater.inflate(R.layout.status, container, false); final ImageView statusIcon = ButterKnife.findById(statusGroup, R.id.status_icon); final TextView statusMessage = ButterKnife.findById(statusGroup, R.id.status_message); - statusSubscription = AndroidObservable.bindFragment(this, StatusUpdater.latestStatus).subscribeOn(Schedulers.io()) + statusSubscription = AndroidObservable.bindFragment(this, StatusUpdater.latestStatus) .subscribe(new Action1<Status>() { @Override public void call(final Status status) { diff --git a/main/src/cgeo/geocaching/Trackable.java b/main/src/cgeo/geocaching/Trackable.java index 9c2b044..fe53109 100644 --- a/main/src/cgeo/geocaching/Trackable.java +++ b/main/src/cgeo/geocaching/Trackable.java @@ -3,13 +3,16 @@ package cgeo.geocaching; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.trackable.TrackableConnector; import cgeo.geocaching.enumerations.LogType; +import cgeo.geocaching.utils.ImageUtils; import org.apache.commons.lang3.StringUtils; import android.text.Html; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; +import java.util.LinkedList; import java.util.List; public class Trackable implements ILogable { @@ -214,6 +217,18 @@ public class Trackable implements ILogable { this.trackingcode = trackingcode; } + public Collection<Image> getImages() { + final List<Image> images = new LinkedList<>(); + if (StringUtils.isNotBlank(image)) { + images.add(new Image(image, StringUtils.defaultIfBlank(name, geocode))); + } + ImageUtils.addImagesFromHtml(images, getDetails(), geocode); + for (final LogEntry log : getLogs()) { + images.addAll(log.getLogImages()); + } + return images; + } + static public List<LogType> getPossibleLogTypes() { final List<LogType> logTypes = new ArrayList<>(); logTypes.add(LogType.RETRIEVED_IT); diff --git a/main/src/cgeo/geocaching/TrackableActivity.java b/main/src/cgeo/geocaching/TrackableActivity.java index dadc37f..81516c3 100644 --- a/main/src/cgeo/geocaching/TrackableActivity.java +++ b/main/src/cgeo/geocaching/TrackableActivity.java @@ -4,6 +4,7 @@ import butterknife.ButterKnife; import butterknife.InjectView; import cgeo.geocaching.activity.AbstractActivity; +import cgeo.geocaching.activity.AbstractActivity.ActivitySharingInterface; import cgeo.geocaching.activity.AbstractViewPagerActivity; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.trackable.TrackableConnector; @@ -14,29 +15,33 @@ import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.ui.AbstractCachingPageViewCreator; import cgeo.geocaching.ui.AnchorAwareLinkMovementMethod; import cgeo.geocaching.ui.CacheDetailsCreator; +import cgeo.geocaching.ui.ImagesList; import cgeo.geocaching.ui.UserActionsClickListener; import cgeo.geocaching.ui.UserNameClickListener; import cgeo.geocaching.ui.logs.TrackableLogsViewCreator; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; import cgeo.geocaching.utils.UnknownTagsHandler; +import org.apache.commons.collections4.CollectionUtils; 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.android.observables.AndroidObservable; import rx.android.observables.ViewObservable; import rx.functions.Action1; +import rx.functions.Func0; +import rx.subscriptions.CompositeSubscription; import android.app.ProgressDialog; import android.content.Intent; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; import android.support.v7.app.ActionBar; import android.support.v7.view.ActionMode; import android.text.Html; @@ -56,11 +61,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivity.Page> { +public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivity.Page> implements ActivitySharingInterface { + + private CompositeSubscription createSubscriptions; public enum Page { DETAILS(R.string.detail), - LOGS(R.string.cache_logs); + LOGS(R.string.cache_logs), + IMAGES(R.string.cache_images); private final int resId; @@ -76,59 +84,9 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi private String id = null; private LayoutInflater inflater = null; private ProgressDialog waitDialog = null; - private final Handler loadTrackableHandler = new Handler() { - - @Override - public void handleMessage(final Message msg) { - if (trackable == null) { - if (waitDialog != null) { - waitDialog.dismiss(); - } - - if (StringUtils.isNotBlank(geocode)) { - showToast(res.getString(R.string.err_tb_find) + " " + geocode + "."); - } else { - showToast(res.getString(R.string.err_tb_find_that)); - } - - finish(); - return; - } - - try { - inflater = getLayoutInflater(); - geocode = trackable.getGeocode(); - - if (StringUtils.isNotBlank(trackable.getName())) { - setTitle(Html.fromHtml(trackable.getName()).toString()); - } else { - setTitle(trackable.getName()); - } - - invalidateOptionsMenuCompatible(); - reinitializeViewPager(); - - } catch (final Exception e) { - Log.e("TrackableActivity.loadTrackableHandler: ", e); - } - - if (waitDialog != null) { - waitDialog.dismiss(); - } - - // if we have a newer Android device setup Android Beam for easy cache sharing - initializeAndroidBeam( - new ActivitySharingInterface() { - @Override - public String getUri() { - return trackable.getUrl(); - } - } - ); - } - }; - private CharSequence clickedItemText = null; + private ImagesList imagesList = null; + /** * Action mode of the current contextual action bar (e.g. for copy and share actions). */ @@ -209,11 +167,33 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi } else { message = res.getString(R.string.trackable); } + + // If we have a newer Android device setup Android Beam for easy cache sharing + initializeAndroidBeam(this); + + createViewPager(0, new OnPageSelectedListener() { + @Override + public void onPageSelected(final int position) { + // Lazy loading of trackable images + if (getPage(position) == Page.IMAGES) { + loadTrackableImages(); + } + } + }); waitDialog = ProgressDialog.show(this, message, res.getString(R.string.trackable_details_loading), true, true); + createSubscriptions = new CompositeSubscription(); + createSubscriptions.add(AndroidObservable.bindActivity(this, loadTrackable(geocode, guid, id)).singleOrDefault(null).subscribe(new Action1<Trackable>() { + @Override + public void call(final Trackable trackable) { + TrackableActivity.this.trackable = trackable; + displayTrackable(); + } + })); + } - createViewPager(0, null); - final LoadTrackableThread thread = new LoadTrackableThread(loadTrackableHandler, geocode, guid, id); - thread.start(); + @Override + public String getAndroidBeamUri() { + return trackable != null ? trackable.getUrl() : null; } @Override @@ -244,87 +224,85 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi return super.onPrepareOptionsMenu(menu); } - private class LoadTrackableThread extends Thread { - final private Handler handler; - final private String geocode; - final private String guid; - final private String id; - - public LoadTrackableThread(final Handler handlerIn, final String geocodeIn, final String guidIn, final String idIn) { - handler = handlerIn; - geocode = geocodeIn; - guid = guidIn; - id = idIn; - } - - @Override - public void run() { - if (StringUtils.isNotEmpty(geocode)) { - - // iterate over the connectors as some codes may be handled by multiple connectors - for (final TrackableConnector trackableConnector : ConnectorFactory.getTrackableConnectors()) { - if (trackableConnector.canHandleTrackable(geocode)) { - trackable = trackableConnector.searchTrackable(geocode, guid, id); - if (trackable != null) { - break; + private static Observable<Trackable> loadTrackable(final String geocode, final String guid, final String id) { + return Observable.defer(new Func0<Observable<Trackable>>() { + @Override + public Observable<Trackable> call() { + if (StringUtils.isNotEmpty(geocode)) { + // iterate over the connectors as some codes may be handled by multiple connectors + for (final TrackableConnector trackableConnector : ConnectorFactory.getTrackableConnectors()) { + if (trackableConnector.canHandleTrackable(geocode)) { + final Trackable trackable = trackableConnector.searchTrackable(geocode, guid, id); + if (trackable != null) { + return Observable.just(trackable); + } } } + // Check local storage (offline case) + final Trackable trackable = DataStore.loadTrackable(geocode); + if (trackable != null) { + return Observable.just(trackable); + } } - // Check local storage (offline case) - if (trackable == null) { - trackable = DataStore.loadTrackable(geocode); - } - } - // fall back to GC search by GUID - if (trackable == null) { - trackable = TravelBugConnector.getInstance().searchTrackable(geocode, guid, id); + + // Fall back to GC search by GUID + final Trackable trackable = TravelBugConnector.getInstance().searchTrackable(geocode, guid, id); + return trackable != null ? Observable.just(trackable) : Observable.<Trackable>empty(); } - handler.sendMessage(Message.obtain()); - } + }).subscribeOn(RxUtils.networkScheduler); } - private class TrackableIconThread extends Thread { - final private String url; - final private Handler handler; + public void displayTrackable() { + if (trackable == null) { + if (waitDialog != null) { + waitDialog.dismiss(); + } + + if (StringUtils.isNotBlank(geocode)) { + showToast(res.getString(R.string.err_tb_find) + " " + geocode + "."); + } else { + showToast(res.getString(R.string.err_tb_find_that)); + } - public TrackableIconThread(final String urlIn, final Handler handlerIn) { - url = urlIn; - handler = handlerIn; + finish(); + return; } - @Override - public void run() { - if (url == null || handler == null) { - return; + try { + inflater = getLayoutInflater(); + geocode = trackable.getGeocode(); + + if (StringUtils.isNotBlank(trackable.getName())) { + setTitle(Html.fromHtml(trackable.getName()).toString()); + } else { + setTitle(trackable.getName()); } - try { - final HtmlImage imgGetter = new HtmlImage(trackable.getGeocode(), false, 0, false); + invalidateOptionsMenuCompatible(); + reinitializeViewPager(); - final BitmapDrawable image = imgGetter.getDrawable(url); - final Message message = handler.obtainMessage(0, image); - handler.sendMessage(message); - } catch (final Exception e) { - Log.e("TrackableActivity.TrackableIconThread.run: ", e); - } + } catch (final Exception e) { + Log.e("TrackableActivity.loadTrackableHandler: ", e); } - } - - private static class TrackableIconHandler extends Handler { - final private ActionBar view; - public TrackableIconHandler(final ActionBar viewIn) { - view = viewIn; + if (waitDialog != null) { + waitDialog.dismiss(); } - @Override - public void handleMessage(final Message message) { - final BitmapDrawable image = (BitmapDrawable) message.obj; - if (image != null && view != null) { - image.setBounds(0, 0, view.getHeight(), view.getHeight()); - view.setIcon(image); + } + + private void setupIcon(final ActionBar actionBar, final String url) { + final HtmlImage imgGetter = new HtmlImage(HtmlImage.SHARED, false, 0, false); + AndroidObservable.bindActivity(this, imgGetter.fetchDrawable(url)).subscribe(new Action1<BitmapDrawable>() { + @Override + public void call(final BitmapDrawable image) { + if (actionBar != null) { + final int height = actionBar.getHeight(); + image.setBounds(0, 0, height, height); + actionBar.setIcon(image); + } } - } + }); } public static void startActivity(final AbstractActivity fromContext, @@ -343,10 +321,37 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi return new DetailsViewCreator(); case LOGS: return new TrackableLogsViewCreator(this); + case IMAGES: + return new ImagesViewCreator(); } throw new IllegalStateException(); // cannot happen as long as switch case is enum complete } + private class ImagesViewCreator extends AbstractCachingPageViewCreator<View> { + + @Override + public View getDispatchedView(final ViewGroup parentView) { + view = getLayoutInflater().inflate(R.layout.cachedetail_images_page, parentView, false); + return view; + } + } + + private void loadTrackableImages() { + if (imagesList != null) { + return; + } + final PageViewCreator creator = getViewCreator(Page.IMAGES); + if (creator == null) { + return; + } + final View imageView = creator.getView(null); + if (imageView == null) { + return; + } + imagesList = new ImagesList(this, trackable.getGeocode()); + createSubscriptions.add(imagesList.loadImages(imageView, trackable.getImages(), false)); + } + @Override protected String getTitle(final Page page) { return res.getString(page.resId); @@ -356,9 +361,12 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi protected Pair<List<? extends Page>, Integer> getOrderedPages() { final List<Page> pages = new ArrayList<>(); pages.add(Page.DETAILS); - if (!trackable.getLogs().isEmpty()) { + if (CollectionUtils.isNotEmpty(trackable.getLogs())) { pages.add(Page.LOGS); } + if (CollectionUtils.isNotEmpty(trackable.getImages())) { + pages.add(Page.IMAGES); + } return new ImmutablePair<List<? extends Page>, Integer>(pages, 0); } @@ -381,9 +389,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi // action bar icon if (StringUtils.isNotBlank(trackable.getIconUrl())) { - final TrackableIconHandler iconHandler = new TrackableIconHandler(getSupportActionBar()); - final TrackableIconThread iconThread = new TrackableIconThread(trackable.getIconUrl(), iconHandler); - iconThread.start(); + setupIcon(getSupportActionBar(), trackable.getIconUrl()); } // trackable name @@ -432,8 +438,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi if (showTimeSpan && trackable.getLogs() != null) { for (final LogEntry log : trackable.getLogs()) { if (log.type == LogType.RETRIEVED_IT || log.type == LogType.GRABBED_IT || log.type == LogType.DISCOVERED_IT || log.type == LogType.PLACED_IT) { - final int days = log.daysSinceLog(); - text.append(" (").append(res.getQuantityString(R.plurals.days_ago, days, days)).append(')'); + text.append(" (").append(Formatter.formatDaysAgo(log.date)).append(')'); break; } } @@ -442,21 +447,21 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi final TextView spotted = details.add(R.string.trackable_spotted, text.toString()); spotted.setClickable(true); if (Trackable.SPOTTED_CACHE == trackable.getSpottedType()) { - spotted.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View arg0) { - final String cacheGeocode = DataStore.getGeocodeForGuid(trackable.getSpottedGuid()); - if (StringUtils.isNotBlank(cacheGeocode)) { - CacheDetailActivity.startActivity(TrackableActivity.this, cacheGeocode, trackable.getSpottedName()); - } else { - // for geokrety we only know the cache geocode - final String cacheCode = trackable.getSpottedName(); - if (ConnectorFactory.canHandle(cacheCode)) { - CacheDetailActivity.startActivity(TrackableActivity.this, cacheCode); - } + spotted.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View arg0) { + if (StringUtils.isNotBlank(trackable.getSpottedGuid())) { + CacheDetailActivity.startActivityGuid(TrackableActivity.this, trackable.getSpottedGuid(), trackable.getSpottedName()); + } + else { + // for geokrety we only know the cache geocode + final String cacheCode = trackable.getSpottedName(); + if (ConnectorFactory.canHandle(cacheCode)) { + CacheDetailActivity.startActivity(TrackableActivity.this, cacheCode); } } - }); + } + }); } else if (Trackable.SPOTTED_USER == trackable.getSpottedType()) { spotted.setOnClickListener(new UserNameClickListener(trackable, Html.fromHtml(trackable.getSpottedName()).toString())); } else if (Trackable.SPOTTED_OWNER == trackable.getSpottedType()) { @@ -559,7 +564,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi 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(); + final CharSequence itemTitle = ((TextView) ((View) view.getParent()).findViewById(R.id.name)).getText(); buildDetailsContextMenu(actionMode, menu, clickedItemText, itemTitle, true); return true; case R.id.goal: @@ -605,6 +610,12 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi } } + @Override + protected void onDestroy() { + createSubscriptions.unsubscribe(); + super.onDestroy(); + } + public Trackable getTrackable() { return trackable; } diff --git a/main/src/cgeo/geocaching/Waypoint.java b/main/src/cgeo/geocaching/Waypoint.java index 7381aab..b2c5305 100644 --- a/main/src/cgeo/geocaching/Waypoint.java +++ b/main/src/cgeo/geocaching/Waypoint.java @@ -21,13 +21,13 @@ public class Waypoint implements IWaypoint { public static final String PREFIX_OWN = "OWN"; private static final int ORDER_UNDEFINED = -2; + private static final Pattern PATTERN_COORDS = Pattern.compile("\\b[nNsS]\\s*\\d"); private int id = -1; private String geocode = "geocode"; private WaypointType waypointType = WaypointType.WAYPOINT; private String prefix = ""; private String lookup = ""; private String name = ""; - private String latlon = ""; private Geopoint coords = null; private String note = ""; private int cachedOrder = ORDER_UNDEFINED; @@ -67,9 +67,6 @@ public class Waypoint implements IWaypoint { if (StringUtils.isBlank(name)) { setName(old.name); } - if (StringUtils.isBlank(latlon) || latlon.startsWith("?")) { // there are waypoints containing "???" - latlon = old.latlon; - } if (coords == null) { coords = old.coords; } @@ -204,14 +201,6 @@ public class Waypoint implements IWaypoint { this.name = name; } - public String getLatlon() { - return latlon; - } - - public void setLatlon(final String latlon) { - this.latlon = latlon; - } - @Override public Geopoint getCoords() { return coords; @@ -303,10 +292,9 @@ public class Waypoint implements IWaypoint { */ public static Collection<Waypoint> parseWaypointsFromNote(@NonNull final String initialNote) { final List<Waypoint> waypoints = new LinkedList<>(); - final Pattern COORDPATTERN = Pattern.compile("\\b[nNsS]{1}\\s*\\d"); // begin of coordinates String note = initialNote; - MatcherWrapper matcher = new MatcherWrapper(COORDPATTERN, note); + MatcherWrapper matcher = new MatcherWrapper(PATTERN_COORDS, note); int count = 1; while (matcher.find()) { try { @@ -321,12 +309,11 @@ public class Waypoint implements IWaypoint { waypoints.add(waypoint); count++; } - } catch (final Geopoint.ParseException e) { - // ignore + } catch (final Geopoint.ParseException ignore) { } note = note.substring(matcher.start() + 1); - matcher = new MatcherWrapper(COORDPATTERN, note); + matcher = new MatcherWrapper(PATTERN_COORDS, note); } return waypoints; } diff --git a/main/src/cgeo/geocaching/activity/AbstractActivity.java b/main/src/cgeo/geocaching/activity/AbstractActivity.java index a28fcfa..603211e 100644 --- a/main/src/cgeo/geocaching/activity/AbstractActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractActivity.java @@ -60,26 +60,36 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs } @Override - public final void showToast(String text) { + public final void showToast(final String text) { ActivityMixin.showToast(this, text); } @Override - public final void showShortToast(String text) { + public final void showShortToast(final String text) { ActivityMixin.showShortToast(this, text); } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); initializeCommonFields(); + } + + @Override + public final void presentShowcase() { + ActivityMixin.presentShowcase(this); + } + @Override + public ShowcaseViewBuilder getShowcase() { + // do nothing by default + return null; } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { return ActivityMixin.navigateUp(this); } @@ -116,7 +126,6 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs } protected void onCreate(final Bundle savedInstanceState, final int resourceLayoutID) { - super.onCreate(savedInstanceState); initializeCommonFields(); @@ -137,11 +146,11 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs // only needed in some activities, but implemented in super class nonetheless Cookies.restoreCookieStore(Settings.getCookieStore()); - ActivityMixin.keepScreenOn(this, keepScreenOn); + ActivityMixin.onCreate(this, keepScreenOn); } @Override - public void setContentView(int layoutResID) { + public void setContentView(final int layoutResID) { super.setContentView(layoutResID); // initialize the action bar title with the activity title for single source @@ -202,27 +211,27 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs // these are so few that we don't want to deal with the older (non Android Beam) API public interface ActivitySharingInterface { - /** Return an URL that represent the current activity for sharing */ - public String getUri(); + /** Return an URL that represent the current activity for sharing or null for no sharing. */ + public String getAndroidBeamUri(); } - protected void initializeAndroidBeam(ActivitySharingInterface sharingInterface) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + protected void initializeAndroidBeam(final ActivitySharingInterface sharingInterface) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { initializeICSAndroidBeam(sharingInterface); } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) protected void initializeICSAndroidBeam(final ActivitySharingInterface sharingInterface) { - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + final NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); if (nfcAdapter == null) { return; } nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { @Override - public NdefMessage createNdefMessage(NfcEvent event) { - NdefRecord record = NdefRecord.createUri(sharingInterface.getUri()); - return new NdefMessage(new NdefRecord[]{record}); + public NdefMessage createNdefMessage(final NfcEvent event) { + final String uri = sharingInterface.getAndroidBeamUri(); + return uri != null ? new NdefMessage(new NdefRecord[]{NdefRecord.createUri(uri)}) : null; } }, this); diff --git a/main/src/cgeo/geocaching/activity/AbstractListActivity.java b/main/src/cgeo/geocaching/activity/AbstractListActivity.java index eac191a..d7482c3 100644 --- a/main/src/cgeo/geocaching/activity/AbstractListActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractListActivity.java @@ -32,17 +32,17 @@ public abstract class AbstractListActivity extends ActionBarListActivity impleme } @Override - public final void showToast(String text) { + public final void showToast(final String text) { ActivityMixin.showToast(this, text); } @Override - public final void showShortToast(String text) { + public final void showShortToast(final String text) { ActivityMixin.showShortToast(this, text); } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); @@ -55,7 +55,7 @@ public abstract class AbstractListActivity extends ActionBarListActivity impleme } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId()== android.R.id.home) { return ActivityMixin.navigateUp(this); } @@ -67,7 +67,7 @@ public abstract class AbstractListActivity extends ActionBarListActivity impleme res = this.getResources(); app = (CgeoApplication) this.getApplication(); - ActivityMixin.keepScreenOn(this, keepScreenOn); + ActivityMixin.onCreate(this, keepScreenOn); } final protected void setTitle(final String title) { @@ -79,7 +79,7 @@ public abstract class AbstractListActivity extends ActionBarListActivity impleme ActivityMixin.invalidateOptionsMenu(this); } - public void onCreate(Bundle savedInstanceState, int resourceLayoutID) { + public void onCreate(final Bundle savedInstanceState, final int resourceLayoutID) { super.onCreate(savedInstanceState); initializeCommonFields(); @@ -88,10 +88,22 @@ public abstract class AbstractListActivity extends ActionBarListActivity impleme } @Override - public void setContentView(int layoutResID) { + public void setContentView(final int layoutResID) { super.setContentView(layoutResID); // initialize action bar title with activity title ActivityMixin.setTitle(this, getTitle()); } + + @Override + public final void presentShowcase() { + ActivityMixin.presentShowcase(this); + } + + @Override + public ShowcaseViewBuilder getShowcase() { + // do nothing by default + return null; + } + } diff --git a/main/src/cgeo/geocaching/activity/ActivityMixin.java b/main/src/cgeo/geocaching/activity/ActivityMixin.java index b58d3ae..28042b0 100644 --- a/main/src/cgeo/geocaching/activity/ActivityMixin.java +++ b/main/src/cgeo/geocaching/activity/ActivityMixin.java @@ -16,7 +16,9 @@ import android.support.v4.app.TaskStackBuilder; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.view.Gravity; +import android.view.Window; import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; import android.widget.EditText; import android.widget.Toast; @@ -103,13 +105,17 @@ public final class ActivityMixin { postShowToast(activity, text, Toast.LENGTH_SHORT); } - public static void keepScreenOn(final Activity abstractActivity, boolean keepScreenOn) { + public static void onCreate(final Activity abstractActivity, final boolean keepScreenOn) { + final Window window = abstractActivity.getWindow(); if (keepScreenOn) { - abstractActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + if (Settings.useHardwareAcceleration()) { + window.setFlags(LayoutParams.FLAG_HARDWARE_ACCELERATED, LayoutParams.FLAG_HARDWARE_ACCELERATED); } } - public static void invalidateOptionsMenu(Activity activity) { + public static void invalidateOptionsMenu(final Activity activity) { if (activity instanceof ActionBarActivity) { ((ActionBarActivity) activity).supportInvalidateOptionsMenu(); } @@ -127,10 +133,10 @@ public final class ActivityMixin { * place the cursor after the inserted text */ public static void insertAtPosition(final EditText editText, final String insertText, final boolean moveCursor) { - int selectionStart = editText.getSelectionStart(); - int selectionEnd = editText.getSelectionEnd(); - int start = Math.min(selectionStart, selectionEnd); - int end = Math.max(selectionStart, selectionEnd); + final int selectionStart = editText.getSelectionStart(); + final int selectionEnd = editText.getSelectionEnd(); + final int start = Math.min(selectionStart, selectionEnd); + final int end = Math.max(selectionStart, selectionEnd); final String content = editText.getText().toString(); String completeText; @@ -141,13 +147,13 @@ public final class ActivityMixin { } editText.getText().replace(start, end, completeText); - int newCursor = moveCursor ? start + completeText.length() : start; + final int newCursor = moveCursor ? start + completeText.length() : start; editText.setSelection(newCursor); } public static boolean navigateUp(@NonNull final Activity activity) { // see http://developer.android.com/training/implementing-navigation/ancestral.html - Intent upIntent = NavUtils.getParentActivityIntent(activity); + final Intent upIntent = NavUtils.getParentActivityIntent(activity); if (upIntent == null) { activity.finish(); return true; @@ -167,4 +173,15 @@ public final class ActivityMixin { } return true; } + + public static void presentShowcase(final IAbstractActivity activity) { + if (VERSION.SDK_INT < 11) { + return; + } + final ShowcaseViewBuilder builder = activity.getShowcase(); + if (builder != null) { + builder.setStyle(R.style.ShowcaseView); + builder.build(); + } + } } diff --git a/main/src/cgeo/geocaching/activity/IAbstractActivity.java b/main/src/cgeo/geocaching/activity/IAbstractActivity.java index 4fb6a2a..59aa284 100644 --- a/main/src/cgeo/geocaching/activity/IAbstractActivity.java +++ b/main/src/cgeo/geocaching/activity/IAbstractActivity.java @@ -8,4 +8,18 @@ public interface IAbstractActivity { public void showShortToast(String text); public void invalidateOptionsMenuCompatible(); + + /** + * Override this method to create a showcase view highlighting the most important UI element. + * + */ + public ShowcaseViewBuilder getShowcase(); + + /** + * Call this method to actually present a showcase. The right time to invoke this method depends on the showcase + * target. I.e. if the showcase target is an action bar item, this method can only be invoked after that item has + * been created in onCreateOptionsMenu. + */ + public void presentShowcase(); + } diff --git a/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java b/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java new file mode 100644 index 0000000..6b00f0f --- /dev/null +++ b/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java @@ -0,0 +1,61 @@ +package cgeo.geocaching.activity; + +import com.github.amlcurran.showcaseview.ShowcaseView.Builder; +import com.github.amlcurran.showcaseview.targets.Target; + +import android.app.Activity; + +/** + * TODO: replace by simple utility class embedding a builder instead of inheriting from it + */ +public class ShowcaseViewBuilder extends Builder { + + private final Activity activity; + + public ShowcaseViewBuilder(final Activity activity) { + super(activity); + this.activity = activity; + } + + @Override + public ShowcaseViewBuilder setContentTitle(final int resId) { + setSingleshot(activity.getResources().getString(resId)); + return (ShowcaseViewBuilder) super.setContentTitle(resId); + } + + /** + * Use the hash of the title for the single shot remembering + * + * @param resId + */ + private void setSingleshot(final CharSequence title) { + super.singleShot(title.hashCode()); + } + + @Override + public ShowcaseViewBuilder setContentText(final int resId) { + return (ShowcaseViewBuilder) super.setContentText(resId); + } + + @Override + public ShowcaseViewBuilder setContentText(final CharSequence text) { + return (ShowcaseViewBuilder) super.setContentText(text); + } + + @Override + public ShowcaseViewBuilder setContentTitle(final CharSequence title) { + setSingleshot(title); + return (ShowcaseViewBuilder) super.setContentTitle(title); + } + + @Override + public ShowcaseViewBuilder setTarget(final Target target) { + return (ShowcaseViewBuilder) super.setTarget(target); + } + + public ShowcaseViewBuilder setContent(final int titleId, final int textId) { + setContentTitle(titleId); + return setContentText(textId); + } + +} diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java index 6c6ffda..f31d175 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java @@ -9,6 +9,9 @@ import android.content.Intent; public abstract class AbstractRadarApp extends AbstractPointNavigationApp { + protected static final String RADAR_EXTRA_LONGITUDE = "longitude"; + protected static final String RADAR_EXTRA_LATITUDE = "latitude"; + private final String intentAction; protected AbstractRadarApp(final String name, final int id, final String intent, final String packageName) { diff --git a/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java b/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java index 03d2220..743ce1f 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java @@ -20,19 +20,19 @@ class CompassApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint coords) { - CompassActivity.startActivity(activity, getString(R.string.navigation_direct_navigation), getString(R.string.navigation_target), coords, null); + public void navigate(final Activity activity, final Geopoint coords) { + CompassActivity.startActivity(activity, getString(R.string.navigation_direct_navigation), getString(R.string.navigation_target), coords); } @Override - public void navigate(Activity activity, Waypoint waypoint) { - CompassActivity.startActivity(activity, waypoint.getPrefix() + "/" + waypoint.getLookup(), waypoint.getName(), waypoint.getCoords(), null, + public void navigate(final Activity activity, final Waypoint waypoint) { + CompassActivity.startActivity(activity, waypoint.getPrefix() + "/" + waypoint.getLookup(), waypoint.getName(), waypoint.getCoords(), waypoint.getWaypointType().getL10n()); } @Override - public void navigate(Activity activity, Geocache cache) { - CompassActivity.startActivity(activity, cache); + public void navigate(final Activity activity, final Geocache cache) { + CompassActivity.startActivityCache(activity, cache); } }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java index 4924786..f5ccef4 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java @@ -1,10 +1,10 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.R; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.maps.MapProviderFactory; +import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.utils.Log; import android.app.Activity; @@ -23,15 +23,13 @@ public class GoogleMapsDirectionApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint coords) { + public void navigate(final Activity activity, final Geopoint coords) { try { - IGeoData geo = CgeoApplication.getInstance().currentGeo(); - final Geopoint coordsNow = geo == null ? null : geo.getCoords(); - - if (coordsNow != null) { + final IGeoData geo = CgeoApplication.getInstance().currentGeo(); + if (geo.getCoords() != null) { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri .parse("http://maps.google.com/maps?f=d&saddr=" - + coordsNow.getLatitude() + "," + coordsNow.getLongitude() + "&daddr=" + + geo.getCoords().getLatitude() + "," + geo.getCoords().getLongitude() + "&daddr=" + coords.getLatitude() + "," + coords.getLongitude()))); } else { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri @@ -39,7 +37,7 @@ public class GoogleMapsDirectionApp extends AbstractPointNavigationApp { + coords.getLatitude() + "," + coords.getLongitude()))); } - } catch (Exception e) { + } catch (final Exception e) { Log.i("GoogleMapsDirectionApp: application not available.", e); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java b/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java index e24c055..c00723d 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java @@ -142,23 +142,21 @@ public final class NavigationAppFactory extends AbstractAppFactory { final boolean showInternalMap, final boolean showDefaultNavigation) { final List<NavigationAppsEnum> items = new ArrayList<>(); final int defaultNavigationTool = Settings.getDefaultNavigationTool(); - for (final NavigationAppsEnum navApp : getInstalledNavigationApps()) { + for (final NavigationAppsEnum navApp : getActiveNavigationApps()) { if ((showInternalMap || !(navApp.app instanceof InternalMap)) && (showDefaultNavigation || defaultNavigationTool != navApp.id)) { - if (Settings.isUseNavigationApp(navApp)) { - boolean add = false; - if (cache != null && navApp.app instanceof CacheNavigationApp && navApp.app.isEnabled(cache)) { - add = true; - } - if (waypoint != null && navApp.app instanceof WaypointNavigationApp && ((WaypointNavigationApp) navApp.app).isEnabled(waypoint)) { - add = true; - } - if (destination != null && navApp.app instanceof GeopointNavigationApp) { - add = true; - } - if (add) { - items.add(navApp); - } + boolean add = false; + if (cache != null && navApp.app instanceof CacheNavigationApp && navApp.app.isEnabled(cache)) { + add = true; + } + if (waypoint != null && navApp.app instanceof WaypointNavigationApp && ((WaypointNavigationApp) navApp.app).isEnabled(waypoint)) { + add = true; + } + if (destination != null && navApp.app instanceof GeopointNavigationApp) { + add = true; + } + if (add) { + items.add(navApp); } } } @@ -203,6 +201,19 @@ public final class NavigationAppFactory extends AbstractAppFactory { } /** + * @return all navigation apps, which are installed and activated in the settings + */ + public static List<NavigationAppsEnum> getActiveNavigationApps() { + final List<NavigationAppsEnum> activeApps = new ArrayList<>(); + for (final NavigationAppsEnum appEnum : getInstalledNavigationApps()) { + if (Settings.isUseNavigationApp(appEnum)) { + activeApps.add(appEnum); + } + } + return activeApps; + } + + /** * Returns all installed navigation apps for default navigation. * * @return diff --git a/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java b/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java index 9abc581..82883a2 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java @@ -47,7 +47,7 @@ public class NavigationSelectionActionProvider extends ActionProvider { if (geocache == null) { return; } - for (final NavigationAppsEnum app : NavigationAppFactory.getInstalledNavigationApps()) { + for (final NavigationAppsEnum app : NavigationAppFactory.getActiveNavigationApps()) { if (app.app.isEnabled(geocache)) { subMenu.add(Menu.NONE, app.id, Menu.NONE, app.app.getName()).setOnMenuItemClickListener(new OnMenuItemClickListener() { diff --git a/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java b/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java index 5d645f7..4dbfadd 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java @@ -8,6 +8,8 @@ import android.content.Intent; class OruxMapsApp extends AbstractPointNavigationApp { + private static final String ORUXMAPS_EXTRA_LONGITUDE = "longitude"; + private static final String ORUXMAPS_EXTRA_LATITUDE = "latitude"; private static final String INTENT = "com.oruxmaps.VIEW_MAP_ONLINE"; OruxMapsApp() { @@ -17,8 +19,8 @@ class OruxMapsApp extends AbstractPointNavigationApp { @Override public void navigate(Activity activity, Geopoint point) { final Intent intent = new Intent(INTENT); - intent.putExtra("latitude", point.getLatitude());//latitude, wgs84 datum - intent.putExtra("longitude", point.getLongitude());//longitude, wgs84 datum + intent.putExtra(ORUXMAPS_EXTRA_LATITUDE, point.getLatitude());//latitude, wgs84 datum + intent.putExtra(ORUXMAPS_EXTRA_LONGITUDE, point.getLongitude());//longitude, wgs84 datum activity.startActivity(intent); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java b/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java index 5012195..a12a38e 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java @@ -20,7 +20,7 @@ class PebbleApp extends AbstractRadarApp { @Override protected void addCoordinates(final Intent intent, final Geopoint coords) { - intent.putExtra("latitude", coords.getLatitude()); - intent.putExtra("longitude", coords.getLongitude()); + intent.putExtra(RADAR_EXTRA_LATITUDE, coords.getLatitude()); + intent.putExtra(RADAR_EXTRA_LONGITUDE, coords.getLongitude()); } }
\ 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 41cf2d8..0ee512b 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java @@ -16,8 +16,8 @@ class RadarApp extends AbstractRadarApp { @Override protected void addCoordinates(final Intent intent, final Geopoint coords) { - intent.putExtra("latitude", (float) coords.getLatitude()); - intent.putExtra("longitude", (float) coords.getLongitude()); + intent.putExtra(RADAR_EXTRA_LATITUDE, (float) coords.getLatitude()); + intent.putExtra(RADAR_EXTRA_LONGITUDE, (float) coords.getLongitude()); } }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/connector/AbstractConnector.java b/main/src/cgeo/geocaching/connector/AbstractConnector.java index 8138e96..a929e2b 100644 --- a/main/src/cgeo/geocaching/connector/AbstractConnector.java +++ b/main/src/cgeo/geocaching/connector/AbstractConnector.java @@ -76,7 +76,7 @@ public abstract class AbstractConnector implements IConnector { } @Override - public boolean supportsFavoritePoints() { + public boolean supportsFavoritePoints(final Geocache cache) { return false; } diff --git a/main/src/cgeo/geocaching/connector/IConnector.java b/main/src/cgeo/geocaching/connector/IConnector.java index fbade5a..e6b6674 100644 --- a/main/src/cgeo/geocaching/connector/IConnector.java +++ b/main/src/cgeo/geocaching/connector/IConnector.java @@ -72,7 +72,7 @@ public interface IConnector { * * @return */ - public boolean supportsFavoritePoints(); + public boolean supportsFavoritePoints(final Geocache cache); /** * enable/disable logging controls in cache details diff --git a/main/src/cgeo/geocaching/connector/gc/GCConnector.java b/main/src/cgeo/geocaching/connector/gc/GCConnector.java index ad00718..4512979 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConnector.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConnector.java @@ -7,6 +7,7 @@ import cgeo.geocaching.ICache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; +import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.connector.AbstractConnector; import cgeo.geocaching.connector.ILoggingManager; import cgeo.geocaching.connector.UserAction; @@ -37,6 +38,7 @@ import org.eclipse.jdt.annotation.Nullable; import rx.functions.Action1; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -143,7 +145,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_loadpage); - final String page = GCParser.requestHtmlPage(geocode, guid, "y", String.valueOf(GCConstants.NUMBER_OF_LOGS)); + final String page = GCParser.requestHtmlPage(geocode, guid, "y"); if (StringUtils.isEmpty(page)) { final SearchResult search = new SearchResult(); @@ -283,8 +285,8 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean supportsFavoritePoints() { - return true; + public boolean supportsFavoritePoints(final Geocache cache) { + return !cache.getType().isEvent(); } @Override @@ -414,7 +416,12 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, @Override public void call(cgeo.geocaching.connector.UserAction.Context context) { - context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/email/?u=" + Network.encode(context.userName)))); + try { + context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/email/?u=" + Network.encode(context.userName)))); + } catch (final ActivityNotFoundException e) { + Log.e("Cannot find suitable activity", e); + ActivityMixin.showToast(context.activity, R.string.err_application_no); + } } })); return actions; diff --git a/main/src/cgeo/geocaching/connector/gc/GCConstants.java b/main/src/cgeo/geocaching/connector/gc/GCConstants.java index 8df7703..c2021bb 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConstants.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConstants.java @@ -74,7 +74,7 @@ public final class GCConstants { public static final Pattern PATTERN_CACHES_FOUND = Pattern.compile("<strong[^>]*>.*?([\\d,.]+) Caches? Found", Pattern.DOTALL); public static final Pattern PATTERN_AVATAR_IMAGE_PROFILE_PAGE = Pattern.compile("src=\"(https?://(imgcdn\\.geocaching\\.com|[^>\"]+\\.cloudfront\\.net)/avatar/[0-9a-f-]+\\.jpg)\"[^>]*alt=\""); public static final Pattern PATTERN_LOGIN_NAME_LOGIN_PAGE = Pattern.compile("ctl00_ContentBody_lbUsername\">.*<strong>(.*)</strong>"); - public static final Pattern PATTERN_CUSTOMDATE = Pattern.compile("<option selected=\"selected\" value=\"([ /Mdy-]+)\">"); + public static final Pattern PATTERN_CUSTOMDATE = Pattern.compile("<option selected=\"selected\" value=\"([ /.Mdy-]+)\">"); public static final Pattern PATTERN_MAP_LOGGED_IN = Pattern.compile("<a href=\"https?://www.geocaching.com/my/\" class=\"CommonUsername\""); /** @@ -130,7 +130,7 @@ public final class GCConstants { public final static Pattern PATTERN_SEARCH_TOTALCOUNT = Pattern.compile("<span>Total Records\\D*(\\d+)<"); public final static Pattern PATTERN_SEARCH_RECAPTCHA = Pattern.compile("<script[^>]*src=\"[^\"]*/recaptcha/api/challenge\\?k=([^\"]+)\"[^>]*>"); public final static Pattern PATTERN_SEARCH_RECAPTCHACHALLENGE = Pattern.compile("challenge : '([^']+)'"); - public final static Pattern PATTERN_SEARCH_HIDDEN_DATE = Pattern.compile("<td valign=\"top\"[^<]+<span class=\"small\">([^<]+)</span>"); + public final static Pattern PATTERN_SEARCH_HIDDEN_DATE = Pattern.compile("<td style=\"width:70px\">[^<]+<span class=\"small\">([^<]+)</span>"); /** * Patterns for waypoints @@ -172,7 +172,7 @@ public final class GCConstants { public final static String STRING_PREMIUMONLY_2 = "Sorry, the owner of this listing has made it viewable to Premium Members only."; public final static String STRING_PREMIUMONLY_1 = "has chosen to make this cache listing visible to Premium Members only."; public final static String STRING_UNPUBLISHED_OTHER = "you cannot view this cache listing until it has been published"; - public final static String STRING_UNPUBLISHED_FROM_SEARCH = "class=\"UnpublishedCacheSearchWidge\""; + public final static String STRING_UNPUBLISHED_FROM_SEARCH = "class=\"UnpublishedCacheSearchWidget"; // do not include closing brace as the CSS can contain additional styles public final static String STRING_UNKNOWN_ERROR = "An Error Has Occurred"; public final static String STRING_DISABLED = "<li>This cache is temporarily unavailable."; public final static String STRING_ARCHIVED = "<li>This cache has been archived,"; diff --git a/main/src/cgeo/geocaching/connector/gc/GCLogin.java b/main/src/cgeo/geocaching/connector/gc/GCLogin.java index df537f5..16f20b8 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCLogin.java +++ b/main/src/cgeo/geocaching/connector/gc/GCLogin.java @@ -47,7 +47,9 @@ public class GCLogin extends AbstractLogin { DEFAULT_CUSTOM_DATE_FORMAT, "yyyy-MM-dd", "yyyy/MM/dd", + "dd.MM.yyyy", "dd/MMM/yyyy", + "dd.MMM.yyyy", "MMM/dd/yyyy", "dd MMM yy", "dd/MM/yyyy" @@ -134,13 +136,11 @@ public class GCLogin extends AbstractLogin { assert loginData != null; // Caught above if (getLoginStatus(loginData)) { - Log.i("Successfully logged in Geocaching.com as " + username + " (" + Settings.getGCMemberStatus() + ')'); - if (switchToEnglish(loginData) && retry) { return login(false); } + Log.i("Successfully logged in Geocaching.com as " + username + " (" + Settings.getGCMemberStatus() + ')'); Settings.setCookieStore(Cookies.dumpCookieStore()); - return StatusCode.NO_ERROR; // logged in } diff --git a/main/src/cgeo/geocaching/connector/gc/GCParser.java b/main/src/cgeo/geocaching/connector/gc/GCParser.java index d1c81fe..6919173 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCParser.java +++ b/main/src/cgeo/geocaching/connector/gc/GCParser.java @@ -30,6 +30,7 @@ import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.DirectionImage; import cgeo.geocaching.utils.CancellableHandler; +import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; @@ -78,6 +79,7 @@ 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 final static ImmutablePair<StatusCode, Geocache> UNKNOWN_PARSE_ERROR = ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); private static SearchResult parseSearch(final String url, final String pageContent, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(pageContent)) { @@ -374,7 +376,7 @@ public abstract class GCParser { final SearchResult result = new SearchResult(parsed.left); if (parsed.left == StatusCode.NO_ERROR) { result.addAndPutInCache(Collections.singletonList(parsed.right)); - DataStore.saveLogsWithoutTransaction(parsed.right.getGeocode(), getLogsFromDetails(page).toBlocking().toIterable()); + DataStore.saveLogsWithoutTransaction(parsed.right.getGeocode(), getLogs(page, Logs.ALL).toBlocking().toIterable()); } return result; } @@ -390,12 +392,13 @@ public abstract class GCParser { * @return a pair, with a {@link StatusCode} on the left, and a non-null cache object on the right * iff the status code is {@link StatusCode.NO_ERROR}. */ + @NonNull static private ImmutablePair<StatusCode, Geocache> parseCacheFromText(final String pageIn, @Nullable final CancellableHandler handler) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_details); if (StringUtils.isBlank(pageIn)) { Log.e("GCParser.parseCache: No page given"); - return null; + return UNKNOWN_PARSE_ERROR; } if (pageIn.contains(GCConstants.STRING_UNPUBLISHED_OTHER) || pageIn.contains(GCConstants.STRING_UNPUBLISHED_FROM_SEARCH)) { @@ -408,7 +411,7 @@ public abstract class GCParser { final String cacheName = Html.fromHtml(TextUtils.getMatch(pageIn, GCConstants.PATTERN_NAME, true, "")).toString(); if (GCConstants.STRING_UNKNOWN_ERROR.equalsIgnoreCase(cacheName)) { - return ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); + return UNKNOWN_PARSE_ERROR; } // first handle the content with line breaks, then trim everything for easier matching and reduced memory consumption in parsed fields @@ -451,7 +454,7 @@ public abstract class GCParser { final int pos = tableInside.indexOf(GCConstants.STRING_CACHEDETAILS); if (pos == -1) { Log.e("GCParser.parseCache: ID \"cacheDetails\" not found on page"); - return null; + return UNKNOWN_PARSE_ERROR; } tableInside = tableInside.substring(pos); @@ -590,7 +593,7 @@ public abstract class GCParser { // cache spoilers try { if (CancellableHandler.isCancelled(handler)) { - return null; + return UNKNOWN_PARSE_ERROR; } CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_spoilers); @@ -691,7 +694,7 @@ public abstract class GCParser { int wpBegin = page.indexOf("<table class=\"Table\" id=\"ctl00_ContentBody_Waypoints\">"); if (wpBegin != -1) { // parse waypoints if (CancellableHandler.isCancelled(handler)) { - return null; + return UNKNOWN_PARSE_ERROR; } CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_waypoints); @@ -735,7 +738,6 @@ public abstract class GCParser { // waypoint latitude and longitude latlon = Html.fromHtml(TextUtils.getMatch(wp[7], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, false, 2, "", false)).toString().trim(); if (!StringUtils.startsWith(latlon, "???")) { - waypoint.setLatlon(latlon); waypoint.setCoords(new Geopoint(latlon)); } @@ -756,7 +758,7 @@ public abstract class GCParser { // last check for necessary cache conditions if (StringUtils.isBlank(cache.getGeocode())) { - return ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); + return UNKNOWN_PARSE_ERROR; } cache.setDetailedUpdatedNow(); @@ -1371,7 +1373,7 @@ public abstract class GCParser { } @Nullable - static String requestHtmlPage(@Nullable final String geocode, @Nullable final String guid, final String log, final String numlogs) { + static String requestHtmlPage(@Nullable final String geocode, @Nullable final String guid, final String log) { final Parameters params = new Parameters("decrypt", "y"); if (StringUtils.isNotBlank(geocode)) { params.put("wp", geocode); @@ -1379,7 +1381,7 @@ public abstract class GCParser { params.put("guid", guid); } params.put("log", log); - params.put("numlogs", numlogs); + params.put("numlogs", "0"); return GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/seek/cache_details.aspx", params); } @@ -1418,7 +1420,7 @@ public abstract class GCParser { } private static String getUserToken(final Geocache cache) { - final String page = requestHtmlPage(cache.getGeocode(), null, "n", "0"); + final String page = requestHtmlPage(cache.getGeocode(), null, "n"); return TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); } @@ -1546,7 +1548,7 @@ public abstract class GCParser { } // trackable goal - trackable.setGoal(convertLinks(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GOAL, true, trackable.getGoal()))); + trackable.setGoal(HtmlUtils.removeExtraParagraph(convertLinks(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GOAL, true, trackable.getGoal())))); // trackable details & image try { @@ -1556,10 +1558,10 @@ public abstract class GCParser { final String details = StringUtils.trim(matcherDetailsImage.group(4)); if (StringUtils.isNotEmpty(image)) { - trackable.setImage(image); + trackable.setImage(StringUtils.replace(image, "/display/", "/large/")); } if (StringUtils.isNotEmpty(details) && !StringUtils.equals(details, "No additional details available.")) { - trackable.setDetails(convertLinks(details)); + trackable.setDetails(HtmlUtils.removeExtraParagraph(convertLinks(details))); } } } catch (final RuntimeException e) { @@ -1638,27 +1640,14 @@ public abstract class GCParser { return StringUtils.replace(input, "../", GCConstants.GC_URL); } - /** - * Extract logs from a cache details page. - * - * @param page - * the text of the details page - * @return a list of log entries which will be empty if the logs could not be retrieved - * - */ - @NonNull - private static Observable<LogEntry> getLogsFromDetails(final String page) { - // extract embedded JSON data from page - return parseLogs(false, TextUtils.getMatch(page, GCConstants.PATTERN_LOGBOOK, "")); - } - - private enum SpecialLogs { + private enum Logs { + ALL(null), FRIENDS("sf"), OWN("sp"); final String paramName; - SpecialLogs(final String paramName) { + Logs(final String paramName) { this.paramName = paramName; } @@ -1676,10 +1665,10 @@ public abstract class GCParser { * The logType to request * @return Observable<LogEntry> The logs */ - private static Observable<LogEntry> getSpecialLogs(final String page, final SpecialLogs logType) { - return Observable.defer(new Func0<Observable<? extends LogEntry>>() { + private static Observable<LogEntry> getLogs(final String page, final Logs logType) { + return Observable.defer(new Func0<Observable<LogEntry>>() { @Override - public Observable<? extends LogEntry> call() { + public Observable<LogEntry> call() { final MatcherWrapper userTokenMatcher = new MatcherWrapper(GCConstants.PATTERN_USERTOKEN, page); if (!userTokenMatcher.find()) { Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); @@ -1691,8 +1680,10 @@ public abstract class GCParser { "tkn", userToken, "idx", "1", "num", String.valueOf(GCConstants.NUMBER_OF_LOGS), - logType.getParamName(), Boolean.toString(Boolean.TRUE), "decrypt", "true"); + if (logType != Logs.ALL) { + params.add(logType.getParamName(), Boolean.toString(Boolean.TRUE)); + } final HttpResponse response = Network.getRequest("http://www.geocaching.com/seek/geocache.logbook", params); if (response == null) { Log.e("GCParser.loadLogsFromDetails: cannot log logs, response is null"); @@ -1708,7 +1699,7 @@ public abstract class GCParser { Log.e("GCParser.loadLogsFromDetails: unable to read whole response"); return Observable.empty(); } - return parseLogs(true, rawResponse); + return parseLogs(logType != Logs.ALL, rawResponse); } }).subscribeOn(RxUtils.networkScheduler); } @@ -1872,16 +1863,11 @@ public abstract class GCParser { return; } - final Observable<LogEntry> logs = getLogsFromDetails(page).subscribeOn(RxUtils.computationScheduler); - Observable<LogEntry> specialLogs; - if (Settings.isFriendLogsWanted()) { - CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); - specialLogs = Observable.merge(getSpecialLogs(page, SpecialLogs.FRIENDS), - getSpecialLogs(page, SpecialLogs.OWN)); - } else { - CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); - specialLogs = Observable.empty(); - } + CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); + final Observable<LogEntry> logs = getLogs(page, Logs.ALL); + final Observable<LogEntry> ownLogs = getLogs(page, Logs.OWN).cache(); + final Observable<LogEntry> specialLogs = Settings.isFriendLogsWanted() ? + Observable.merge(getLogs(page, Logs.FRIENDS), ownLogs) : Observable.<LogEntry>empty(); final Observable<List<LogEntry>> mergedLogs = Observable.zip(logs.toList(), specialLogs.toList(), new Func2<List<LogEntry>, List<LogEntry>, List<LogEntry>>() { @Override @@ -1896,6 +1882,16 @@ public abstract class GCParser { DataStore.saveLogsWithoutTransaction(cache.getGeocode(), logEntries); } }); + if (cache.isFound() && cache.getVisitedDate() == 0) { + ownLogs.subscribe(new Action1<LogEntry>() { + @Override + public void call(final LogEntry logEntry) { + if (logEntry.type == LogType.FOUND_IT) { + cache.setVisitedDate(logEntry.date); + } + } + }); + } if (Settings.isRatingWanted() && !CancellableHandler.isCancelled(handler)) { CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_gcvote); diff --git a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java index 6095514..affeb7d 100644 --- a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java +++ b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java @@ -42,15 +42,15 @@ public class RecaptchaHandler extends Handler { } private void loadChallenge(final ImageView imageView, final View reloadButton) { - final Observable<Bitmap> captcha = Observable.defer(new Func0<Observable<? extends Bitmap>>() { + final Observable<Bitmap> captcha = Observable.defer(new Func0<Observable<Bitmap>>() { @Override - public Observable<? extends Bitmap> call() { + public Observable<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); + return Observable.just(img); } catch (final Exception e) { Log.e("RecaptchaHandler.getCaptcha", e); return Observable.error(e); diff --git a/main/src/cgeo/geocaching/enumerations/CacheType.java b/main/src/cgeo/geocaching/enumerations/CacheType.java index 16677da..1d190e4 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheType.java +++ b/main/src/cgeo/geocaching/enumerations/CacheType.java @@ -68,8 +68,10 @@ public enum CacheType { mappingPattern.put(ct.pattern.toLowerCase(Locale.US), ct); mappingGuid.put(ct.guid, ct); } - // add old mystery type for GPX file compatibility + // Add old mystery type for GPX file compatibility. mappingPattern.put("Mystery Cache".toLowerCase(Locale.US), MYSTERY); + // This pattern briefly appeared on gc.com in 2014-08. + mappingPattern.put("Traditional Geocache".toLowerCase(Locale.US), TRADITIONAL); FIND_BY_ID = Collections.unmodifiableMap(mappingId); FIND_BY_PATTERN = Collections.unmodifiableMap(mappingPattern); diff --git a/main/src/cgeo/geocaching/enumerations/LogType.java b/main/src/cgeo/geocaching/enumerations/LogType.java index 84ab7b9..5345611 100644 --- a/main/src/cgeo/geocaching/enumerations/LogType.java +++ b/main/src/cgeo/geocaching/enumerations/LogType.java @@ -52,7 +52,7 @@ public enum LogType { private final int stringId; public final int markerId; - LogType(int id, String iconName, String type, String oc_type, int stringId, int markerId) { + LogType(final int id, final String iconName, final String type, final String oc_type, final int stringId, final int markerId) { this.id = id; this.iconName = iconName; this.type = type; @@ -61,7 +61,7 @@ public enum LogType { this.markerId = markerId; } - LogType(int id, String iconName, String type, String oc_type, int stringId) { + LogType(final int id, final String iconName, final String type, final String oc_type, final int stringId) { this(id, iconName, type, oc_type, stringId, R.drawable.mark_gray); } @@ -70,7 +70,7 @@ public enum LogType { static { final HashMap<String, LogType> mappingPattern = new HashMap<>(); final HashMap<String, LogType> mappingType = new HashMap<>(); - for (LogType lt : values()) { + for (final LogType lt : values()) { if (lt.iconName != null) { mappingPattern.put(lt.iconName, lt); } @@ -81,7 +81,7 @@ public enum LogType { } public static LogType getById(final int id) { - for (LogType logType : values()) { + for (final LogType logType : values()) { if (logType.id == id) { return logType; } @@ -113,4 +113,12 @@ public enum LogType { public final String getL10n() { return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); } + + public final boolean isFoundLog() { + return this == LogType.FOUND_IT || this == LogType.ATTENDED || this == LogType.WEBCAM_PHOTO_TAKEN; + } + + public boolean mustConfirmLog() { + return this == ARCHIVE || this == NEEDS_ARCHIVE; + } } diff --git a/main/src/cgeo/geocaching/files/GPXImporter.java b/main/src/cgeo/geocaching/files/GPXImporter.java index 85d12f7..4f1d391 100644 --- a/main/src/cgeo/geocaching/files/GPXImporter.java +++ b/main/src/cgeo/geocaching/files/GPXImporter.java @@ -113,7 +113,7 @@ public class GPXImporter { fileType = getFileTypeFromMimeType(mimeType); } - ImportThread importer = getImporterFromFileType(uri, contentResolver, + final ImportThread importer = getImporterFromFileType(uri, contentResolver, fileType); if (importer != null) { @@ -146,8 +146,8 @@ public class GPXImporter { return FileType.UNKNOWN; } - private ImportThread getImporterFromFileType(Uri uri, - ContentResolver contentResolver, FileType fileType) { + private ImportThread getImporterFromFileType(final Uri uri, + final ContentResolver contentResolver, final FileType fileType) { switch (fileType) { case ZIP: return new ImportGpxZipAttachmentThread(uri, contentResolver, @@ -179,7 +179,7 @@ public class GPXImporter { final Handler importStepHandler; final CancellableHandler progressHandler; - protected ImportThread(int listId, Handler importStepHandler, CancellableHandler progressHandler) { + protected ImportThread(final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { this.listId = listId; this.importStepHandler = importStepHandler; this.progressHandler = progressHandler; @@ -245,7 +245,7 @@ public class GPXImporter { static class ImportLocFileThread extends ImportThread { private final File file; - public ImportLocFileThread(final File file, int listId, Handler importStepHandler, CancellableHandler progressHandler) { + public ImportLocFileThread(final File file, final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); this.file = file; } @@ -263,7 +263,7 @@ public class GPXImporter { private final Uri uri; private final ContentResolver contentResolver; - public ImportLocAttachmentThread(Uri uri, ContentResolver contentResolver, int listId, Handler importStepHandler, CancellableHandler progressHandler) { + public ImportLocAttachmentThread(final Uri uri, final ContentResolver contentResolver, final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); this.uri = uri; this.contentResolver = contentResolver; @@ -285,7 +285,7 @@ public class GPXImporter { static abstract class ImportGpxThread extends ImportThread { - protected ImportGpxThread(int listId, Handler importStepHandler, CancellableHandler progressHandler) { + protected ImportGpxThread(final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); } @@ -306,13 +306,13 @@ public class GPXImporter { static class ImportGpxFileThread extends ImportGpxThread { private final File cacheFile; - public ImportGpxFileThread(final File file, int listId, Handler importStepHandler, CancellableHandler progressHandler) { + public ImportGpxFileThread(final File file, final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); this.cacheFile = file; } @Override - protected Collection<Geocache> doImport(GPXParser parser) throws IOException, ParserException { + protected Collection<Geocache> doImport(final GPXParser parser) throws IOException, ParserException { Log.i("Import GPX file: " + cacheFile.getAbsolutePath()); importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_FILE, R.string.gpx_import_loading_caches, (int) cacheFile.length())); Collection<Geocache> caches = parser.parse(cacheFile, progressHandler); @@ -334,17 +334,21 @@ public class GPXImporter { private final Uri uri; private final ContentResolver contentResolver; - public ImportGpxAttachmentThread(Uri uri, ContentResolver contentResolver, int listId, Handler importStepHandler, CancellableHandler progressHandler) { + public ImportGpxAttachmentThread(final Uri uri, final ContentResolver contentResolver, final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); this.uri = uri; this.contentResolver = contentResolver; } @Override - protected Collection<Geocache> doImport(GPXParser parser) throws IOException, ParserException { + protected Collection<Geocache> doImport(final GPXParser parser) throws IOException, ParserException { Log.i("Import GPX from uri: " + uri); - importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_FILE, R.string.gpx_import_loading_caches, -1)); final InputStream is = contentResolver.openInputStream(uri); + int streamSize = is.available(); + if (streamSize == 0) { + streamSize = -1; + } + importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_FILE, R.string.gpx_import_loading_caches, streamSize)); try { return parser.parse(is, progressHandler); } finally { @@ -355,12 +359,12 @@ public class GPXImporter { static abstract class ImportGpxZipThread extends ImportGpxThread { - protected ImportGpxZipThread(int listId, Handler importStepHandler, CancellableHandler progressHandler) { + protected ImportGpxZipThread(final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); } @Override - protected Collection<Geocache> doImport(GPXParser parser) throws IOException, ParserException { + protected Collection<Geocache> doImport(final GPXParser parser) throws IOException, ParserException { Collection<Geocache> caches = Collections.emptySet(); // can't assume that GPX file comes before waypoint file in zip -> so we need two passes // 1. parse GPX files @@ -404,7 +408,7 @@ public class GPXImporter { static class ImportGpxZipFileThread extends ImportGpxZipThread { private final File cacheFile; - public ImportGpxZipFileThread(final File file, int listId, Handler importStepHandler, CancellableHandler progressHandler) { + public ImportGpxZipFileThread(final File file, final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); this.cacheFile = file; Log.i("Import zipped GPX: " + file); @@ -420,7 +424,7 @@ public class GPXImporter { private final Uri uri; private final ContentResolver contentResolver; - public ImportGpxZipAttachmentThread(Uri uri, ContentResolver contentResolver, int listId, Handler importStepHandler, CancellableHandler progressHandler) { + public ImportGpxZipAttachmentThread(final Uri uri, final ContentResolver contentResolver, final int listId, final Handler importStepHandler, final CancellableHandler progressHandler) { super(listId, importStepHandler, progressHandler); this.uri = uri; this.contentResolver = contentResolver; @@ -435,14 +439,14 @@ public class GPXImporter { final private CancellableHandler progressHandler = new CancellableHandler() { @Override - public void handleRegularMessage(Message msg) { + public void handleRegularMessage(final Message msg) { progress.setProgress(msg.arg1); } }; final private Handler importStepHandler = new Handler() { @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { switch (msg.what) { case IMPORT_STEP_START: final Message cancelMessage = importStepHandler.obtainMessage(IMPORT_STEP_CANCEL); diff --git a/main/src/cgeo/geocaching/files/GPXParser.java b/main/src/cgeo/geocaching/files/GPXParser.java index 370b8aa..ccc265e 100644 --- a/main/src/cgeo/geocaching/files/GPXParser.java +++ b/main/src/cgeo/geocaching/files/GPXParser.java @@ -8,6 +8,8 @@ import cgeo.geocaching.R; import cgeo.geocaching.Trackable; import cgeo.geocaching.Waypoint; import cgeo.geocaching.connector.ConnectorFactory; +import cgeo.geocaching.connector.IConnector; +import cgeo.geocaching.connector.capability.ILogin; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; @@ -120,12 +122,12 @@ public abstract class GPXParser extends FileParser { private final class UserDataListener implements EndTextElementListener { private final int index; - public UserDataListener(int index) { + public UserDataListener(final int index) { this.index = index; } @Override - public void end(String user) { + public void end(final String user) { userData[index] = validate(user); } } @@ -250,13 +252,13 @@ public abstract class GPXParser extends FileParser { } } - protected GPXParser(int listIdIn, String namespaceIn, String versionIn) { + protected GPXParser(final int listIdIn, final String namespaceIn, final String versionIn) { listId = listIdIn; namespace = namespaceIn; version = versionIn; } - static Date parseDate(String inputUntrimmed) throws ParseException { + static Date parseDate(final String inputUntrimmed) throws ParseException { String input = inputUntrimmed.trim(); // remove milliseconds to reduce number of needed patterns final MatcherWrapper matcher = new MatcherWrapper(PATTERN_MILLISECONDS, input); @@ -280,7 +282,7 @@ public abstract class GPXParser extends FileParser { root.getChild(namespace, "url").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { scriptUrl = body; } }); @@ -289,7 +291,7 @@ public abstract class GPXParser extends FileParser { waypoint.setStartElementListener(new StartElementListener() { @Override - public void start(Attributes attrs) { + public void start(final Attributes attrs) { try { if (attrs.getIndex("lat") > -1 && attrs.getIndex("lon") > -1) { final String latitude = attrs.getValue("lat"); @@ -399,7 +401,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "time").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { try { cache.setHidden(parseDate(body)); } catch (final Exception e) { @@ -412,7 +414,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "name").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { name = body; String content = body.trim(); @@ -431,7 +433,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "desc").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { desc = body; cache.setShortDescription(validate(body)); @@ -442,7 +444,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "cmt").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { cmt = body; cache.setDescription(validate(body)); @@ -453,7 +455,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "type").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { final String[] content = StringUtils.split(body, '|'); if (content.length > 0) { type = content[0].toLowerCase(Locale.US).trim(); @@ -477,7 +479,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "url").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String url) { + public void end(final String url) { final MatcherWrapper matcher = new MatcherWrapper(PATTERN_GUID, url); if (matcher.matches()) { final String guid = matcher.group(1); @@ -497,7 +499,7 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "urlname").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String urlName) { + public void end(final String urlName) { if (cache.getName().equals(cache.getGeocode()) && StringUtils.startsWith(cache.getGeocode(), "WM")) { cache.setName(StringUtils.trim(urlName)); } @@ -520,7 +522,7 @@ public abstract class GPXParser extends FileParser { gcCache.setStartElementListener(new StartElementListener() { @Override - public void start(Attributes attrs) { + public void start(final Attributes attrs) { try { if (attrs.getIndex("id") > -1) { cache.setCacheId(attrs.getValue("id")); @@ -541,7 +543,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "name").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String cacheName) { + public void end(final String cacheName) { cache.setName(validate(cacheName)); } }); @@ -550,7 +552,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "owner").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String ownerUserId) { + public void end(final String ownerUserId) { cache.setOwnerUserId(validate(ownerUserId)); } }); @@ -559,7 +561,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "placed_by").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String ownerDisplayName) { + public void end(final String ownerDisplayName) { cache.setOwnerDisplayName(validate(ownerDisplayName)); } }); @@ -568,7 +570,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { cache.setType(CacheType.getByPattern(validate(body))); } }); @@ -577,7 +579,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "container").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { cache.setSize(CacheSize.getById(validate(body))); } }); @@ -597,7 +599,7 @@ public abstract class GPXParser extends FileParser { gcAttribute.setStartElementListener(new StartElementListener() { @Override - public void start(Attributes attrs) { + public void start(final Attributes attrs) { try { if (attrs.getIndex("id") > -1 && attrs.getIndex("inc") > -1) { final int attributeId = Integer.parseInt(attrs.getValue("id")); @@ -617,7 +619,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "difficulty").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { try { cache.setDifficulty(Float.parseFloat(body)); } catch (final NumberFormatException e) { @@ -630,7 +632,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "terrain").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { try { cache.setTerrain(Float.parseFloat(body)); } catch (final NumberFormatException e) { @@ -643,7 +645,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "country").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String country) { + public void end(final String country) { if (StringUtils.isBlank(cache.getLocation())) { cache.setLocation(validate(country)); } else { @@ -656,7 +658,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "state").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String state) { + public void end(final String state) { final String trimmedState = state.trim(); if (StringUtils.isNotEmpty(trimmedState)) { // state can be completely empty if (StringUtils.isBlank(cache.getLocation())) { @@ -672,7 +674,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "encoded_hints").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String encoded) { + public void end(final String encoded) { cache.setHint(validate(encoded)); } }); @@ -680,7 +682,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "short_description").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String shortDesc) { + public void end(final String shortDesc) { cache.setShortDescription(validate(shortDesc)); } }); @@ -688,7 +690,7 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "long_description").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String desc) { + public void end(final String desc) { cache.setDescription(validate(desc)); } }); @@ -703,7 +705,7 @@ public abstract class GPXParser extends FileParser { gcTB.setStartElementListener(new StartElementListener() { @Override - public void start(Attributes attrs) { + public void start(final Attributes attrs) { trackable = new Trackable(); try { @@ -733,7 +735,7 @@ public abstract class GPXParser extends FileParser { gcTB.getChild(nsGC, "name").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String tbName) { + public void end(final String tbName) { trackable.setName(validate(tbName)); } }); @@ -747,7 +749,7 @@ public abstract class GPXParser extends FileParser { gcLog.setStartElementListener(new StartElementListener() { @Override - public void start(Attributes attrs) { + public void start(final Attributes attrs) { log = new LogEntry("", 0, LogType.UNKNOWN, ""); try { @@ -765,6 +767,13 @@ public abstract class GPXParser extends FileParser { @Override public void end() { if (log.type != LogType.UNKNOWN) { + if (log.type.isFoundLog() && StringUtils.isNotBlank(log.author)) { + final IConnector connector = ConnectorFactory.getConnector(cache); + if (connector instanceof ILogin && StringUtils.equals(log.author, ((ILogin) connector).getUserName())) { + cache.setFound(true); + cache.setVisitedDate(log.date); + } + } logs.add(log); } } @@ -774,7 +783,7 @@ public abstract class GPXParser extends FileParser { gcLog.getChild(nsGC, "date").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { try { log.date = parseDate(body).getTime(); } catch (final Exception e) { @@ -787,7 +796,7 @@ public abstract class GPXParser extends FileParser { gcLog.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { final String logType = validate(body); log.type = LogType.getByType(logType); } @@ -797,7 +806,7 @@ public abstract class GPXParser extends FileParser { gcLog.getChild(nsGC, "finder").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String finderName) { + public void end(final String finderName) { log.author = validate(finderName); } }); @@ -806,7 +815,7 @@ public abstract class GPXParser extends FileParser { gcLog.getChild(nsGC, "text").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String logText) { + public void end(final String logText) { log.log = validate(logText); } }); @@ -814,7 +823,7 @@ public abstract class GPXParser extends FileParser { try { progressStream = new ProgressInputStream(stream); - BufferedReader reader = new BufferedReader(new InputStreamReader(progressStream, CharEncoding.UTF_8)); + final BufferedReader reader = new BufferedReader(new InputStreamReader(progressStream, CharEncoding.UTF_8)); Xml.parse(new InvalidXMLCharacterFilterReader(reader), root.getContentHandler()); return DataStore.loadCaches(result, EnumSet.of(LoadFlag.DB_MINIMAL)); } catch (final SAXException e) { @@ -833,7 +842,7 @@ public abstract class GPXParser extends FileParser { gsak.getChild(gsakNamespace, "Watch").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String watchList) { + public void end(final String watchList) { cache.setOnWatchlist(Boolean.valueOf(watchList.trim())); } }); @@ -847,7 +856,7 @@ public abstract class GPXParser extends FileParser { gsak.getChild(gsakNamespace, "Parent").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { + public void end(final String body) { parentCacheCode = body; } }); @@ -855,7 +864,7 @@ public abstract class GPXParser extends FileParser { gsak.getChild(gsakNamespace, "FavPoints").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String favoritePoints) { + public void end(final String favoritePoints) { try { cache.setFavoritePoints(Integer.parseInt(favoritePoints)); } @@ -894,7 +903,7 @@ public abstract class GPXParser extends FileParser { cgeoVisited.setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String visited) { + public void end(final String visited) { wptVisited = Boolean.valueOf(visited.trim()); } }); @@ -904,7 +913,7 @@ public abstract class GPXParser extends FileParser { cgeoUserDefined.setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String userDefined) { + public void end(final String userDefined) { wptUserDefined = Boolean.valueOf(userDefined.trim()); } }); @@ -917,7 +926,7 @@ public abstract class GPXParser extends FileParser { * @param cache * currently imported cache */ - protected void afterParsing(Geocache cache) { + protected void afterParsing(final Geocache cache) { // can be overridden by sub classes } @@ -930,7 +939,7 @@ public abstract class GPXParser extends FileParser { */ protected abstract Element getCacheParent(Element waypoint); - protected static String validate(String input) { + protected static String validate(final String input) { if ("nil".equalsIgnoreCase(input)) { return ""; } diff --git a/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java b/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java index ed8c074..f7ac4db 100644 --- a/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java +++ b/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java @@ -16,7 +16,7 @@ class PopularityRatioFilter extends AbstractFilter { private final int minRatio; private final int maxRatio; - public PopularityRatioFilter(String name, final int minRatio, final int maxRatio) { + public PopularityRatioFilter(final String name, final int minRatio, final int maxRatio) { super(name); this.minRatio = minRatio; this.maxRatio = maxRatio; @@ -35,11 +35,11 @@ class PopularityRatioFilter extends AbstractFilter { return ratio > minRatio && ratio <= maxRatio; } - private static int getFindsCount(Geocache cache) { + private static int getFindsCount(final Geocache cache) { if (cache.getLogCounts().isEmpty()) { cache.setLogCounts(DataStore.loadLogCounts(cache.getGeocode())); } - Integer logged = cache.getLogCounts().get(LogType.FOUND_IT); + final Integer logged = cache.getLogCounts().get(LogType.FOUND_IT); if (logged != null) { return logged; } @@ -55,7 +55,7 @@ class PopularityRatioFilter extends AbstractFilter { final List<IFilter> filters = new ArrayList<>(RATIOS.length); for (final int minRange : RATIOS) { final int maxRange = Integer.MAX_VALUE; - final String name = "> " + minRange + " " + CgeoApplication.getInstance().getResources().getString(R.string.percent_favorite_points); + final String name = CgeoApplication.getInstance().getResources().getString(R.string.more_than_percent_favorite_points, minRange); filters.add(new PopularityRatioFilter(name, minRange, maxRange)); } return filters; diff --git a/main/src/cgeo/geocaching/gcvote/GCVote.java b/main/src/cgeo/geocaching/gcvote/GCVote.java index d42bb34..a5b31f0 100644 --- a/main/src/cgeo/geocaching/gcvote/GCVote.java +++ b/main/src/cgeo/geocaching/gcvote/GCVote.java @@ -8,30 +8,27 @@ import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.LeastRecentlyUsedMap; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.MatcherWrapper; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; public final class GCVote { public static final float NO_RATING = 0; - private static final Pattern PATTERN_LOG_IN = Pattern.compile("loggedIn='([^']+)'", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_GUID = Pattern.compile("cacheId='([^']+)'", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_WAYPOINT = Pattern.compile("waypoint='([^']+)'", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_RATING = Pattern.compile("voteAvg='([0-9.]+)'", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_VOTES = Pattern.compile("voteCnt='([0-9]+)'", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_VOTE = Pattern.compile("voteUser='([0-9.]+)'", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_VOTE_ELEMENT = Pattern.compile("<vote ([^>]+)>", Pattern.CASE_INSENSITIVE); private static final int MAX_CACHED_RATINGS = 1000; private static final LeastRecentlyUsedMap<String, GCVoteRating> RATINGS_CACHE = new LeastRecentlyUsedMap.LruCache<>(MAX_CACHED_RATINGS); @@ -70,124 +67,68 @@ public final class GCVote { * @param geocodes * @return */ + @NonNull private static Map<String, GCVoteRating> getRating(final List<String> guids, final List<String> geocodes) { if (guids == null && geocodes == null) { - return null; + return Collections.emptyMap(); } - final Map<String, GCVoteRating> ratings = new HashMap<>(); + final Parameters params = new Parameters("version", "cgeo"); + final ImmutablePair<String, String> login = Settings.getGCvoteLogin(); + if (login != null) { + params.put("userName", login.left, "password", login.right); + } + // use guid or gccode for lookup + final boolean requestByGuids = CollectionUtils.isNotEmpty(guids); + if (requestByGuids) { + params.put("cacheIds", StringUtils.join(guids, ',')); + } else { + params.put("waypoints", StringUtils.join(geocodes, ',')); + } + final InputStream response = Network.getResponseStream(Network.getRequest("http://gcvote.com/getVotes.php", params)); + if (response == null) { + return Collections.emptyMap(); + } try { - final Parameters params = new Parameters(); - if (Settings.isLogin()) { - final ImmutablePair<String, String> login = Settings.getGCvoteLogin(); - if (login != null) { - params.put("userName", login.left, "password", login.right); - } - } - // use guid or gccode for lookup - boolean requestByGuids = true; - if (guids != null && !guids.isEmpty()) { - params.put("cacheIds", StringUtils.join(guids.toArray(), ',')); - } else { - params.put("waypoints", StringUtils.join(geocodes.toArray(), ',')); - requestByGuids = false; - } - params.put("version", "cgeo"); - final String page = Network.getResponseData(Network.getRequest("http://gcvote.com/getVotes.php", params)); - if (page == null) { - return null; - } - - final MatcherWrapper matcherVoteElement = new MatcherWrapper(PATTERN_VOTE_ELEMENT, page); - while (matcherVoteElement.find()) { - String voteData = matcherVoteElement.group(1); - if (voteData == null) { - continue; - } - - String id = null; - String guid = null; - final MatcherWrapper matcherGuid = new MatcherWrapper(PATTERN_GUID, voteData); - if (matcherGuid.find()) { - if (matcherGuid.groupCount() > 0) { - guid = matcherGuid.group(1); - if (requestByGuids) { - id = guid; - } - } - } - if (!requestByGuids) { - final MatcherWrapper matcherWp = new MatcherWrapper(PATTERN_WAYPOINT, voteData); - if (matcherWp.find()) { - if (matcherWp.groupCount() > 0) { - id = matcherWp.group(1); - } - } - } - if (id == null) { - continue; - } - - boolean loggedIn = false; - final MatcherWrapper matcherLoggedIn = new MatcherWrapper(PATTERN_LOG_IN, page); - if (matcherLoggedIn.find()) { - if (matcherLoggedIn.groupCount() > 0) { - if (matcherLoggedIn.group(1).equalsIgnoreCase("true")) { - loggedIn = true; - } - } - } - - float rating = NO_RATING; - try { - final MatcherWrapper matcherRating = new MatcherWrapper(PATTERN_RATING, voteData); - if (matcherRating.find()) { - rating = Float.parseFloat(matcherRating.group(1)); - } - } catch (NumberFormatException e) { - Log.w("GCVote.getRating: Failed to parse rating"); - } - if (!isValidRating(rating)) { - continue; - } - - int votes = -1; - try { - final MatcherWrapper matcherVotes = new MatcherWrapper(PATTERN_VOTES, voteData); - if (matcherVotes.find()) { - votes = Integer.parseInt(matcherVotes.group(1)); - } - } catch (NumberFormatException e) { - Log.w("GCVote.getRating: Failed to parse vote count"); - } - if (votes < 0) { - continue; - } + return getRatingsFromXMLResponse(response, requestByGuids); + } finally { + IOUtils.closeQuietly(response); + } + } - float myVote = NO_RATING; - if (loggedIn) { - try { - final MatcherWrapper matcherVote = new MatcherWrapper(PATTERN_VOTE, voteData); - if (matcherVote.find()) { - myVote = Float.parseFloat(matcherVote.group(1)); - } - } catch (NumberFormatException e) { - Log.w("GCVote.getRating: Failed to parse user's vote"); + static Map<String, GCVoteRating> getRatingsFromXMLResponse(@NonNull final InputStream response, final boolean requestByGuids) { + try { + final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + final XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(response, Charsets.UTF_8.name()); + boolean loggedIn = false; + final Map<String, GCVoteRating> ratings = new HashMap<>(); + int eventType = xpp.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + final String tagName = xpp.getName(); + if (StringUtils.equals(tagName, "vote")) { + final String guid = xpp.getAttributeValue(null, "cacheId"); + final String id = requestByGuids ? guid : xpp.getAttributeValue(null, "waypoint"); + final float myVote = loggedIn ? Float.parseFloat(xpp.getAttributeValue(null, "voteUser")) : 0; + final GCVoteRating voteRating = new GCVoteRating(Float.parseFloat(xpp.getAttributeValue(null, "voteAvg")), + Integer.parseInt(xpp.getAttributeValue(null, "voteCnt")), + myVote); + ratings.put(id, voteRating); + } else if (StringUtils.equals(tagName, "votes")) { + loggedIn = StringUtils.equals(xpp.getAttributeValue(null, "loggedIn"), "true"); } } - - if (StringUtils.isNotBlank(id)) { - GCVoteRating gcvoteRating = new GCVoteRating(rating, votes, myVote); - ratings.put(id, gcvoteRating); - RATINGS_CACHE.put(guid, gcvoteRating); - } + eventType = xpp.next(); } - } catch (RuntimeException e) { - Log.e("GCVote.getRating", e); - } + RATINGS_CACHE.putAll(ratings); + return ratings; + } catch (final Exception e) { + Log.e("Cannot parse GC vote result", e); + return Collections.emptyMap(); - return ratings; + } } /** @@ -235,16 +176,14 @@ public final class GCVote { try { final Map<String, GCVoteRating> ratings = GCVote.getRating(null, geocodes); - if (MapUtils.isNotEmpty(ratings)) { - // save found cache coordinates - for (Geocache cache : caches) { - if (ratings.containsKey(cache.getGeocode())) { - GCVoteRating rating = ratings.get(cache.getGeocode()); + // save found cache coordinates + for (Geocache cache : caches) { + if (ratings.containsKey(cache.getGeocode())) { + GCVoteRating rating = ratings.get(cache.getGeocode()); - cache.setRating(rating.getRating()); - cache.setVotes(rating.getVotes()); - cache.setMyVote(rating.getMyVote()); - } + cache.setRating(rating.getRating()); + cache.setVotes(rating.getVotes()); + cache.setMyVote(rating.getMyVote()); } } } catch (Exception e) { diff --git a/main/src/cgeo/geocaching/maps/CGeoMap.java b/main/src/cgeo/geocaching/maps/CGeoMap.java index b0e6464..d257cc6 100644 --- a/main/src/cgeo/geocaching/maps/CGeoMap.java +++ b/main/src/cgeo/geocaching/maps/CGeoMap.java @@ -6,6 +6,7 @@ import cgeo.geocaching.CacheListActivity; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; +import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; import cgeo.geocaching.Waypoint; @@ -31,7 +32,6 @@ import cgeo.geocaching.maps.interfaces.MapProvider; import cgeo.geocaching.maps.interfaces.MapSource; import cgeo.geocaching.maps.interfaces.MapViewImpl; import cgeo.geocaching.maps.interfaces.OnMapDragListener; -import cgeo.geocaching.sensors.DirectionProvider; import cgeo.geocaching.sensors.GeoDirHandler; import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.settings.Settings; @@ -125,16 +125,6 @@ public class CGeoMap extends AbstractMap implements ViewFactory { private static final int UPDATE_PROGRESS = 0; private static final int FINISHED_LOADING_DETAILS = 1; - //Menu - private static final String EXTRAS_GEOCODE = "geocode"; - private static final String EXTRAS_COORDS = "coords"; - private static final String EXTRAS_WPTTYPE = "wpttype"; - private static final String EXTRAS_MAPSTATE = "mapstate"; - private static final String EXTRAS_SEARCH = "search"; - private static final String EXTRAS_MAP_TITLE = "mapTitle"; - private static final String EXTRAS_MAP_MODE = "mapMode"; - private static final String EXTRAS_LIVE_ENABLED = "liveEnabled"; - private static final String BUNDLE_MAP_SOURCE = "mapSource"; private static final String BUNDLE_MAP_STATE = "mapState"; private static final String BUNDLE_LIVE_ENABLED = "liveEnabled"; @@ -269,13 +259,13 @@ public class CGeoMap extends AbstractMap implements ViewFactory { titleview.setText(title); } - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)) { - setTitleHoneyComb(title); + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)) { + setTitleIceCreamSandwich(title); } } - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private void setTitleHoneyComb(final String title) { + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setTitleIceCreamSandwich(final String title) { activity.getActionBar().setTitle(title); } /** Updates the progress. */ @@ -383,7 +373,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { outState.putParcelableArrayList(BUNDLE_TRAIL_HISTORY, overlayPositionAndScale.getHistory()); } - @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -402,14 +392,14 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // Get parameters from the intent final Bundle extras = activity.getIntent().getExtras(); if (extras != null) { - mapMode = (MapMode) extras.get(EXTRAS_MAP_MODE); - isLiveEnabled = extras.getBoolean(EXTRAS_LIVE_ENABLED, false); - searchIntent = extras.getParcelable(EXTRAS_SEARCH); - geocodeIntent = extras.getString(EXTRAS_GEOCODE); - coordsIntent = extras.getParcelable(EXTRAS_COORDS); - waypointTypeIntent = WaypointType.findById(extras.getString(EXTRAS_WPTTYPE)); - mapStateIntent = extras.getIntArray(EXTRAS_MAPSTATE); - mapTitle = extras.getString(EXTRAS_MAP_TITLE); + mapMode = (MapMode) extras.get(Intents.EXTRA_MAP_MODE); + isLiveEnabled = extras.getBoolean(Intents.EXTRA_LIVE_ENABLED, false); + searchIntent = extras.getParcelable(Intents.EXTRA_SEARCH); + geocodeIntent = extras.getString(Intents.EXTRA_GEOCODE); + coordsIntent = extras.getParcelable(Intents.EXTRA_COORDS); + waypointTypeIntent = WaypointType.findById(extras.getString(Intents.EXTRA_WPTTYPE)); + mapStateIntent = extras.getIntArray(Intents.EXTRA_MAPSTATE); + mapTitle = extras.getString(Intents.EXTRA_MAP_TITLE); } else { mapMode = MapMode.LIVE; @@ -439,12 +429,12 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // reset status noMapTokenShowed = false; - ActivityMixin.keepScreenOn(activity, true); + ActivityMixin.onCreate(activity, true); // set layout ActivityMixin.setTheme(activity); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { activity.getActionBar().setDisplayHomeAsUpEnabled(true); } activity.setContentView(mapProvider.getMapLayoutId()); @@ -560,6 +550,13 @@ public class CGeoMap extends AbstractMap implements ViewFactory { super.onPause(); } + @Override + public void onStop() { + // Ensure that handlers will not try to update the dialog once the view is detached. + waitDialog = null; + super.onStop(); + } + @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public boolean onCreateOptionsMenu(final Menu menu) { @@ -572,7 +569,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { subMenuStrategy.setHeaderTitle(res.getString(R.string.map_strategy_title)); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { /* if we have an Actionbar find the my position toggle */ final MenuItem item = menu.findItem(R.id.menu_toggle_mypos); myLocSwitch = new CheckBox(activity); @@ -606,6 +603,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } else { item.setTitle(res.getString(R.string.map_live_enable)); } + item.setVisible(coordsIntent == null); item = menu.findItem(R.id.menu_mycaches_mode); // own & found caches item.setChecked(Settings.isExcludeMyCaches()); @@ -849,19 +847,19 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // prepare information to restart a similar view final Intent mapIntent = new Intent(activity, Settings.getMapProvider().getMapClass()); - mapIntent.putExtra(EXTRAS_SEARCH, searchIntent); - mapIntent.putExtra(EXTRAS_GEOCODE, geocodeIntent); + mapIntent.putExtra(Intents.EXTRA_SEARCH, searchIntent); + mapIntent.putExtra(Intents.EXTRA_GEOCODE, geocodeIntent); if (coordsIntent != null) { - mapIntent.putExtra(EXTRAS_COORDS, coordsIntent); + mapIntent.putExtra(Intents.EXTRA_COORDS, coordsIntent); } - mapIntent.putExtra(EXTRAS_WPTTYPE, waypointTypeIntent != null ? waypointTypeIntent.id : null); - mapIntent.putExtra(EXTRAS_MAP_TITLE, mapTitle); - mapIntent.putExtra(EXTRAS_MAP_MODE, mapMode); - mapIntent.putExtra(EXTRAS_LIVE_ENABLED, isLiveEnabled); + mapIntent.putExtra(Intents.EXTRA_WPTTYPE, waypointTypeIntent != null ? waypointTypeIntent.id : null); + mapIntent.putExtra(Intents.EXTRA_MAP_TITLE, mapTitle); + mapIntent.putExtra(Intents.EXTRA_MAP_MODE, mapMode); + mapIntent.putExtra(Intents.EXTRA_LIVE_ENABLED, isLiveEnabled); final int[] mapState = currentMapState(); if (mapState != null) { - mapIntent.putExtra(EXTRAS_MAPSTATE, mapState); + mapIntent.putExtra(Intents.EXTRA_MAPSTATE, mapState); } // start the new map @@ -874,6 +872,9 @@ public class CGeoMap extends AbstractMap implements ViewFactory { * @return the current map state as an array of int, or null if no map state is available */ private int[] currentMapState() { + if (mapView == null) { + return null; + } final GeoPointImpl mapCenter = mapView.getMapViewCenter(); return new int[] { mapCenter.getLatitudeE6(), @@ -907,7 +908,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // minimum change of location in fraction of map width/height (whatever is smaller) for position overlay update private static final float MIN_LOCATION_DELTA = 0.01f; - Location currentLocation = new Location(""); + Location currentLocation = CgeoApplication.getInstance().currentGeo().getLocation(); float currentHeading; private long timeLastPositionOverlayCalculation = 0; @@ -923,7 +924,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { @Override public void updateGeoDir(final IGeoData geo, final float dir) { currentLocation = geo.getLocation(); - currentHeading = DirectionProvider.getDirectionNow(dir); + currentHeading = AngleUtils.getDirectionNow(dir); repaintPositionOverlay(); } @@ -1120,7 +1121,9 @@ public class CGeoMap extends AbstractMap implements ViewFactory { final boolean excludeMine = Settings.isExcludeMyCaches(); final boolean excludeDisabled = Settings.isExcludeDisabledCaches(); if (mapMode == MapMode.LIVE) { - CGeoMap.filter(caches); + synchronized(caches) { + CGeoMap.filter(caches); + } } countVisibleCaches(); if (cachesCnt < Settings.getWayPointsThreshold() || geocodeIntent != null) { @@ -1485,9 +1488,12 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // switch My Location button image private void switchMyLocationButton() { - myLocSwitch.setChecked(followMyLocation); - if (followMyLocation) { - myLocationInMiddle(app.currentGeo()); + // FIXME: temporary workaround for the absence of "follow my location" on Android 3.x (see issue #4289). + if (myLocSwitch != null) { + myLocSwitch.setChecked(followMyLocation); + if (followMyLocation) { + myLocationInMiddle(app.currentGeo()); + } } } @@ -1558,42 +1564,41 @@ public class CGeoMap extends AbstractMap implements ViewFactory { public static void startActivitySearch(final Activity fromActivity, final SearchResult search, final String title) { final Intent mapIntent = newIntent(fromActivity); - mapIntent.putExtra(EXTRAS_SEARCH, search); - mapIntent.putExtra(EXTRAS_MAP_MODE, MapMode.LIST); - mapIntent.putExtra(EXTRAS_LIVE_ENABLED, false); + mapIntent.putExtra(Intents.EXTRA_SEARCH, search); + mapIntent.putExtra(Intents.EXTRA_MAP_MODE, MapMode.LIST); + mapIntent.putExtra(Intents.EXTRA_LIVE_ENABLED, false); if (StringUtils.isNotBlank(title)) { - mapIntent.putExtra(CGeoMap.EXTRAS_MAP_TITLE, title); + mapIntent.putExtra(Intents.EXTRA_MAP_TITLE, title); } fromActivity.startActivity(mapIntent); } - public static void startActivityLiveMap(final Activity fromActivity) { - final Intent mapIntent = newIntent(fromActivity); - mapIntent.putExtra(EXTRAS_MAP_MODE, MapMode.LIVE); - mapIntent.putExtra(EXTRAS_LIVE_ENABLED, Settings.isLiveMap()); - fromActivity.startActivity(mapIntent); + public static Intent getLiveMapIntent(final Activity fromActivity) { + return newIntent(fromActivity) + .putExtra(Intents.EXTRA_MAP_MODE, MapMode.LIVE) + .putExtra(Intents.EXTRA_LIVE_ENABLED, Settings.isLiveMap()); } public static void startActivityCoords(final Activity fromActivity, final Geopoint coords, final WaypointType type, final String title) { final Intent mapIntent = newIntent(fromActivity); - mapIntent.putExtra(EXTRAS_MAP_MODE, MapMode.COORDS); - mapIntent.putExtra(EXTRAS_LIVE_ENABLED, false); - mapIntent.putExtra(EXTRAS_COORDS, coords); + mapIntent.putExtra(Intents.EXTRA_MAP_MODE, MapMode.COORDS); + mapIntent.putExtra(Intents.EXTRA_LIVE_ENABLED, false); + mapIntent.putExtra(Intents.EXTRA_COORDS, coords); if (type != null) { - mapIntent.putExtra(EXTRAS_WPTTYPE, type.id); + mapIntent.putExtra(Intents.EXTRA_WPTTYPE, type.id); } if (StringUtils.isNotBlank(title)) { - mapIntent.putExtra(EXTRAS_MAP_TITLE, title); + mapIntent.putExtra(Intents.EXTRA_MAP_TITLE, title); } fromActivity.startActivity(mapIntent); } public static void startActivityGeoCode(final Activity fromActivity, final String geocode) { final Intent mapIntent = newIntent(fromActivity); - mapIntent.putExtra(EXTRAS_MAP_MODE, MapMode.SINGLE); - mapIntent.putExtra(EXTRAS_LIVE_ENABLED, false); - mapIntent.putExtra(EXTRAS_GEOCODE, geocode); - mapIntent.putExtra(EXTRAS_MAP_TITLE, geocode); + mapIntent.putExtra(Intents.EXTRA_MAP_MODE, MapMode.SINGLE); + mapIntent.putExtra(Intents.EXTRA_LIVE_ENABLED, false); + mapIntent.putExtra(Intents.EXTRA_GEOCODE, geocode); + mapIntent.putExtra(Intents.EXTRA_MAP_TITLE, geocode); fromActivity.startActivity(mapIntent); } diff --git a/main/src/cgeo/geocaching/maps/MapActivity.java b/main/src/cgeo/geocaching/maps/MapActivity.java new file mode 100644 index 0000000..28668ca --- /dev/null +++ b/main/src/cgeo/geocaching/maps/MapActivity.java @@ -0,0 +1,17 @@ +package cgeo.geocaching.maps; + +import android.app.Activity; +import android.os.Bundle; + +/** + * This activity provides an entry point for external intent calls, and then forwards to the currently used map activity + * implementation. + */ +public class MapActivity extends Activity { + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + startActivity(CGeoMap.getLiveMapIntent(this)); + finish(); + } +} diff --git a/main/src/cgeo/geocaching/network/HtmlImage.java b/main/src/cgeo/geocaching/network/HtmlImage.java index 4a5c506..ab902d2 100644 --- a/main/src/cgeo/geocaching/network/HtmlImage.java +++ b/main/src/cgeo/geocaching/network/HtmlImage.java @@ -154,6 +154,10 @@ public class HtmlImage implements Html.ImageGetter { if (view == null) { return drawable.toBlocking().lastOrDefault(null); } + return getContainerDrawable(drawable); + } + + protected BitmapDrawable getContainerDrawable(final Observable<BitmapDrawable> drawable) { return new ContainerDrawable(view, drawable); } @@ -162,17 +166,17 @@ public class HtmlImage implements Html.ImageGetter { public Observable<BitmapDrawable> fetchDrawable(final String url) { if (StringUtils.isBlank(url) || ImageUtils.containsPattern(url, BLOCKED)) { - return Observable.from(ImageUtils.getTransparent1x1Drawable(resources)); + return Observable.just(ImageUtils.getTransparent1x1Drawable(resources)); } // Explicit local file URLs are loaded from the filesystem regardless of their age. The IO part is short // enough to make the whole operation on the computation scheduler. if (FileUtils.isFileUrl(url)) { - return Observable.defer(new Func0<Observable<? extends BitmapDrawable>>() { + return Observable.defer(new Func0<Observable<BitmapDrawable>>() { @Override - public Observable<? extends BitmapDrawable> call() { + public Observable<BitmapDrawable> call() { final Bitmap bitmap = loadCachedImage(FileUtils.urlToFile(url), true).getLeft(); - return bitmap != null ? Observable.from(ImageUtils.scaleBitmapToFitDisplay(bitmap)) : Observable.<BitmapDrawable>empty(); + return bitmap != null ? Observable.just(ImageUtils.scaleBitmapToFitDisplay(bitmap)) : Observable.<BitmapDrawable>empty(); } }).subscribeOn(RxUtils.computationScheduler); } diff --git a/main/src/cgeo/geocaching/network/Network.java b/main/src/cgeo/geocaching/network/Network.java index 40f7f7e..e8c2b28 100644 --- a/main/src/cgeo/geocaching/network/Network.java +++ b/main/src/cgeo/geocaching/network/Network.java @@ -238,7 +238,7 @@ public abstract class Network { Log.w(status + " [" + response.getStatusLine().getReasonPhrase() + "]" + formatTimeSpan(before) + reqLogStr); } return response; - } catch (final IOException e) { + } catch (final Exception e) { final String timeSpan = formatTimeSpan(before); Log.w("Failure" + timeSpan + reqLogStr + " (" + e.toString() + ")"); } diff --git a/main/src/cgeo/geocaching/network/SmileyImage.java b/main/src/cgeo/geocaching/network/SmileyImage.java index ebac2bb..86baeaa 100644 --- a/main/src/cgeo/geocaching/network/SmileyImage.java +++ b/main/src/cgeo/geocaching/network/SmileyImage.java @@ -1,14 +1,21 @@ package cgeo.geocaching.network; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.utils.ImageUtils; +import cgeo.geocaching.utils.ImageUtils.LineHeightContainerDrawable; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import rx.Observable; + import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.widget.TextView; +/** + * Specialized image class for fetching and displaying smileys in the log book. + */ public class SmileyImage extends HtmlImage { public SmileyImage(final String geocode, final TextView view) { @@ -20,10 +27,8 @@ public class SmileyImage extends HtmlImage { final Bitmap bitmap = loadResult.getLeft(); BitmapDrawable drawable; if (bitmap != null) { - final int lineHeight = (int) (view.getLineHeight() * 0.8); drawable = new BitmapDrawable(view.getResources(), bitmap); - final int width = drawable.getIntrinsicWidth() * lineHeight / drawable.getIntrinsicHeight(); - drawable.setBounds(0, 0, width, lineHeight); + drawable.setBounds(ImageUtils.scaleImageToLineHeight(drawable, view)); } else { drawable = null; @@ -31,4 +36,9 @@ public class SmileyImage extends HtmlImage { return new ImmutablePair<>(drawable, loadResult.getRight()); } + @Override + protected BitmapDrawable getContainerDrawable(final Observable<BitmapDrawable> drawable) { + return new LineHeightContainerDrawable(view, drawable); + } + } diff --git a/main/src/cgeo/geocaching/playservices/LocationProvider.java b/main/src/cgeo/geocaching/playservices/LocationProvider.java index a1edb7a..f235a3b 100644 --- a/main/src/cgeo/geocaching/playservices/LocationProvider.java +++ b/main/src/cgeo/geocaching/playservices/LocationProvider.java @@ -2,8 +2,9 @@ package cgeo.geocaching.playservices; import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.RxUtils.LooperCallbacks; +import cgeo.geocaching.utils.RxUtils; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks; @@ -13,45 +14,105 @@ import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; import rx.functions.Func1; +import rx.subjects.ReplaySubject; +import rx.subscriptions.Subscriptions; import android.content.Context; import android.location.Location; import android.os.Bundle; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; -public class LocationProvider extends LooperCallbacks<IGeoData> implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener { +public class LocationProvider implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener { - private final LocationClient locationClient; - private final boolean lowPower; - private final boolean withInitialLocation; - private boolean onlyInitialLocation; private static final LocationRequest LOCATION_REQUEST = LocationRequest.create().setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY).setInterval(2000).setFastestInterval(250); private static final LocationRequest LOCATION_REQUEST_LOW_POWER = LocationRequest.create().setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY).setInterval(10000).setFastestInterval(5000); + private static final AtomicInteger mostPreciseCount = new AtomicInteger(0); + private static final AtomicInteger lowPowerCount = new AtomicInteger(0); + private static LocationProvider instance = null; + private static ReplaySubject<IGeoData> subject = ReplaySubject.createWithSize(1); + private final LocationClient locationClient; + + private static synchronized LocationProvider getInstance(final Context context) { + if (instance == null) { + instance = new LocationProvider(context); + } + return instance; + } + + private synchronized void updateRequest() { + if (locationClient.isConnected()) { + if (mostPreciseCount.get() > 0) { + Log.d("LocationProvider: requesting most precise locations"); + locationClient.requestLocationUpdates(LOCATION_REQUEST, this, RxUtils.looperCallbacksLooper); + } else if (lowPowerCount.get() > 0) { + Log.d("LocationProvider: requesting low-power locations"); + locationClient.requestLocationUpdates(LOCATION_REQUEST_LOW_POWER, this, RxUtils.looperCallbacksLooper); + } else { + Log.d("LocationProvider: stopping location requests"); + locationClient.removeLocationUpdates(this); + } + } + } - public static Observable<IGeoData> getInitialLocation(final Context context, final boolean lowPower) { - return Observable.create(new LocationProvider(context, lowPower, true, true)); + private static Observable<IGeoData> get(final Context context, final AtomicInteger reference) { + final LocationProvider instance = getInstance(context); + return Observable.create(new OnSubscribe<IGeoData>() { + @Override + public void call(final Subscriber<? super IGeoData> subscriber) { + if (reference.incrementAndGet() == 1) { + instance.updateRequest(); + } + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + RxUtils.looperCallbacksWorker.schedule(new Action0() { + @Override + public void call() { + if (reference.decrementAndGet() == 0) { + instance.updateRequest(); + } + } + }, 2500, TimeUnit.MILLISECONDS); + } + })); + subscriber.add(subject.subscribe(new Action1<IGeoData>() { + @Override + public void call(final IGeoData geoData) { + subscriber.onNext(geoData); + } + })); + } + }); } - public static Observable<IGeoData> getMostPrecise(Context context, boolean withInitialLocation) { - return Observable.create(new LocationProvider(context, false, withInitialLocation, false)); + private static Observable<IGeoData> getInitialLocation(final Context context, final boolean lowPower) { + return get(context, lowPower ? lowPowerCount : mostPreciseCount).first(); + } + + public static Observable<IGeoData> getMostPrecise(final Context context) { + return get(context, mostPreciseCount); } public static Observable<IGeoData> getLowPower(Context context, boolean withInitialLocation) { final Observable<IGeoData> initialLocationObservable = withInitialLocation ? getInitialLocation(context, true) : Observable.<IGeoData>empty(); - final Observable<IGeoData> lowPowerObservable = Observable.create(new LocationProvider(context, true, false, false)); - final Observable<IGeoData> lowPowerObservable2 = Observable.create(new LocationProvider(context, true, false, false)); - final Observable<IGeoData> gpsFixObservable = getMostPrecise(context, false).takeFirst(new Func1<IGeoData, Boolean>() { + final Observable<IGeoData> lowPowerObservable = get(context, lowPowerCount).skip(1); + final Observable<IGeoData> gpsFixObservable = get(context, mostPreciseCount).skip(1).lift(RxUtils.operatorTakeUntil(new Func1<IGeoData, Boolean>() { @Override public Boolean call(final IGeoData geoData) { return geoData.getAccuracy() < 20; } - }).delaySubscription(2, TimeUnit.SECONDS); - return initialLocationObservable.concatWith(lowPowerObservable.mergeWith(gpsFixObservable).first() - .concatWith(lowPowerObservable2).timeout(60, TimeUnit.SECONDS).retry()); + })); + return initialLocationObservable.concatWith(lowPowerObservable.ambWith(gpsFixObservable.delaySubscription(6, TimeUnit.SECONDS)).first() + .concatWith(lowPowerObservable).timeout(25, TimeUnit.SECONDS).retry()); } /** @@ -61,48 +122,19 @@ public class LocationProvider extends LooperCallbacks<IGeoData> implements Conne * at will. * * @param context the context used to retrieve the system services - * @param lowPower <tt>true</tt> if the GPS must not be used - * @param withInitialLocation <tt>true</tt> if the initial location must be provided - * @param onlyInitialLocation <tt>true</tt> if the observable must be closed after providing the initial location */ - protected LocationProvider(final Context context, final boolean lowPower, final boolean withInitialLocation, final boolean onlyInitialLocation) { - super(lowPower ? 0 : 2500, TimeUnit.MILLISECONDS); - if (onlyInitialLocation && !withInitialLocation) { - throw new IllegalArgumentException("LocationProvider: cannot request only initial location without requesting it at all"); + private LocationProvider(final Context context) { + final IGeoData initialLocation = GeoData.getInitialLocation(context); + if (initialLocation != null) { + subject.onNext(initialLocation); } locationClient = new LocationClient(context, this, this); - this.withInitialLocation = withInitialLocation; - this.lowPower = lowPower; - this.onlyInitialLocation = onlyInitialLocation; - } - - @Override - public void onStart() { - Log.d("LocationProvider: starting the location listener - lowPower: " + lowPower); locationClient.connect(); } @Override - public void onStop() { - Log.d("LocationProvider: stopping the location listener - lowPower: " + lowPower); - if (!onlyInitialLocation) { - locationClient.removeLocationUpdates(this); - } - locationClient.disconnect(); - } - - @Override public void onConnected(final Bundle bundle) { - if (withInitialLocation) { - final Location initialLocation = locationClient.getLastLocation(); - Log.d("LocationProvider: starting with " + (initialLocation == null ? "dummy" : "previous") + " location"); - subscriber.onNext(initialLocation != null ? new GeoData(initialLocation) : GeoData.dummyLocation()); - } - if (onlyInitialLocation) { - subscriber.onCompleted(); - } else { - locationClient.requestLocationUpdates(lowPower ? LOCATION_REQUEST_LOW_POWER : LOCATION_REQUEST, this); - } + updateRequest(); } @Override @@ -112,12 +144,14 @@ public class LocationProvider extends LooperCallbacks<IGeoData> implements Conne @Override public void onConnectionFailed(final ConnectionResult connectionResult) { Log.e("cannot connect to Google Play location service: " + connectionResult); - subscriber.onError(new RuntimeException("Connection failed: " + connectionResult)); + subject.onError(new RuntimeException("Connection failed: " + connectionResult)); } @Override public void onLocationChanged(final Location location) { - location.setProvider(lowPower ? GeoData.LOW_POWER_PROVIDER : GeoData.FUSED_PROVIDER); - subscriber.onNext(new GeoData(location)); + if (Settings.useLowPowerMode()) { + location.setProvider(GeoData.LOW_POWER_PROVIDER); + } + subject.onNext(new GeoData(location)); } } diff --git a/main/src/cgeo/geocaching/sensors/GeoData.java b/main/src/cgeo/geocaching/sensors/GeoData.java index 1291d3c..561c09f 100644 --- a/main/src/cgeo/geocaching/sensors/GeoData.java +++ b/main/src/cgeo/geocaching/sensors/GeoData.java @@ -2,20 +2,44 @@ package cgeo.geocaching.sensors; import cgeo.geocaching.enumerations.LocationProviderType; import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.utils.Log; +import android.content.Context; import android.location.Location; import android.location.LocationManager; +import javax.annotation.Nullable; + public class GeoData extends Location implements IGeoData { public static final String INITIAL_PROVIDER = "initial"; public static final String FUSED_PROVIDER = "fused"; public static final String LOW_POWER_PROVIDER = "low-power"; + // Some devices will not have the last position available (for example the emulator). In this case, + // rather than waiting forever for a position update which might never come, we emulate it by placing + // the user arbitrarly at Paris Notre-Dame, one of the most visited free tourist attractions in the world. + final public static GeoData DUMMY_LOCATION = new GeoData(new Location(INITIAL_PROVIDER)); + static { + DUMMY_LOCATION.setLatitude(48.85308); + DUMMY_LOCATION.setLongitude(2.34962); + } + public GeoData(final Location location) { super(location); } + @Nullable + static Location best(@Nullable final Location gpsLocation, @Nullable final Location netLocation) { + if (isRecent(gpsLocation) || !(netLocation != null)) { + return gpsLocation; + } + if (!(gpsLocation != null)) { + return netLocation; + } + return gpsLocation.getTime() >= netLocation.getTime() ? gpsLocation : netLocation; + } + @Override public Location getLocation() { return this; @@ -48,13 +72,35 @@ public class GeoData extends Location implements IGeoData { return new Geopoint(this); } - // Some devices will not have the last position available (for example the emulator). In this case, - // rather than waiting forever for a position update which might never come, we emulate it by placing - // the user arbitrarly at Paris Notre-Dame, one of the most visited free tourist attractions in the world. - public static GeoData dummyLocation() { - final Location location = new Location(INITIAL_PROVIDER); - location.setLatitude(48.85308); - location.setLongitude(2.34962); - return new GeoData(location); + @Nullable public static GeoData getInitialLocation(final Context context) { + final LocationManager geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (geoManager == null) { + Log.w("No LocationManager available"); + return null; + } + try { + // Try to find a sensible initial location from the last locations known to Android. + final Location lastGpsLocation = geoManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + final Location lastNetworkLocation = geoManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + final Location bestLocation = best(lastGpsLocation, lastNetworkLocation); + if (bestLocation != null) { + bestLocation.setProvider(INITIAL_PROVIDER); + return new GeoData(bestLocation); + } + Log.i("No last known location available"); + return null; + } catch (final Exception e) { + // This error is non-fatal as its only consequence is that we will start with a dummy location + // instead of a previously known one. + Log.e("Error when retrieving last known location", e); + return null; + } } + + + + public static boolean isRecent(@Nullable final Location location) { + return location != null && System.currentTimeMillis() <= location.getTime() + 30000; + } + } diff --git a/main/src/cgeo/geocaching/sensors/GeoDataProvider.java b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java index 19eef6a..faecbe3 100644 --- a/main/src/cgeo/geocaching/sensors/GeoDataProvider.java +++ b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java @@ -3,6 +3,8 @@ package cgeo.geocaching.sensors; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.RxUtils.LooperCallbacks; +import org.apache.commons.lang3.StringUtils; + import rx.Observable; import android.content.Context; @@ -15,29 +17,11 @@ import java.util.concurrent.TimeUnit; public class GeoDataProvider extends LooperCallbacks<IGeoData> { + private final Context context; private final LocationManager geoManager; - 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 static class LocationData { - public Location location; - public long timestamp = 0; - - public void update(final Location location) { - this.location = location; - timestamp = System.currentTimeMillis(); - } - - public boolean isRecent() { - return isValid() && System.currentTimeMillis() < timestamp + 30000; - } - - public boolean isValid() { - return location != null; - } - } + private Location latestGPSLocation = null; + private final Listener networkListener = new Listener(); + private final Listener gpsListener = new Listener(); /** * Build a new geo data provider object. @@ -49,7 +33,8 @@ public class GeoDataProvider extends LooperCallbacks<IGeoData> { */ protected GeoDataProvider(final Context context) { super(2500, TimeUnit.MILLISECONDS); - geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + this.context = context.getApplicationContext(); + geoManager = (LocationManager) this.context.getSystemService(Context.LOCATION_SERVICE); } public static Observable<IGeoData> create(final Context context) { @@ -58,14 +43,20 @@ public class GeoDataProvider extends LooperCallbacks<IGeoData> { @Override public void onStart() { - subscriber.onNext(findInitialLocation()); + final IGeoData initialLocation = GeoData.getInitialLocation(context); + if (initialLocation != null) { + subscriber.onNext(initialLocation); + } Log.d("GeoDataProvider: starting the GPS and network listeners"); - 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, e); - } + try { + geoManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsListener); + } catch (final Exception e) { + Log.w("Unable to create GPS location provider: " + e.getMessage()); + } + try { + geoManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, networkListener); + } catch (final Exception e) { + Log.w("Unable to create network location provider: " + e.getMessage()); } } @@ -76,50 +67,16 @@ public class GeoDataProvider extends LooperCallbacks<IGeoData> { geoManager.removeUpdates(gpsListener); } - private IGeoData findInitialLocation() { - final Location initialLocation = new Location(GeoData.INITIAL_PROVIDER); - try { - // Try to find a sensible initial location from the last locations known to Android. - final Location lastGpsLocation = geoManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - final Location lastNetworkLocation = geoManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - - // If both providers are non-null, take the most recent one - if (lastGpsLocation != null && lastNetworkLocation != null) { - if (lastGpsLocation.getTime() >= lastNetworkLocation.getTime()) { - copyCoords(initialLocation, lastGpsLocation); - } else { - copyCoords(initialLocation, lastNetworkLocation); - } - } else if (lastGpsLocation != null) { - copyCoords(initialLocation, lastGpsLocation); - } else if (lastNetworkLocation != null) { - copyCoords(initialLocation, lastNetworkLocation); - } else { - Log.i("GeoDataProvider: no last known location available"); - return GeoData.dummyLocation(); - } - } catch (final Exception e) { - // This error is non-fatal as its only consequence is that we will start with a dummy location - // instead of a previously known one. - Log.e("GeoDataProvider: error when retrieving last known location", e); - } - // Start with an historical GeoData just in case someone queries it before we get - // a chance to get any information. - return new GeoData(initialLocation); - } - - private static void copyCoords(final Location target, final Location source) { - target.setLatitude(source.getLatitude()); - target.setLongitude(source.getLongitude()); - } - private class Listener implements LocationListener { - private final String locationProvider; - private final LocationData locationData; - Listener(final String locationProvider, final LocationData locationData) { - this.locationProvider = locationProvider; - this.locationData = locationData; + @Override + public void onLocationChanged(final Location location) { + if (StringUtils.equals(location.getProvider(), LocationManager.GPS_PROVIDER)) { + latestGPSLocation = location; + assign(latestGPSLocation); + } else { + assign(GeoData.best(latestGPSLocation, location)); + } } @Override @@ -136,35 +93,11 @@ public class GeoDataProvider extends LooperCallbacks<IGeoData> { public void onProviderEnabled(final String provider) { // nothing } - - @Override - public void onLocationChanged(final Location location) { - locationData.update(location); - selectBest(); - } - } - - private LocationData best() { - if (gpsLocation.isRecent() || !netLocation.isValid()) { - return gpsLocation.isValid() ? gpsLocation : null; - } - if (!gpsLocation.isValid()) { - return netLocation; - } - return gpsLocation.timestamp > netLocation.timestamp ? gpsLocation : netLocation; } - private void selectBest() { - assign(best()); - } - - private void assign(final LocationData locationData) { - if (locationData == null) { - return; - } - + private void assign(final Location location) { // We do not necessarily get signalled when satellites go to 0/0. - final IGeoData current = new GeoData(locationData.location); + final IGeoData current = new GeoData(location); subscriber.onNext(current); } diff --git a/main/src/cgeo/geocaching/sensors/GeoDirHandler.java b/main/src/cgeo/geocaching/sensors/GeoDirHandler.java index 4a63c3d..d127784 100644 --- a/main/src/cgeo/geocaching/sensors/GeoDirHandler.java +++ b/main/src/cgeo/geocaching/sensors/GeoDirHandler.java @@ -2,6 +2,7 @@ package cgeo.geocaching.sensors; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.AngleUtils; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -21,7 +22,7 @@ import rx.subscriptions.CompositeSubscription; * accordingly in {@code onPause}. * * The direction is always relative to the top of the device (natural direction), and that it must - * be fixed using {@link DirectionProvider#getDirectionNow(float)}. When the direction is derived from the GPS, + * be fixed using {@link cgeo.geocaching.utils.AngleUtils#getDirectionNow(float)}. When the direction is derived from the GPS, * it is altered so that the fix can still be applied as if the information came from the compass. */ public abstract class GeoDirHandler { @@ -77,7 +78,7 @@ public abstract class GeoDirHandler { private static float fixDirection(final IGeoData geoData, final float direction) { final boolean useGPSBearing = !Settings.isUseCompass() || geoData.getSpeed() > 5; - return useGPSBearing ? DirectionProvider.reverseDirectionNow(geoData.getBearing()) : direction; + return useGPSBearing ? AngleUtils.reverseDirectionNow(geoData.getBearing()) : direction; } /** diff --git a/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java b/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java index ec29a6a..5f12e99 100644 --- a/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java +++ b/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java @@ -13,8 +13,6 @@ import android.location.LocationManager; public class GpsStatusProvider extends LooperCallbacks<Status> { - public static final Status NO_GPS = new Status(false, 0, 0); - public static class Status { final public boolean gpsEnabled; final public int satellitesVisible; @@ -31,6 +29,8 @@ public class GpsStatusProvider extends LooperCallbacks<Status> { private final GpsStatus.Listener gpsStatusListener = new GpsStatusListener(); private Status latest = new Status(false, 0, 0); + private static final Status NO_GPS = new Status(false, 0, 0); + /** * Build a new gps status provider object. * <p/> @@ -50,6 +50,7 @@ public class GpsStatusProvider extends LooperCallbacks<Status> { @Override protected void onStart() { Log.d("GpsStatusProvider: starting the GPS status listener"); + subscriber.onNext(NO_GPS); geoManager.addGpsStatusListener(gpsStatusListener); } diff --git a/main/src/cgeo/geocaching/sensors/DirectionProvider.java b/main/src/cgeo/geocaching/sensors/OrientationProvider.java index 907dc3d..83e0638 100644 --- a/main/src/cgeo/geocaching/sensors/DirectionProvider.java +++ b/main/src/cgeo/geocaching/sensors/OrientationProvider.java @@ -1,7 +1,6 @@ package cgeo.geocaching.sensors; -import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.utils.AngleUtils; +import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.RxUtils.LooperCallbacks; import rx.Observable; @@ -11,20 +10,21 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; -import android.view.Surface; -import android.view.WindowManager; -public class DirectionProvider extends LooperCallbacks<Float> implements SensorEventListener { - - private static final WindowManager WINDOW_MANAGER = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); +public class OrientationProvider extends LooperCallbacks<Float> implements SensorEventListener { private final SensorManager sensorManager; private final Sensor orientationSensor; @SuppressWarnings("deprecation") - protected DirectionProvider(final Context context) { + protected OrientationProvider(final Context context) { sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + if (orientationSensor != null) { + Log.d("OrientationProvider: sensor found"); + } else { + Log.w("OrientationProvider: no orientation sensor on this device"); + } } @Override @@ -45,47 +45,23 @@ public class DirectionProvider extends LooperCallbacks<Float> implements SensorE @Override public void onStart() { if (orientationSensor != null) { + Log.d("OrientationProvider: starting the orientation provider"); sensorManager.registerListener(this, orientationSensor, SensorManager.SENSOR_DELAY_NORMAL); + } else { + subscriber.onError(new RuntimeException("orientation sensor is absent on this device")); } } @Override public void onStop() { if (orientationSensor != null) { + Log.d("OrientationProvider: stopping the orientation provider"); sensorManager.unregisterListener(this); } } public static Observable<Float> create(final Context context) { - return Observable.create(new DirectionProvider(context)); - } - - /** - * Take the phone rotation (through a given activity) in account and adjust the direction. - * - * @param direction the unadjusted direction in degrees, in the [0, 360[ range - * @return the adjusted direction in degrees, in the [0, 360[ range - */ - - public static float getDirectionNow(final float direction) { - return AngleUtils.normalize(direction + getRotationOffset()); - } - - static float reverseDirectionNow(final float direction) { - return AngleUtils.normalize(direction - getRotationOffset()); - } - - private static int getRotationOffset() { - switch (WINDOW_MANAGER.getDefaultDisplay().getRotation()) { - case Surface.ROTATION_90: - return 90; - case Surface.ROTATION_180: - return 180; - case Surface.ROTATION_270: - return 270; - default: - return 0; - } + return Observable.create(new OrientationProvider(context)); } } diff --git a/main/src/cgeo/geocaching/sensors/RotationProvider.java b/main/src/cgeo/geocaching/sensors/RotationProvider.java new file mode 100644 index 0000000..40e2c3c --- /dev/null +++ b/main/src/cgeo/geocaching/sensors/RotationProvider.java @@ -0,0 +1,83 @@ +package cgeo.geocaching.sensors; + +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils.LooperCallbacks; + +import rx.Observable; + +import android.annotation.TargetApi; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +public class RotationProvider extends LooperCallbacks<Float> implements SensorEventListener { + + private final SensorManager sensorManager; + private final Sensor rotationSensor; + private final float[] rotationMatrix = new float[16]; + private final float[] orientation = new float[4]; + private final float[] values = new float[4]; + + @TargetApi(19) + protected RotationProvider(final Context context, final boolean lowPower) { + sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + // The geomagnetic rotation vector introduced in Android 4.4 (API 19) requires less power. Favour it + // even if it is more sensible to noise in low-power settings. + final Sensor sensor = lowPower ? sensorManager.getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR) : null; + if (sensor != null) { + rotationSensor = sensor; + Log.d("RotationProvider: geomagnetic (low-power) sensor found"); + } else { + rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + if (rotationSensor != null) { + Log.d("RotationProvider: sensor found"); + } else { + Log.w("RotationProvider: no rotation sensor on this device"); + } + } + } + + @Override + public void onSensorChanged(final SensorEvent event) { + // On some Samsung devices, SensorManager#getRotationMatrixFromVector throws an exception if the rotation + // vector has more than 4 elements. Since only the four first elements are used, we can truncate the vector + // without losing precision. + if (event.values.length > 4) { + System.arraycopy(event.values, 0, values, 0, 4); + SensorManager.getRotationMatrixFromVector(rotationMatrix, values); + } else { + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values); + } + SensorManager.getOrientation(rotationMatrix, orientation); + subscriber.onNext((float) (orientation[0] * 180 / Math.PI)); + } + + @Override + public void onAccuracyChanged(final Sensor sensor, final int accuracy) { + } + + @Override + public void onStart() { + if (rotationSensor != null) { + Log.d("RotationProvider: starting the rotation provider"); + sensorManager.registerListener(this, rotationSensor, SensorManager.SENSOR_DELAY_NORMAL); + } else { + subscriber.onError(new RuntimeException("rotation sensor is absent on this device")); + } + } + + @Override + public void onStop() { + if (rotationSensor != null) { + Log.d("RotationProvider: stopping the rotation provider"); + sensorManager.unregisterListener(this); + } + } + + public static Observable<Float> create(final Context context, final boolean lowPower) { + return Observable.create(new RotationProvider(context, lowPower)); + } + +} diff --git a/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java b/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java index 84c343a..93480ee 100644 --- a/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java +++ b/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java @@ -69,7 +69,7 @@ public class RegisterSend2CgeoPreference extends AbstractClickablePreference { final String[] strings = StringUtils.split(Network.getResponseData(response), ','); Settings.setWebNameCode(nam, strings[0]); try { - return Observable.from(Integer.parseInt(strings[1].trim())); + return Observable.just(Integer.parseInt(strings[1].trim())); } catch (final Exception e) { Log.e("RegisterSend2CgeoPreference", e); } diff --git a/main/src/cgeo/geocaching/settings/Settings.java b/main/src/cgeo/geocaching/settings/Settings.java index 6b9cbd5..4dd959b 100644 --- a/main/src/cgeo/geocaching/settings/Settings.java +++ b/main/src/cgeo/geocaching/settings/Settings.java @@ -34,6 +34,7 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.Configuration; import android.content.res.Resources; +import android.os.Build; import android.os.Environment; import android.preference.PreferenceManager; @@ -53,6 +54,17 @@ public class Settings { public static final int SHOW_WP_THRESHOLD_MAX = 50; private static final int MAP_SOURCE_DEFAULT = GoogleMapProvider.GOOGLE_MAP_ID.hashCode(); + public static final boolean HW_ACCEL_DISABLED_BY_DEFAULT = + Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || + StringUtils.equals(Build.MODEL, "HTC One X") || // HTC One X + StringUtils.equals(Build.MODEL, "HTC One S") || // HTC One S + StringUtils.equals(Build.MODEL, "GT-I8190") || // Samsung S3 mini + StringUtils.equals(Build.MODEL, "GT-S6310L") || // Samsung Galaxy Young + StringUtils.equals(Build.MODEL, "GT-P5210") || // Samsung Galaxy Tab 3 + StringUtils.equals(Build.MODEL, "GT-S7580") || // Samsung Galaxy Trend Plus + StringUtils.equals(Build.MODEL, "ST25i") || // Sony Xperia U + StringUtils.equals(Build.MODEL, "bq Aquaris 5"); // bq Aquaris 5 + private final static int unitsMetric = 1; // twitter api keys @@ -65,6 +77,8 @@ public class Settings { Min, Sec; + static int DEFAULT_INT_VALUE = Min.ordinal(); + public static CoordInputFormatEnum fromInt(final int id) { final CoordInputFormatEnum[] values = CoordInputFormatEnum.values(); if (id < 0 || id >= values.length) { @@ -94,74 +108,89 @@ public class Settings { } private static void migrateSettings() { - // migrate from non standard file location and integer based boolean types - final int oldVersion = getInt(R.string.pref_settingsversion, 0); - if (oldVersion < 1) { - final String oldPreferencesName = "cgeo.pref"; - final SharedPreferences old = CgeoApplication.getInstance().getSharedPreferences(oldPreferencesName, Context.MODE_PRIVATE); + final int LATEST_PREFERENCES_VERSION = 2; + final int currentVersion = getInt(R.string.pref_settingsversion, 0); + + // No need to migrate if we are up to date. + if (currentVersion == LATEST_PREFERENCES_VERSION) { + return; + } + + // No need to migrate if we don't have older settings, defaults will be used instead. + final String preferencesNameV0 = "cgeo.pref"; + final SharedPreferences prefsV0 = CgeoApplication.getInstance().getSharedPreferences(preferencesNameV0, Context.MODE_PRIVATE); + if (currentVersion == 0 && prefsV0.getAll().isEmpty()) { + final Editor e = sharedPrefs.edit(); + e.putInt(getKey(R.string.pref_settingsversion), LATEST_PREFERENCES_VERSION); + e.commit(); + return; + } + + if (currentVersion < 1) { + // migrate from non standard file location and integer based boolean types final Editor e = sharedPrefs.edit(); - e.putString(getKey(R.string.pref_temp_twitter_token_secret), old.getString(getKey(R.string.pref_temp_twitter_token_secret), null)); - e.putString(getKey(R.string.pref_temp_twitter_token_public), old.getString(getKey(R.string.pref_temp_twitter_token_public), null)); - e.putBoolean(getKey(R.string.pref_help_shown), old.getInt(getKey(R.string.pref_help_shown), 0) != 0); - e.putFloat(getKey(R.string.pref_anylongitude), old.getFloat(getKey(R.string.pref_anylongitude), 0)); - e.putFloat(getKey(R.string.pref_anylatitude), old.getFloat(getKey(R.string.pref_anylatitude), 0)); - e.putBoolean(getKey(R.string.pref_offlinemaps), 0 != old.getInt(getKey(R.string.pref_offlinemaps), 1)); - e.putBoolean(getKey(R.string.pref_offlinewpmaps), 0 != old.getInt(getKey(R.string.pref_offlinewpmaps), 0)); - e.putString(getKey(R.string.pref_webDeviceCode), old.getString(getKey(R.string.pref_webDeviceCode), null)); - e.putString(getKey(R.string.pref_webDeviceName), old.getString(getKey(R.string.pref_webDeviceName), null)); - e.putBoolean(getKey(R.string.pref_maplive), old.getInt(getKey(R.string.pref_maplive), 1) != 0); - e.putInt(getKey(R.string.pref_mapsource), old.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT)); - e.putBoolean(getKey(R.string.pref_twitter), 0 != old.getInt(getKey(R.string.pref_twitter), 0)); - e.putBoolean(getKey(R.string.pref_showaddress), 0 != old.getInt(getKey(R.string.pref_showaddress), 1)); - e.putBoolean(getKey(R.string.pref_showcaptcha), old.getBoolean(getKey(R.string.pref_showcaptcha), false)); - e.putBoolean(getKey(R.string.pref_maptrail), old.getInt(getKey(R.string.pref_maptrail), 1) != 0); - e.putInt(getKey(R.string.pref_lastmapzoom), old.getInt(getKey(R.string.pref_lastmapzoom), 14)); - e.putBoolean(getKey(R.string.pref_livelist), 0 != old.getInt(getKey(R.string.pref_livelist), 1)); - e.putBoolean(getKey(R.string.pref_units), old.getInt(getKey(R.string.pref_units), unitsMetric) == unitsMetric); - e.putBoolean(getKey(R.string.pref_skin), old.getInt(getKey(R.string.pref_skin), 0) != 0); - e.putInt(getKey(R.string.pref_lastusedlist), old.getInt(getKey(R.string.pref_lastusedlist), StoredList.STANDARD_LIST_ID)); - e.putString(getKey(R.string.pref_cachetype), old.getString(getKey(R.string.pref_cachetype), CacheType.ALL.id)); - e.putString(getKey(R.string.pref_twitter_token_secret), old.getString(getKey(R.string.pref_twitter_token_secret), null)); - e.putString(getKey(R.string.pref_twitter_token_public), old.getString(getKey(R.string.pref_twitter_token_public), null)); - e.putInt(getKey(R.string.pref_version), old.getInt(getKey(R.string.pref_version), 0)); - e.putBoolean(getKey(R.string.pref_autoloaddesc), 0 != old.getInt(getKey(R.string.pref_autoloaddesc), 1)); - e.putBoolean(getKey(R.string.pref_ratingwanted), old.getBoolean(getKey(R.string.pref_ratingwanted), true)); - e.putBoolean(getKey(R.string.pref_friendlogswanted), old.getBoolean(getKey(R.string.pref_friendlogswanted), true)); - e.putBoolean(getKey(R.string.pref_useenglish), old.getBoolean(getKey(R.string.pref_useenglish), false)); - e.putBoolean(getKey(R.string.pref_usecompass), 0 != old.getInt(getKey(R.string.pref_usecompass), 1)); - e.putBoolean(getKey(R.string.pref_trackautovisit), old.getBoolean(getKey(R.string.pref_trackautovisit), false)); - e.putBoolean(getKey(R.string.pref_sigautoinsert), old.getBoolean(getKey(R.string.pref_sigautoinsert), false)); - e.putBoolean(getKey(R.string.pref_logimages), old.getBoolean(getKey(R.string.pref_logimages), false)); - e.putBoolean(getKey(R.string.pref_excludedisabled), 0 != old.getInt(getKey(R.string.pref_excludedisabled), 0)); - e.putBoolean(getKey(R.string.pref_excludemine), 0 != old.getInt(getKey(R.string.pref_excludemine), 0)); - e.putString(getKey(R.string.pref_mapfile), old.getString(getKey(R.string.pref_mapfile), null)); - e.putString(getKey(R.string.pref_signature), old.getString(getKey(R.string.pref_signature), null)); - e.putString(getKey(R.string.pref_pass_vote), old.getString(getKey(R.string.pref_pass_vote), null)); - e.putString(getKey(R.string.pref_password), old.getString(getKey(R.string.pref_password), null)); - e.putString(getKey(R.string.pref_username), old.getString(getKey(R.string.pref_username), null)); - e.putString(getKey(R.string.pref_memberstatus), old.getString(getKey(R.string.pref_memberstatus), "")); - e.putInt(getKey(R.string.pref_coordinputformat), old.getInt(getKey(R.string.pref_coordinputformat), 0)); - e.putBoolean(getKey(R.string.pref_log_offline), old.getBoolean(getKey(R.string.pref_log_offline), false)); - e.putBoolean(getKey(R.string.pref_choose_list), old.getBoolean(getKey(R.string.pref_choose_list), true)); - e.putBoolean(getKey(R.string.pref_loaddirectionimg), old.getBoolean(getKey(R.string.pref_loaddirectionimg), true)); - e.putString(getKey(R.string.pref_gccustomdate), old.getString(getKey(R.string.pref_gccustomdate), null)); - e.putInt(getKey(R.string.pref_showwaypointsthreshold), old.getInt(getKey(R.string.pref_showwaypointsthreshold), SHOW_WP_THRESHOLD_DEFAULT)); - e.putString(getKey(R.string.pref_cookiestore), old.getString(getKey(R.string.pref_cookiestore), null)); - e.putBoolean(getKey(R.string.pref_opendetailslastpage), old.getBoolean(getKey(R.string.pref_opendetailslastpage), false)); - e.putInt(getKey(R.string.pref_lastdetailspage), old.getInt(getKey(R.string.pref_lastdetailspage), 1)); - e.putInt(getKey(R.string.pref_defaultNavigationTool), old.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id)); - e.putInt(getKey(R.string.pref_defaultNavigationTool2), old.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id)); - e.putInt(getKey(R.string.pref_livemapstrategy), old.getInt(getKey(R.string.pref_livemapstrategy), Strategy.AUTO.id)); - e.putBoolean(getKey(R.string.pref_debug), old.getBoolean(getKey(R.string.pref_debug), false)); - e.putInt(getKey(R.string.pref_livemaphintshowcount), old.getInt(getKey(R.string.pref_livemaphintshowcount), 0)); + e.putString(getKey(R.string.pref_temp_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_temp_twitter_token_secret), null)); + e.putString(getKey(R.string.pref_temp_twitter_token_public), prefsV0.getString(getKey(R.string.pref_temp_twitter_token_public), null)); + e.putBoolean(getKey(R.string.pref_help_shown), prefsV0.getInt(getKey(R.string.pref_help_shown), 0) != 0); + e.putFloat(getKey(R.string.pref_anylongitude), prefsV0.getFloat(getKey(R.string.pref_anylongitude), 0)); + e.putFloat(getKey(R.string.pref_anylatitude), prefsV0.getFloat(getKey(R.string.pref_anylatitude), 0)); + e.putBoolean(getKey(R.string.pref_offlinemaps), 0 != prefsV0.getInt(getKey(R.string.pref_offlinemaps), 1)); + e.putBoolean(getKey(R.string.pref_offlinewpmaps), 0 != prefsV0.getInt(getKey(R.string.pref_offlinewpmaps), 0)); + e.putString(getKey(R.string.pref_webDeviceCode), prefsV0.getString(getKey(R.string.pref_webDeviceCode), null)); + e.putString(getKey(R.string.pref_webDeviceName), prefsV0.getString(getKey(R.string.pref_webDeviceName), null)); + e.putBoolean(getKey(R.string.pref_maplive), prefsV0.getInt(getKey(R.string.pref_maplive), 1) != 0); + e.putInt(getKey(R.string.pref_mapsource), prefsV0.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT)); + e.putBoolean(getKey(R.string.pref_twitter), 0 != prefsV0.getInt(getKey(R.string.pref_twitter), 0)); + e.putBoolean(getKey(R.string.pref_showaddress), 0 != prefsV0.getInt(getKey(R.string.pref_showaddress), 1)); + e.putBoolean(getKey(R.string.pref_showcaptcha), prefsV0.getBoolean(getKey(R.string.pref_showcaptcha), false)); + e.putBoolean(getKey(R.string.pref_maptrail), prefsV0.getInt(getKey(R.string.pref_maptrail), 1) != 0); + e.putInt(getKey(R.string.pref_lastmapzoom), prefsV0.getInt(getKey(R.string.pref_lastmapzoom), 14)); + e.putBoolean(getKey(R.string.pref_livelist), 0 != prefsV0.getInt(getKey(R.string.pref_livelist), 1)); + e.putBoolean(getKey(R.string.pref_units), prefsV0.getInt(getKey(R.string.pref_units), unitsMetric) == unitsMetric); + e.putBoolean(getKey(R.string.pref_skin), prefsV0.getInt(getKey(R.string.pref_skin), 0) != 0); + e.putInt(getKey(R.string.pref_lastusedlist), prefsV0.getInt(getKey(R.string.pref_lastusedlist), StoredList.STANDARD_LIST_ID)); + e.putString(getKey(R.string.pref_cachetype), prefsV0.getString(getKey(R.string.pref_cachetype), CacheType.ALL.id)); + e.putString(getKey(R.string.pref_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_twitter_token_secret), null)); + e.putString(getKey(R.string.pref_twitter_token_public), prefsV0.getString(getKey(R.string.pref_twitter_token_public), null)); + e.putInt(getKey(R.string.pref_version), prefsV0.getInt(getKey(R.string.pref_version), 0)); + e.putBoolean(getKey(R.string.pref_autoloaddesc), 0 != prefsV0.getInt(getKey(R.string.pref_autoloaddesc), 1)); + e.putBoolean(getKey(R.string.pref_ratingwanted), prefsV0.getBoolean(getKey(R.string.pref_ratingwanted), true)); + e.putBoolean(getKey(R.string.pref_friendlogswanted), prefsV0.getBoolean(getKey(R.string.pref_friendlogswanted), true)); + e.putBoolean(getKey(R.string.pref_useenglish), prefsV0.getBoolean(getKey(R.string.pref_useenglish), false)); + e.putBoolean(getKey(R.string.pref_usecompass), 0 != prefsV0.getInt(getKey(R.string.pref_usecompass), 1)); + e.putBoolean(getKey(R.string.pref_trackautovisit), prefsV0.getBoolean(getKey(R.string.pref_trackautovisit), false)); + e.putBoolean(getKey(R.string.pref_sigautoinsert), prefsV0.getBoolean(getKey(R.string.pref_sigautoinsert), false)); + e.putBoolean(getKey(R.string.pref_logimages), prefsV0.getBoolean(getKey(R.string.pref_logimages), false)); + e.putBoolean(getKey(R.string.pref_excludedisabled), 0 != prefsV0.getInt(getKey(R.string.pref_excludedisabled), 0)); + e.putBoolean(getKey(R.string.pref_excludemine), 0 != prefsV0.getInt(getKey(R.string.pref_excludemine), 0)); + e.putString(getKey(R.string.pref_mapfile), prefsV0.getString(getKey(R.string.pref_mapfile), null)); + e.putString(getKey(R.string.pref_signature), prefsV0.getString(getKey(R.string.pref_signature), null)); + e.putString(getKey(R.string.pref_pass_vote), prefsV0.getString(getKey(R.string.pref_pass_vote), null)); + e.putString(getKey(R.string.pref_password), prefsV0.getString(getKey(R.string.pref_password), null)); + e.putString(getKey(R.string.pref_username), prefsV0.getString(getKey(R.string.pref_username), null)); + e.putString(getKey(R.string.pref_memberstatus), prefsV0.getString(getKey(R.string.pref_memberstatus), "")); + e.putInt(getKey(R.string.pref_coordinputformat), prefsV0.getInt(getKey(R.string.pref_coordinputformat), CoordInputFormatEnum.DEFAULT_INT_VALUE)); + e.putBoolean(getKey(R.string.pref_log_offline), prefsV0.getBoolean(getKey(R.string.pref_log_offline), false)); + e.putBoolean(getKey(R.string.pref_choose_list), prefsV0.getBoolean(getKey(R.string.pref_choose_list), true)); + e.putBoolean(getKey(R.string.pref_loaddirectionimg), prefsV0.getBoolean(getKey(R.string.pref_loaddirectionimg), true)); + e.putString(getKey(R.string.pref_gccustomdate), prefsV0.getString(getKey(R.string.pref_gccustomdate), null)); + e.putInt(getKey(R.string.pref_showwaypointsthreshold), prefsV0.getInt(getKey(R.string.pref_showwaypointsthreshold), SHOW_WP_THRESHOLD_DEFAULT)); + e.putString(getKey(R.string.pref_cookiestore), prefsV0.getString(getKey(R.string.pref_cookiestore), null)); + e.putBoolean(getKey(R.string.pref_opendetailslastpage), prefsV0.getBoolean(getKey(R.string.pref_opendetailslastpage), false)); + e.putInt(getKey(R.string.pref_lastdetailspage), prefsV0.getInt(getKey(R.string.pref_lastdetailspage), 1)); + e.putInt(getKey(R.string.pref_defaultNavigationTool), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id)); + e.putInt(getKey(R.string.pref_defaultNavigationTool2), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id)); + e.putInt(getKey(R.string.pref_livemapstrategy), prefsV0.getInt(getKey(R.string.pref_livemapstrategy), Strategy.AUTO.id)); + e.putBoolean(getKey(R.string.pref_debug), prefsV0.getBoolean(getKey(R.string.pref_debug), false)); + e.putInt(getKey(R.string.pref_livemaphintshowcount), prefsV0.getInt(getKey(R.string.pref_livemaphintshowcount), 0)); e.putInt(getKey(R.string.pref_settingsversion), 1); // mark migrated e.commit(); } // changes for new settings dialog - if (oldVersion < 2) { + if (currentVersion < 2) { final Editor e = sharedPrefs.edit(); e.putBoolean(getKey(R.string.pref_units), !isUseImperialUnits()); @@ -398,7 +427,7 @@ public class Settings { } public static boolean useLowPowerMode() { - return useGooglePlayServices() && getBoolean(R.string.pref_lowpowermode, false); + return getBoolean(R.string.pref_lowpowermode, false); } /** @@ -473,7 +502,7 @@ public class Settings { } public static CoordInputFormatEnum getCoordInputFormat() { - return CoordInputFormatEnum.fromInt(getInt(R.string.pref_coordinputformat, CoordInputFormatEnum.Min.ordinal())); + return CoordInputFormatEnum.fromInt(getInt(R.string.pref_coordinputformat, CoordInputFormatEnum.DEFAULT_INT_VALUE)); } public static void setCoordInputFormat(final CoordInputFormatEnum format) { @@ -1033,4 +1062,12 @@ public class Settings { history.add(0, geocode); putString(R.string.pref_caches_history, StringUtils.join(history, HISTORY_SEPARATOR)); } + + public static boolean useHardwareAcceleration() { + return getBoolean(R.string.pref_hardware_acceleration, !HW_ACCEL_DISABLED_BY_DEFAULT); + } + + public static boolean setUseHardwareAcceleration(final boolean useHardwareAcceleration) { + return putBoolean(R.string.pref_hardware_acceleration, useHardwareAcceleration); + } } diff --git a/main/src/cgeo/geocaching/settings/SettingsActivity.java b/main/src/cgeo/geocaching/settings/SettingsActivity.java index 5297857..b5d7b68 100644 --- a/main/src/cgeo/geocaching/settings/SettingsActivity.java +++ b/main/src/cgeo/geocaching/settings/SettingsActivity.java @@ -21,6 +21,7 @@ import org.openintents.intents.FileManagerIntents; import android.app.ProgressDialog; import android.app.backup.BackupManager; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -87,6 +88,7 @@ public class SettingsActivity extends PreferenceActivity { setTheme(Settings.isLightSkin() && Build.VERSION.SDK_INT > 10 ? R.style.settings_light : R.style.settings); super.onCreate(savedInstanceState); + initHardwareAccelerationPreferences(); SettingsActivity.addPreferencesFromResource(this, R.xml.preferences); initPreferences(); @@ -122,7 +124,7 @@ public class SettingsActivity extends PreferenceActivity { initDefaultNavigationPreferences(); initBackupButtons(); initDbLocationPreference(); - initGeolocationPreference(); + initGeoDirPreferences(); initDebugPreference(); initBasicMemberPreferences(); initSend2CgeoPreferences(); @@ -185,7 +187,12 @@ public class SettingsActivity extends PreferenceActivity { preference.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference preference) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://" + host))); + try { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://" + host))); + } catch (final ActivityNotFoundException e) { + Log.e("Cannot find suitable activity", e); + ActivityMixin.showToast(SettingsActivity.this, R.string.err_application_no); + } return true; } }); @@ -374,6 +381,12 @@ public class SettingsActivity extends PreferenceActivity { }); } + public static void initHardwareAccelerationPreferences() { + // We have to ensure that the preference is initialized so that devices with hardware acceleration disabled + // get the appropriate value. + Settings.setUseHardwareAcceleration(Settings.useHardwareAcceleration()); + } + private void initDbLocationPreference() { final Preference p = getPreference(R.string.pref_dbonsdcard); p.setPersistent(false); @@ -400,27 +413,26 @@ public class SettingsActivity extends PreferenceActivity { }); } - private void initGeolocationPreference() { - final Preference p = getPreference(R.string.pref_googleplayservices); - final Preference p2 = getPreference(R.string.pref_lowpowermode); - p.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + private void initGeoDirPreferences() { + final Preference playServices = getPreference(R.string.pref_googleplayservices); + playServices.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(final Preference preference, final Object newValue) { - final boolean useGooglePlayServices = (Boolean) newValue; - p2.setEnabled(useGooglePlayServices); - CgeoApplication.getInstance().setupGeoDataObservables(useGooglePlayServices, Settings.useLowPowerMode()); + CgeoApplication.getInstance().setupGeoDataObservables((Boolean) newValue, Settings.useLowPowerMode()); return true; } }); - p2.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + playServices.setEnabled(CgeoApplication.getInstance().isGooglePlayServicesAvailable()); + getPreference(R.string.pref_lowpowermode).setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(final Preference preference, final Object newValue) { - CgeoApplication.getInstance().setupGeoDataObservables(Settings.useGooglePlayServices(), (Boolean) newValue); + final CgeoApplication app = CgeoApplication.getInstance(); + final Boolean useLowPower = (Boolean) newValue; + app.setupGeoDataObservables(Settings.useGooglePlayServices(), useLowPower); + app.setupDirectionObservable(useLowPower); return true; } }); - p.setEnabled(CgeoApplication.getInstance().isGooglePlayServicesAvailable()); - p2.setEnabled(Settings.useGooglePlayServices()); } void initBasicMemberPreferences() { diff --git a/main/src/cgeo/geocaching/sorting/DistanceComparator.java b/main/src/cgeo/geocaching/sorting/DistanceComparator.java index 541ce48..6812865 100644 --- a/main/src/cgeo/geocaching/sorting/DistanceComparator.java +++ b/main/src/cgeo/geocaching/sorting/DistanceComparator.java @@ -3,6 +3,7 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; import cgeo.geocaching.geopoint.Geopoint; +import java.util.ArrayList; import java.util.List; /** @@ -15,9 +16,10 @@ public class DistanceComparator extends AbstractCacheComparator { final private List<Geocache> list; private boolean cachedDistances; - public DistanceComparator(final Geopoint coords, List<Geocache> list) { + public DistanceComparator(final Geopoint coords, final List<Geocache> list) { this.coords = coords; - this.list = list; + // create new list so we can iterate over the list in parallel with the cache list adapter + this.list = new ArrayList<>(list); } /** diff --git a/main/src/cgeo/geocaching/speech/SpeechService.java b/main/src/cgeo/geocaching/speech/SpeechService.java index fbd2d7e..11e10c1 100644 --- a/main/src/cgeo/geocaching/speech/SpeechService.java +++ b/main/src/cgeo/geocaching/speech/SpeechService.java @@ -1,5 +1,6 @@ package cgeo.geocaching.speech; +import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.geopoint.Geopoint; @@ -31,7 +32,6 @@ public class SpeechService extends Service implements OnInitListener { private static final int SPEECH_MINPAUSE_SECONDS = 5; private static final int SPEECH_MAXPAUSE_SECONDS = 30; - private static final String EXTRA_TARGET_COORDS = "target"; private static Activity startingActivity; private static boolean isRunning = false; /** @@ -152,7 +152,7 @@ public class SpeechService extends Service implements OnInitListener { @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { if (intent != null) { - target = intent.getParcelableExtra(EXTRA_TARGET_COORDS); + target = intent.getParcelableExtra(Intents.EXTRA_COORDS); } return START_NOT_STICKY; // service can be stopped by system, if under memory pressure } @@ -168,7 +168,7 @@ public class SpeechService extends Service implements OnInitListener { isRunning = true; startingActivity = activity; final Intent talkingService = new Intent(activity, SpeechService.class); - talkingService.putExtra(EXTRA_TARGET_COORDS, dstCoords); + talkingService.putExtra(Intents.EXTRA_COORDS, dstCoords); activity.startService(talkingService); } diff --git a/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java b/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java index 40cd726..d55d9c9 100644 --- a/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java +++ b/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java @@ -102,11 +102,14 @@ public final class CacheDetailsCreator { public void addCacheState(final Geocache cache) { if (cache.isLogOffline() || cache.isArchived() || cache.isDisabled() || cache.isPremiumMembersOnly() || cache.isFound()) { final List<String> states = new ArrayList<>(5); + String date = getVisitedDate(cache); if (cache.isLogOffline()) { - states.add(res.getString(R.string.cache_status_offline_log)); + states.add(res.getString(R.string.cache_status_offline_log) + date); + // reset the found date, to avoid showing it twice + date = ""; } if (cache.isFound()) { - states.add(res.getString(R.string.cache_status_found)); + states.add(res.getString(R.string.cache_status_found) + date); } if (cache.isArchived()) { states.add(res.getString(R.string.cache_status_archived)); @@ -121,12 +124,17 @@ public final class CacheDetailsCreator { } } + private static String getVisitedDate(final Geocache cache) { + final long visited = cache.getVisitedDate(); + return visited != 0 ? " (" + Formatter.formatShortDate(visited) + ")" : ""; + } + public void addRating(final Geocache cache) { if (cache.getRating() > 0) { final RelativeLayout itemLayout = addStars(R.string.cache_rating, cache.getRating()); if (cache.getVotes() > 0) { final TextView itemAddition = ButterKnife.findById(itemLayout, R.id.addition); - itemAddition.setText("(" + cache.getVotes() + ")"); + itemAddition.setText(" (" + cache.getVotes() + ')'); itemAddition.setVisibility(View.VISIBLE); } } @@ -170,7 +178,7 @@ public final class CacheDetailsCreator { } public void addDistance(final Waypoint wpt, final TextView waypointDistanceView) { - Float distance = CgeoApplication.getInstance().distanceNonBlocking(wpt); + final Float distance = CgeoApplication.getInstance().distanceNonBlocking(wpt); String text = "--"; if (distance != null) { text = Units.getDistanceFromKilometers(distance); diff --git a/main/src/cgeo/geocaching/ui/CacheListAdapter.java b/main/src/cgeo/geocaching/ui/CacheListAdapter.java index b879e54..b0b30aa 100644 --- a/main/src/cgeo/geocaching/ui/CacheListAdapter.java +++ b/main/src/cgeo/geocaching/ui/CacheListAdapter.java @@ -120,9 +120,7 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { public CacheListAdapter(final Activity activity, final List<Geocache> list, final CacheListType cacheListType) { super(activity, 0, list); final IGeoData currentGeo = CgeoApplication.getInstance().currentGeo(); - if (currentGeo != null) { - coords = currentGeo.getCoords(); - } + coords = currentGeo.getCoords(); this.res = activity.getResources(); this.list = list; this.cacheListType = cacheListType; diff --git a/main/src/cgeo/geocaching/ui/CompassView.java b/main/src/cgeo/geocaching/ui/CompassView.java index 240afcf..a227770 100644 --- a/main/src/cgeo/geocaching/ui/CompassView.java +++ b/main/src/cgeo/geocaching/ui/CompassView.java @@ -81,8 +81,9 @@ public class CompassView extends View { } public void updateGraphics() { - final float newAzimuthShown = smoothUpdate(northMeasured, azimuthShown); - final float newCacheHeadingShown = smoothUpdate(cacheHeadingMeasured, cacheHeadingShown); + final float newAzimuthShown = initialDisplay ? northMeasured : smoothUpdate(northMeasured, azimuthShown); + final float newCacheHeadingShown = initialDisplay ? cacheHeadingMeasured : smoothUpdate(cacheHeadingMeasured, cacheHeadingShown); + initialDisplay = false; if (Math.abs(AngleUtils.difference(azimuthShown, newAzimuthShown)) >= 2 || Math.abs(AngleUtils.difference(cacheHeadingShown, newCacheHeadingShown)) >= 2) { azimuthShown = newAzimuthShown; @@ -151,17 +152,6 @@ public class CompassView extends View { * @param cacheHeading the cache direction (extra rotation of the needle) */ public void updateNorth(final float northHeading, final float cacheHeading) { - if (initialDisplay) { - // We will force the compass to move brutally if this is the first - // update since it is visible. - azimuthShown = northHeading; - cacheHeadingShown = cacheHeading; - - // it may take some time to get an initial direction measurement for the device - if (northHeading != 0.0) { - initialDisplay = false; - } - } northMeasured = northHeading; cacheHeadingMeasured = cacheHeading; } diff --git a/main/src/cgeo/geocaching/ui/EditNoteDialog.java b/main/src/cgeo/geocaching/ui/EditNoteDialog.java index 013fdff..00d1604 100644 --- a/main/src/cgeo/geocaching/ui/EditNoteDialog.java +++ b/main/src/cgeo/geocaching/ui/EditNoteDialog.java @@ -31,15 +31,19 @@ public class EditNoteDialog extends DialogFragment { public static final String ARGUMENT_INITIAL_NOTE = "initialNote"; private EditText mEditText; - private EditNoteDialogListener listener; - public static EditNoteDialog newInstance(final String initialNote, final EditNoteDialogListener listener) { + /** + * Create a new dialog to edit a note. + * <em>This fragment must be inserted into an activity implementing the EditNoteDialogListener interface.</em> + * + * @param initialNote the initial note to insert in the edit dialog + */ + public static EditNoteDialog newInstance(final String initialNote) { final EditNoteDialog dialog = new EditNoteDialog(); final Bundle arguments = new Bundle(); arguments.putString(EditNoteDialog.ARGUMENT_INITIAL_NOTE, initialNote); dialog.setArguments(arguments); - dialog.listener = listener; return dialog; } @@ -70,7 +74,7 @@ public class EditNoteDialog extends DialogFragment { new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int whichButton) { - listener.onFinishEditNoteDialog(mEditText.getText().toString()); + ((EditNoteDialogListener) getActivity()).onFinishEditNoteDialog(mEditText.getText().toString()); dialog.dismiss(); } }); diff --git a/main/src/cgeo/geocaching/ui/dialog/Dialogs.java b/main/src/cgeo/geocaching/ui/dialog/Dialogs.java index 21e1a82..47ce6e1 100644 --- a/main/src/cgeo/geocaching/ui/dialog/Dialogs.java +++ b/main/src/cgeo/geocaching/ui/dialog/Dialogs.java @@ -25,8 +25,15 @@ import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.ArrayAdapter; import android.widget.EditText; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.List; /** * Wrapper for {@link AlertDialog}. If you want to show a simple text, use one of the @@ -410,4 +417,44 @@ public final class Dialogs { private static void enableDialogButtonIfNotEmpty(final AlertDialog dialog, final String input) { dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(StringUtils.isNotBlank(input)); } + + public static interface ItemWithIcon { + /** + * @return the drawable + */ + int getIcon(); + } + + public static <T extends ItemWithIcon> void select(final Activity activity, final String title, final List<T> items, final Action1<T> listener) { + final ListAdapter adapter = new ArrayAdapter<T>( + activity, + android.R.layout.select_dialog_item, + android.R.id.text1, + items) { + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + // standard list entry + final View v = super.getView(position, convertView, parent); + + // add image + final TextView tv = (TextView) v.findViewById(android.R.id.text1); + tv.setCompoundDrawablesWithIntrinsicBounds(items.get(position).getIcon(), 0, 0, 0); + + // Add margin between image and text + final int dp5 = (int) (5 * activity.getResources().getDisplayMetrics().density + 0.5f); + tv.setCompoundDrawablePadding(dp5); + + return v; + } + }; + + new AlertDialog.Builder(activity) + .setTitle(title) + .setAdapter(adapter, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int item) { + listener.call(items.get(item)); + } + }).show(); + } } diff --git a/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java b/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java index ecb6469..24c8871 100644 --- a/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java +++ b/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java @@ -1,7 +1,6 @@ package cgeo.geocaching.ui.logs; import cgeo.geocaching.CacheDetailActivity; -import cgeo.geocaching.DataStore; import cgeo.geocaching.LogEntry; import cgeo.geocaching.Trackable; import cgeo.geocaching.TrackableActivity; @@ -50,16 +49,14 @@ public class TrackableLogsViewCreator extends LogsViewCreator { holder.countOrLocation.setVisibility(View.GONE); } else { holder.countOrLocation.setText(Html.fromHtml(log.cacheName)); - final String cacheCode = DataStore.getGeocodeForGuid(log.cacheGuid); - if (cacheCode != null) { - final String cacheName = log.cacheName; - holder.countOrLocation.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View arg0) { - CacheDetailActivity.startActivity(activity, cacheCode, Html.fromHtml(cacheName).toString()); - } - }); - } + final String cacheGuid = log.cacheGuid; + final String cacheName = log.cacheName; + holder.countOrLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View arg0) { + CacheDetailActivity.startActivityGuid(activity, cacheGuid, Html.fromHtml(cacheName).toString()); + } + }); } } diff --git a/main/src/cgeo/geocaching/utils/AngleUtils.java b/main/src/cgeo/geocaching/utils/AngleUtils.java index fdd9a9d..5ab2c75 100644 --- a/main/src/cgeo/geocaching/utils/AngleUtils.java +++ b/main/src/cgeo/geocaching/utils/AngleUtils.java @@ -1,7 +1,17 @@ package cgeo.geocaching.utils; +import cgeo.geocaching.CgeoApplication; + +import android.content.Context; +import android.view.Surface; +import android.view.WindowManager; + public final class AngleUtils { + private static class WindowManagerHolder { + public static final WindowManager WINDOW_MANAGER = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); + } + private AngleUtils() { // Do not instantiate } @@ -27,4 +37,37 @@ public final class AngleUtils { public static float normalize(final float angle) { return (angle >= 0 ? angle : (360 - ((-angle) % 360))) % 360; } + + public static int getRotationOffset() { + switch (WindowManagerHolder.WINDOW_MANAGER.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + default: + return 0; + } + } + + /** + * Take the phone rotation (through a given activity) in account and adjust the direction. + * + * @param direction the unadjusted direction in degrees, in the [0, 360[ range + * @return the adjusted direction in degrees, in the [0, 360[ range + */ + public static float getDirectionNow(final float direction) { + return normalize(direction + getRotationOffset()); + } + + /** + * Reverse the phone rotation (through a given activity) in account and adjust the direction. + * + * @param direction the unadjusted direction in degrees, in the [0, 360[ range + * @return the adjusted direction in degrees, in the [0, 360[ range + */ + public static float reverseDirectionNow(final float direction) { + return normalize(direction - getRotationOffset()); + } } diff --git a/main/src/cgeo/geocaching/utils/Formatter.java b/main/src/cgeo/geocaching/utils/Formatter.java index 3068cd4..1b774f8 100644 --- a/main/src/cgeo/geocaching/utils/Formatter.java +++ b/main/src/cgeo/geocaching/utils/Formatter.java @@ -33,7 +33,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatTime(long date) { + public static String formatTime(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME); } @@ -45,7 +45,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatDate(long date) { + public static String formatDate(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE); } @@ -58,7 +58,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatFullDate(long date) { + public static String formatFullDate(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR); } @@ -71,8 +71,8 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatShortDate(long date) { - DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context); + public static String formatShortDate(final long date) { + final DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context); return dateFormat.format(date); } @@ -84,8 +84,8 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatShortDateVerbally(long date) { - int diff = cgeo.geocaching.utils.DateUtils.daysSince(date); + public static String formatShortDateVerbally(final long date) { + final int diff = cgeo.geocaching.utils.DateUtils.daysSince(date); switch (diff) { case 0: return CgeoApplication.getInstance().getString(R.string.log_today); @@ -104,7 +104,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatShortDateTime(long date) { + public static String formatShortDateTime(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL); } @@ -116,11 +116,11 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatDateTime(long date) { + public static String formatDateTime(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); } - public static String formatCacheInfoLong(Geocache cache, CacheListType cacheListType) { + public static String formatCacheInfoLong(final Geocache cache, final CacheListType cacheListType) { final ArrayList<String> infos = new ArrayList<>(); if (StringUtils.isNotBlank(cache.getGeocode())) { infos.add(cache.getGeocode()); @@ -137,13 +137,13 @@ public abstract class Formatter { return StringUtils.join(infos, Formatter.SEPARATOR); } - public static String formatCacheInfoShort(Geocache cache) { + public static String formatCacheInfoShort(final Geocache cache) { final ArrayList<String> infos = new ArrayList<>(); addShortInfos(cache, infos); return StringUtils.join(infos, Formatter.SEPARATOR); } - private static void addShortInfos(Geocache cache, final ArrayList<String> infos) { + private static void addShortInfos(final Geocache cache, final ArrayList<String> infos) { if (cache.hasDifficulty()) { infos.add("D " + String.format("%.1f", cache.getDifficulty())); } @@ -162,7 +162,7 @@ public abstract class Formatter { } } - public static String formatCacheInfoHistory(Geocache cache) { + public static String formatCacheInfoHistory(final Geocache cache) { final ArrayList<String> infos = new ArrayList<>(3); infos.add(StringUtils.upperCase(cache.getGeocode())); infos.add(Formatter.formatDate(cache.getVisitedDate())); @@ -170,9 +170,9 @@ public abstract class Formatter { return StringUtils.join(infos, Formatter.SEPARATOR); } - public static String formatWaypointInfo(Waypoint waypoint) { + public static String formatWaypointInfo(final Waypoint waypoint) { final List<String> infos = new ArrayList<>(3); - WaypointType waypointType = waypoint.getWaypointType(); + final WaypointType waypointType = waypoint.getWaypointType(); if (waypointType != WaypointType.OWN && waypointType != null) { infos.add(waypointType.getL10n()); } @@ -188,4 +188,16 @@ public abstract class Formatter { } return StringUtils.join(infos, Formatter.SEPARATOR); } + + public static String formatDaysAgo(final long date) { + final int days = cgeo.geocaching.utils.DateUtils.daysSince(date); + switch (days) { + case 0: + return CgeoApplication.getInstance().getString(R.string.log_today); + case 1: + return CgeoApplication.getInstance().getString(R.string.log_yesterday); + default: + return CgeoApplication.getInstance().getResources().getQuantityString(R.plurals.days_ago, days, days); + } + } } diff --git a/main/src/cgeo/geocaching/utils/HtmlUtils.java b/main/src/cgeo/geocaching/utils/HtmlUtils.java index 51c4d6e..e90b70d 100644 --- a/main/src/cgeo/geocaching/utils/HtmlUtils.java +++ b/main/src/cgeo/geocaching/utils/HtmlUtils.java @@ -24,7 +24,7 @@ public final class HtmlUtils { * @param html * @return */ - public static String extractText(CharSequence html) { + public static String extractText(final CharSequence html) { if (StringUtils.isBlank(html)) { return StringUtils.EMPTY; } @@ -32,13 +32,13 @@ public final class HtmlUtils { // recognize images in textview HTML contents if (html instanceof Spanned) { - Spanned text = (Spanned) html; - Object[] styles = text.getSpans(0, text.length(), Object.class); - ArrayList<Pair<Integer, Integer>> removals = new ArrayList<>(); - for (Object style : styles) { + final Spanned text = (Spanned) html; + final Object[] styles = text.getSpans(0, text.length(), Object.class); + final ArrayList<Pair<Integer, Integer>> removals = new ArrayList<>(); + for (final Object style : styles) { if (style instanceof ImageSpan) { - int start = text.getSpanStart(style); - int end = text.getSpanEnd(style); + final int start = text.getSpanStart(style); + final int end = text.getSpanEnd(style); removals.add(Pair.of(start, end)); } } @@ -47,12 +47,12 @@ public final class HtmlUtils { Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() { @Override - public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) { + public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) { return rhs.getRight().compareTo(lhs.getRight()); } }); result = text.toString(); - for (Pair<Integer, Integer> removal : removals) { + for (final Pair<Integer, Integer> removal : removals) { result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight()); } } @@ -60,4 +60,14 @@ public final class HtmlUtils { // now that images are gone, do a normal html to text conversion return Html.fromHtml(result).toString().trim(); } + + public static String removeExtraParagraph(final String html) { + if (StringUtils.startsWith(html, "<p>") && StringUtils.endsWith(html, "</p>")) { + final String paragraph = StringUtils.substring(html, "<p>".length(), html.length() - "</p>".length()).trim(); + if (extractText(paragraph).equals(paragraph)) { + return paragraph; + } + } + return html; + } } diff --git a/main/src/cgeo/geocaching/utils/ImageUtils.java b/main/src/cgeo/geocaching/utils/ImageUtils.java index d2edfc6..c2b7327 100644 --- a/main/src/cgeo/geocaching/utils/ImageUtils.java +++ b/main/src/cgeo/geocaching/utils/ImageUtils.java @@ -1,6 +1,7 @@ package cgeo.geocaching.utils; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.Image; import cgeo.geocaching.R; import cgeo.geocaching.compatibility.Compatibility; @@ -25,6 +26,8 @@ import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.net.Uri; import android.os.Environment; +import android.text.Html; +import android.text.Html.ImageGetter; import android.util.Base64; import android.util.Base64InputStream; import android.widget.TextView; @@ -36,8 +39,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; +import java.util.Collection; import java.util.Date; +import java.util.LinkedHashSet; import java.util.Locale; +import java.util.Set; public final class ImageUtils { private static final int[] ORIENTATIONS = new int[] { @@ -49,6 +55,10 @@ public final class ImageUtils { private static final int[] ROTATION = new int[] { 90, 180, 270 }; private static final int MAX_DISPLAY_IMAGE_XY = 800; + // Images whose URL contains one of those patterns will not be available on the Images tab + // for opening into an external application. + private final static String[] NO_EXTERNAL = new String[] { "geocheck.org" }; + private ImageUtils() { // Do not let this class be instantiated, this is a utility class. } @@ -303,11 +313,35 @@ public final class ImageUtils { } /** + * Add images present in the HTML description to the existing collection. + * + * @param images a collection of images + * @param htmlText the HTML description to be parsed + * @param geocode the common title for images in the description + */ + public static void addImagesFromHtml(final Collection<Image> images, final String htmlText, final String geocode) { + final Set<String> urls = new LinkedHashSet<>(); + for (final Image image : images) { + urls.add(image.getUrl()); + } + Html.fromHtml(StringUtils.defaultString(htmlText), new ImageGetter() { + @Override + public Drawable getDrawable(final String source) { + if (!urls.contains(source) && canBeOpenedExternally(source)) { + images.add(new Image(source, StringUtils.defaultString(geocode))); + urls.add(source); + } + return null; + } + }, null); + } + + /** * Container which can hold a drawable (initially an empty one) and get a newer version when it * becomes available. It also invalidates the view the container belongs to, so that it is * redrawn properly. */ - public final static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> { + public static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> { private Drawable drawable; final private TextView view; @@ -324,7 +358,7 @@ public final class ImageUtils { } @Override - public void draw(final Canvas canvas) { + public final void draw(final Canvas canvas) { if (drawable != null) { drawable.draw(canvas); } @@ -337,8 +371,55 @@ public final class ImageUtils { view.setText(view.getText()); } - public void updateFrom(final Observable<? extends Drawable> drawableObservable) { + public final void updateFrom(final Observable<? extends Drawable> drawableObservable) { drawableObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(this); } } + + /** + * Image that automatically scales to fit a line of text in the containing {@link TextView}. + */ + public final static class LineHeightContainerDrawable extends ContainerDrawable { + private final TextView view; + + public LineHeightContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { + super(view, drawableObservable); + this.view = view; + } + + @Override + public void call(final Drawable newDrawable) { + super.call(newDrawable); + setBounds(ImageUtils.scaleImageToLineHeight(newDrawable, view)); + } + } + + public static boolean canBeOpenedExternally(final String source) { + return !containsPattern(source, NO_EXTERNAL); + } + + public static Rect scaleImageToLineHeight(final Drawable drawable, final TextView view) { + final int lineHeight = (int) (view.getLineHeight() * 0.8); + final int width = drawable.getIntrinsicWidth() * lineHeight / drawable.getIntrinsicHeight(); + return new Rect(0, 0, width, lineHeight); + } + + public static Bitmap convertToBitmap(final Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + // handle solid colors, which have no width + int width = drawable.getIntrinsicWidth(); + width = width > 0 ? width : 1; + int height = drawable.getIntrinsicHeight(); + height = height > 0 ? height : 1; + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } } diff --git a/main/src/cgeo/geocaching/utils/RxUtils.java b/main/src/cgeo/geocaching/utils/RxUtils.java index 3af98cc..ef79f93 100644 --- a/main/src/cgeo/geocaching/utils/RxUtils.java +++ b/main/src/cgeo/geocaching/utils/RxUtils.java @@ -2,17 +2,21 @@ package cgeo.geocaching.utils; import rx.Observable; import rx.Observable.OnSubscribe; +import rx.Observable.Operator; import rx.Scheduler; import rx.Scheduler.Worker; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action0; +import rx.functions.Func1; +import rx.internal.operators.OperatorTakeWhile; import rx.observables.BlockingObservable; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -21,8 +25,9 @@ import java.util.concurrent.atomic.AtomicInteger; public class RxUtils { - // Utility class, not to be instanciated - private RxUtils() {} + private RxUtils() { + // Utility class, not to be instantiated + } public final static Scheduler computationScheduler = Schedulers.computation(); @@ -30,10 +35,13 @@ public class RxUtils { private static final HandlerThread looperCallbacksThread = new HandlerThread("Looper callbacks thread", android.os.Process.THREAD_PRIORITY_BACKGROUND); + static { looperCallbacksThread.start(); } - public static final Scheduler looperCallbacksScheduler = AndroidSchedulers.handlerThread(new Handler(looperCallbacksThread.getLooper())); + + public static final Looper looperCallbacksLooper = looperCallbacksThread.getLooper(); + public static final Scheduler looperCallbacksScheduler = AndroidSchedulers.handlerThread(new Handler(looperCallbacksLooper)); public static final Worker looperCallbacksWorker = looperCallbacksScheduler.createWorker(); public static <T> void waitForCompletion(final BlockingObservable<T> observable) { @@ -47,7 +55,8 @@ public class RxUtils { /** * Subscribe function whose subscription and unsubscription take place on a looper thread. * - * @param <T> the type of the observable + * @param <T> + * the type of the observable */ public static abstract class LooperCallbacks<T> implements OnSubscribe<T> { @@ -92,6 +101,23 @@ public class RxUtils { } abstract protected void onStart(); + abstract protected void onStop(); } + + public static <T> Operator<T, T> operatorTakeUntil(final Func1<? super T, Boolean> predicate) { + return new OperatorTakeWhile<>(new Func1<T, Boolean>() { + private boolean quitting = false; + + @Override + public Boolean call(final T item) { + if (quitting) { + return false; + } + quitting |= predicate.call(item); + return true; + } + }); + } + } |
