diff options
Diffstat (limited to 'main/src/cgeo/geocaching')
398 files changed, 10369 insertions, 8457 deletions
diff --git a/main/src/cgeo/geocaching/AboutActivity.java b/main/src/cgeo/geocaching/AboutActivity.java index f6d204b..e5a98c2 100644 --- a/main/src/cgeo/geocaching/AboutActivity.java +++ b/main/src/cgeo/geocaching/AboutActivity.java @@ -3,9 +3,20 @@ package cgeo.geocaching; import butterknife.ButterKnife; import butterknife.InjectView; +import cgeo.calendar.CalendarAddon; +import cgeo.contacts.ContactsAddon; import cgeo.geocaching.activity.AbstractViewPagerActivity; +import cgeo.geocaching.compatibility.Compatibility; +import cgeo.geocaching.connector.ConnectorFactory; +import cgeo.geocaching.connector.capability.ILogin; +import cgeo.geocaching.sensors.OrientationProvider; +import cgeo.geocaching.sensors.RotationProvider; +import cgeo.geocaching.sensors.Sensors; +import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.AbstractCachingPageViewCreator; import cgeo.geocaching.ui.AnchorAwareLinkMovementMethod; +import cgeo.geocaching.utils.ClipboardUtils; +import cgeo.geocaching.utils.ProcessUtils; import cgeo.geocaching.utils.Version; import org.apache.commons.io.IOUtils; @@ -17,15 +28,20 @@ import org.apache.commons.lang3.tuple.Pair; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION; import android.os.Bundle; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ScrollView; import android.widget.TextView; import java.io.InputStream; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Scanner; public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> { @@ -65,6 +81,7 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> @InjectView(R.id.changelog_master) protected TextView changeLogMaster; @InjectView(R.id.changelog_release) protected TextView changeLogRelease; + @InjectView(R.id.changelog_github) protected TextView changeLogLink; @Override public ScrollView getDispatchedView(final ViewGroup parentView) { @@ -77,11 +94,42 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> } else { changeLogMaster.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); } + changeLogLink.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(final View v) { + startUrl("https://github.com/cgeo/cgeo/releases"); + } + }); return view; } } + class SystemViewCreator extends AbstractCachingPageViewCreator<ScrollView> { + + @InjectView(R.id.system) protected TextView system; + @InjectView(R.id.copy) protected Button copy; + + @Override + public ScrollView getDispatchedView(final ViewGroup parentView) { + final ScrollView view = (ScrollView) getLayoutInflater().inflate(R.layout.about_system_page, parentView, false); + ButterKnife.inject(this, view); + final String systemInfo = systemInformation(AboutActivity.this); + system.setText(systemInfo); + system.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); + Compatibility.setTextIsSelectable(system, true); + copy.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View view) { + ClipboardUtils.copyToClipboard(systemInfo); + showShortToast(getString(R.string.clipboard_copy_ok)); + } + }); + return view; + } + } + class HelpViewCreator extends AbstractCachingPageViewCreator<ScrollView> { @InjectView(R.id.support) protected TextView support; @@ -95,7 +143,8 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> public ScrollView getDispatchedView(final ViewGroup parentView) { final ScrollView view = (ScrollView) getLayoutInflater().inflate(R.layout.about_help_page, parentView, false); ButterKnife.inject(this, view); - setClickListener(support, "mailto:support@cgeo.org?subject=" + Uri.encode("cgeo " + Version.getVersionName(AboutActivity.this))); + setClickListener(support, "mailto:support@cgeo.org?subject=" + Uri.encode("cgeo " + Version.getVersionName(AboutActivity.this)) + + "&body=" + Uri.encode(systemInformation(AboutActivity.this)) + "\n"); setClickListener(website, "http://www.cgeo.org/"); setClickListener(facebook, "http://www.facebook.com/pages/cgeo/297269860090"); setClickListener(twitter, "http://twitter.com/android_gc"); @@ -104,7 +153,7 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> @Override public void onClick(final View v) { - market(); + ProcessUtils.openMarket(AboutActivity.this, getPackageName()); } }); return view; @@ -131,6 +180,7 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> VERSION(R.string.about_version), HELP(R.string.about_help), CHANGELOG(R.string.about_changelog), + SYSTEM(R.string.about_system), CONTRIBUTORS(R.string.about_contributors), LICENSE(R.string.about_license); @@ -168,12 +218,6 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } - final void market() { - final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + getPackageName())); - marketIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - startActivity(marketIntent); - } - @Override protected final cgeo.geocaching.activity.AbstractViewPagerActivity.PageViewCreator createViewCreator(final Page page) { switch (page) { @@ -183,6 +227,8 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> return new HelpViewCreator(); case CHANGELOG: return new ChangeLogViewCreator(); + case SYSTEM: + return new SystemViewCreator(); case CONTRIBUTORS: return new ContributorsViewCreator(); case LICENSE: @@ -224,4 +270,57 @@ public class AboutActivity extends AbstractViewPagerActivity<AboutActivity.Page> fromActivity.startActivity(intent); } + private static String presence(final Boolean present) { + return present ? "present" : "absent"; + } + + private static String systemInformation(final Context context) { + final boolean googlePlayServicesAvailable = CgeoApplication.getInstance().isGooglePlayServicesAvailable(); + final StringBuilder body = new StringBuilder("--- System information ---") + .append("\nDevice: ").append(Build.MODEL).append(" (").append(Build.PRODUCT).append(", ").append(Build.BRAND).append(")") + .append("\nAndroid version: ").append(VERSION.RELEASE) + .append("\nAndroid build: ").append(Build.DISPLAY) + .append("\nCgeo version: ").append(Version.getVersionName(context)) + .append("\nGoogle Play services: ").append(googlePlayServicesAvailable ? (Settings.useGooglePlayServices() ? "enabled" : "disabled") : "unavailable") + .append("\nLow power mode: ").append(Settings.useLowPowerMode() ? "active" : "inactive") + .append("\nCompass capabilities: ").append(Sensors.getInstance().hasCompassCapabilities() ? "yes" : "no") + .append("\nRotation sensor: ").append(presence(RotationProvider.hasRotationSensor(context))) + .append("\nGeomagnetic rotation sensor: ").append(presence(RotationProvider.hasGeomagneticRotationSensor(context))) + .append("\nOrientation sensor: ").append(presence(OrientationProvider.hasOrientationSensor(context))) + .append("\nHide own/found: ").append(Settings.isExcludeMyCaches()) + .append("\nMap strategy: ").append(Settings.getLiveMapStrategy().toString().toLowerCase(Locale.getDefault())) + .append("\nHW-acceleration: ").append(Settings.useHardwareAcceleration() ? "enabled" : "disabled") + .append(" (").append(Settings.useHardwareAcceleration() != Settings.HW_ACCEL_DISABLED_BY_DEFAULT ? "default state" : "manually changed").append(")"); + final StringBuilder connectors = new StringBuilder(); + int connectorCount = 0; + for (final ILogin connector : ConnectorFactory.getActiveLiveConnectors()) { + connectorCount++; + connectors.append("\n - ").append(connector.getName()).append(": ").append(connector.isLoggedIn() ? "logged in" : "not logged in") + .append(" (").append(connector.getLoginStatusString()).append(')'); + if (connector.getName().equals("geocaching.com") && connector.isLoggedIn()) { + connectors.append(" / ").append(Settings.getGCMemberStatus()); + } + } + body.append("\nGeocaching sites enabled:").append(connectorCount > 0 ? connectors : " none") + .append("\nSystem language: ").append(Locale.getDefault()); + if (Settings.isUseEnglish()) { + body.append(" (cgeo forced to English)"); + } + final boolean calendarAddonAvailable = CalendarAddon.isAvailable(); + final boolean contactsAddonAvailable = ContactsAddon.isAvailable(); + body.append("\nInstalled cgeo plugins:"); + if (calendarAddonAvailable || contactsAddonAvailable) { + if (calendarAddonAvailable) { + body.append(" calendar"); + } + if (contactsAddonAvailable) { + body.append(" contacts"); + } + } else { + body.append(" none"); + } + body.append("\n--- End of system information ---\n"); + return body.toString(); + } + } diff --git a/main/src/cgeo/geocaching/AbstractDialogFragment.java b/main/src/cgeo/geocaching/AbstractDialogFragment.java index 9277d8c..6e58bd3 100644 --- a/main/src/cgeo/geocaching/AbstractDialogFragment.java +++ b/main/src/cgeo/geocaching/AbstractDialogFragment.java @@ -7,10 +7,10 @@ import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.gcvote.GCVote; import cgeo.geocaching.gcvote.GCVoteRating; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.CacheDetailsCreator; import cgeo.geocaching.ui.LoggingUI; @@ -19,7 +19,7 @@ import cgeo.geocaching.utils.RxUtils; import rx.Observable; import rx.Subscription; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action1; import rx.functions.Func0; import rx.subscriptions.Subscriptions; @@ -41,7 +41,6 @@ import android.widget.ImageView; import android.widget.TextView; public abstract class AbstractDialogFragment extends DialogFragment implements CacheMenuHandler.ActivityInterface, PopupMenu.OnMenuItemClickListener, MenuItem.OnMenuItemClickListener { - protected CgeoApplication app = null; protected Resources res = null; protected String geocode; protected CacheDetailsCreator details; @@ -60,7 +59,6 @@ public abstract class AbstractDialogFragment extends DialogFragment implements C public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); res = getResources(); - app = (CgeoApplication) getActivity().getApplication(); setHasOptionsMenu(true); } @@ -189,7 +187,7 @@ public abstract class AbstractDialogFragment extends DialogFragment implements C if (!cache.supportsGCVote()) { return; } - AndroidObservable.bindActivity(getActivity(), Observable.defer(new Func0<Observable<GCVoteRating>>() { + AppObservable.bindActivity(getActivity(), Observable.defer(new Func0<Observable<GCVoteRating>>() { @Override public Observable<GCVoteRating> call() { final GCVoteRating rating = GCVote.getRating(cache.getGuid(), geocode); @@ -256,15 +254,15 @@ public abstract class AbstractDialogFragment extends DialogFragment implements C private final GeoDirHandler geoUpdate = new GeoDirHandler() { @Override - public void updateGeoData(final IGeoData geo) { + public void updateGeoData(final GeoData geo) { try { - if (geo.getCoords() != null && cache != null && cache.getCoords() != null) { + if (cache != null && cache.getCoords() != null) { cacheDistance.setText(Units.getDistanceFromKilometers(geo.getCoords().distanceTo(cache.getCoords()))); cacheDistance.bringToFront(); } onUpdateGeoData(geo); } catch (final RuntimeException e) { - Log.w("Failed to UpdateLocation location."); + Log.w("Failed to update location", e); } } }; @@ -273,7 +271,7 @@ public abstract class AbstractDialogFragment extends DialogFragment implements C * @param geo * location */ - protected void onUpdateGeoData(final IGeoData geo) { + protected void onUpdateGeoData(final GeoData geo) { // do nothing by default } @@ -324,7 +322,7 @@ public abstract class AbstractDialogFragment extends DialogFragment implements C try { CacheMenuHandler.onPrepareOptionsMenu(menu, cache); LoggingUI.onPrepareOptionsMenu(menu, cache); - } catch (final RuntimeException e) { + } catch (final RuntimeException ignored) { // nothing } } @@ -342,7 +340,7 @@ public abstract class AbstractDialogFragment extends DialogFragment implements C showToast(res.getString(R.string.err_location_unknown)); return; } - CacheListActivity.startActivityCoordinates((AbstractActivity) getActivity(), coords); + CacheListActivity.startActivityCoordinates((AbstractActivity) getActivity(), coords, cache != null ? cache.getName() : null); getActivity().finish(); } diff --git a/main/src/cgeo/geocaching/AbstractLoggingActivity.java b/main/src/cgeo/geocaching/AbstractLoggingActivity.java index 8448e45..cc5546f 100644 --- a/main/src/cgeo/geocaching/AbstractLoggingActivity.java +++ b/main/src/cgeo/geocaching/AbstractLoggingActivity.java @@ -2,6 +2,7 @@ package cgeo.geocaching; import cgeo.geocaching.activity.AbstractActionBarActivity; import cgeo.geocaching.activity.ActivityMixin; +import cgeo.geocaching.activity.Keyboard; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.GCSmiliesProvider; @@ -11,6 +12,8 @@ import cgeo.geocaching.utils.LogTemplateProvider; import cgeo.geocaching.utils.LogTemplateProvider.LogContext; import cgeo.geocaching.utils.LogTemplateProvider.LogTemplate; +import org.apache.commons.lang3.StringUtils; + import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; @@ -55,6 +58,10 @@ public abstract class AbstractLoggingActivity extends AbstractActionBarActivity @Override public boolean onOptionsItemSelected(final MenuItem item) { final int id = item.getItemId(); + if (id == R.id.menu_repeat_last) { + replaceLog(getLastLog()); + return true; + } final LogTemplate template = LogTemplateProvider.getTemplate(id); if (template != null) { @@ -71,10 +78,26 @@ public abstract class AbstractLoggingActivity extends AbstractActionBarActivity return super.onOptionsItemSelected(item); } + /** + * @return the last log text used with this logging activity + */ + protected abstract String getLastLog(); + protected abstract LogContext getLogContext(); protected final void insertIntoLog(final String newText, final boolean moveCursor) { final EditText log = (EditText) findViewById(R.id.log); ActivityMixin.insertAtPosition(log, newText, moveCursor); } + + private void replaceLog(final String newText) { + final EditText log = (EditText) findViewById(R.id.log); + log.setText(StringUtils.EMPTY); + insertIntoLog(newText, true); + } + + protected void requestKeyboardForLogging() { + new Keyboard(this).show(findViewById(R.id.log)); + } + } diff --git a/main/src/cgeo/geocaching/AddressListActivity.java b/main/src/cgeo/geocaching/AddressListActivity.java index dc0239f..4ff48f6 100644 --- a/main/src/cgeo/geocaching/AddressListActivity.java +++ b/main/src/cgeo/geocaching/AddressListActivity.java @@ -1,75 +1,55 @@ package cgeo.geocaching; import cgeo.geocaching.activity.AbstractListActivity; +import cgeo.geocaching.location.AndroidGeocoder; +import cgeo.geocaching.location.GCGeocoder; +import cgeo.geocaching.location.MapQuestGeocoder; import cgeo.geocaching.ui.AddressListAdapter; -import cgeo.geocaching.utils.Log; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; +import rx.Observable; +import rx.android.app.AppObservable; +import rx.functions.Action1; import android.app.ProgressDialog; import android.location.Address; -import android.location.Geocoder; -import android.os.AsyncTask; import android.os.Bundle; import java.util.List; -import java.util.Locale; public class AddressListActivity extends AbstractListActivity { @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.addresslist_activity); - // get parameters - final String keyword = getIntent().getStringExtra(Intents.EXTRA_KEYWORD); - - if (keyword == null) { - showToast(res.getString(R.string.err_search_address_forgot)); - finish(); - return; - } - final AddressListAdapter adapter = new AddressListAdapter(this); setListAdapter(adapter); + final String keyword = getIntent().getStringExtra(Intents.EXTRA_KEYWORD); final ProgressDialog waitDialog = ProgressDialog.show(this, res.getString(R.string.search_address_started), keyword, true); waitDialog.setCancelable(true); + lookupAddressInBackground(keyword, adapter, waitDialog); + } - new AsyncTask<Void, Void, List<Address>>() { - + private void lookupAddressInBackground(final String keyword, final AddressListAdapter adapter, final ProgressDialog waitDialog) { + final Observable<Address> geocoderObservable = new AndroidGeocoder(this).getFromLocationName(keyword) + .onErrorResumeNext(MapQuestGeocoder.getFromLocationName(keyword)) + .onErrorResumeNext(GCGeocoder.getFromLocationName(keyword)); + AppObservable.bindActivity(this, geocoderObservable.toList()).subscribe(new Action1<List<Address>>() { @Override - protected List<Address> doInBackground(Void... params) { - final Geocoder geocoder = new Geocoder(AddressListActivity.this, Locale.getDefault()); - try { - return geocoder.getFromLocationName(keyword, 20); - } catch (Exception e) { - // non Google devices come without the geocoder - if (StringUtils.containsIgnoreCase(e.getMessage(), "Service not Available")) { - Log.i("No geocoder available"); - } - else { - Log.e("AddressListActivity.doInBackground", e); - } - return null; + public void call(final List<Address> addresses) { + waitDialog.dismiss(); + for (final Address address : addresses) { + adapter.add(address); // don't use addAll, it's only available with API >= 11 } } - + }, new Action1<Throwable>() { @Override - protected void onPostExecute(final List<Address> addresses) { - waitDialog.dismiss(); - if (CollectionUtils.isNotEmpty(addresses)) { - for (final Address address : addresses) { - adapter.add(address); // don't use addAll, it's only available with API >= 11 - } - } else { - finish(); - CacheListActivity.startActivityAddress(AddressListActivity.this, null, keyword); - } + public void call(final Throwable throwable) { + finish(); + showToast(res.getString(R.string.err_unknown_address)); } - - }.execute(); + }); } }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/AttributesGridAdapter.java b/main/src/cgeo/geocaching/AttributesGridAdapter.java new file mode 100644 index 0000000..fd81339 --- /dev/null +++ b/main/src/cgeo/geocaching/AttributesGridAdapter.java @@ -0,0 +1,79 @@ +package cgeo.geocaching; + +import cgeo.geocaching.enumerations.CacheAttribute; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import java.util.List; + +public class AttributesGridAdapter extends BaseAdapter { + private final Context context; + private final Resources resources; + private final List<String> attributes; + private final LayoutInflater inflater; + + public AttributesGridAdapter(final Context context, final Geocache cache) { + this.context = context; + resources = context.getResources(); + attributes = cache.getAttributes(); + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public int getCount() { + return attributes.size(); + } + + @Override + public Object getItem(final int position) { + return attributes.get(position); + } + + @Override + public long getItemId(final int position) { + return 0; + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + final FrameLayout attributeLayout; + if (convertView == null) { + attributeLayout = (FrameLayout) inflater.inflate(R.layout.attribute_image, parent, false); + } else { + attributeLayout = (FrameLayout) convertView; + } + + drawAttribute(attributeLayout, attributes.get(position)); + return attributeLayout; + } + + private void drawAttribute(final FrameLayout attributeLayout, final String attributeName) { + final ImageView imageView = (ImageView) attributeLayout.getChildAt(0); + + final boolean strikeThrough = !CacheAttribute.isEnabled(attributeName); + final CacheAttribute attrib = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attributeName)); + if (attrib != null) { + Drawable drawable = resources.getDrawable(attrib.drawableId); + imageView.setImageDrawable(drawable); + if (strikeThrough) { + // generate strike through image with same properties as attribute image + final ImageView strikeThroughImage = new ImageView(context); + strikeThroughImage.setLayoutParams(imageView.getLayoutParams()); + drawable = resources.getDrawable(R.drawable.attribute__strikethru); + strikeThroughImage.setImageDrawable(drawable); + attributeLayout.addView(strikeThroughImage); + } + } else { + imageView.setImageDrawable(resources.getDrawable(R.drawable.attribute_unknown)); + } + } + +}
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/CacheCache.java b/main/src/cgeo/geocaching/CacheCache.java index 1913d3c..74e22b5 100644 --- a/main/src/cgeo/geocaching/CacheCache.java +++ b/main/src/cgeo/geocaching/CacheCache.java @@ -3,7 +3,7 @@ package cgeo.geocaching; import cgeo.geocaching.DataStore.StorageLocation; import cgeo.geocaching.connector.gc.Tile; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.utils.LeastRecentlyUsedMap; import cgeo.geocaching.utils.LeastRecentlyUsedMap.RemoveHandler; import cgeo.geocaching.utils.Log; diff --git a/main/src/cgeo/geocaching/CacheDetailActivity.java b/main/src/cgeo/geocaching/CacheDetailActivity.java index 0976b35..4f0c4d2 100644 --- a/main/src/cgeo/geocaching/CacheDetailActivity.java +++ b/main/src/cgeo/geocaching/CacheDetailActivity.java @@ -5,29 +5,31 @@ 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; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; import cgeo.geocaching.apps.cache.navi.NavigationSelectionActionProvider; import cgeo.geocaching.apps.cachelist.MapsWithMeCacheListApp; -import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; +import cgeo.geocaching.connector.capability.IgnoreCapability; 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; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.gcvote.GCVote; +import cgeo.geocaching.gcvote.GCVoteDialog; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.network.AndroidBeam; import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.network.Network; +import cgeo.geocaching.network.SmileyImage; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.AbstractCachingPageViewCreator; import cgeo.geocaching.ui.AnchorAwareLinkMovementMethod; @@ -44,7 +46,7 @@ import cgeo.geocaching.ui.OwnerActionsClickListener; import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.ui.logs.CacheLogsViewCreator; -import cgeo.geocaching.utils.CancellableHandler; +import cgeo.geocaching.utils.CheckerUtils; import cgeo.geocaching.utils.CryptUtils; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.ImageUtils; @@ -66,10 +68,12 @@ import org.eclipse.jdt.annotation.Nullable; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; -import rx.android.observables.AndroidObservable; +import rx.Subscription; +import rx.android.app.AppObservable; import rx.functions.Action0; import rx.functions.Action1; import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; import android.R.color; import android.app.AlertDialog; @@ -84,6 +88,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -97,6 +102,7 @@ import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; +import android.text.util.Linkify; import android.util.TypedValue; import android.view.ContextMenu; import android.view.Menu; @@ -105,16 +111,15 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewParent; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.Button; -import android.widget.FrameLayout; +import android.widget.GridView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; +import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.TextView; import android.widget.TextView.BufferType; @@ -133,7 +138,7 @@ import java.util.regex.Pattern; * e.g. details, description, logs, waypoints, inventory... */ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailActivity.Page> - implements CacheMenuHandler.ActivityInterface, INavigationSource, ActivitySharingInterface, EditNoteDialogListener { + implements CacheMenuHandler.ActivityInterface, INavigationSource, AndroidBeam.ActivitySharingInterface, EditNoteDialogListener { private static final int MESSAGE_FAILED = -1; private static final int MESSAGE_SUCCEEDED = 1; @@ -171,6 +176,9 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc */ private Waypoint selectedWaypoint; + private boolean requireGeodata; + private Subscription geoDataSubscription = Subscriptions.empty(); + @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.cachedetail_activity); @@ -179,12 +187,13 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // get parameters final Bundle extras = getIntent().getExtras(); - final Uri uri = getIntent().getData(); + final Uri uri = AndroidBeam.getUri(getIntent()); // try to get data from extras String name = null; String geocode = null; String guid = null; + if (extras != null) { geocode = extras.getString(Intents.EXTRA_GEOCODE); name = extras.getString(Intents.EXTRA_NAME); @@ -196,6 +205,10 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc geocode = MapsWithMeCacheListApp.getCacheFromMapsWithMe(this, getIntent()); } + if (geocode == null && uri != null) { + geocode = ConnectorFactory.getGeocodeFromURL(uri.toString()); + } + // try to get data from URI if (geocode == null && guid == null && uri != null) { final String uriHost = uri.getHost().toLowerCase(Locale.US); @@ -227,31 +240,6 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return; } } - } else if (uriHost.contains("coord.info")) { - if (StringUtils.startsWith(uriPath, "/gc")) { - geocode = uriPath.substring(1).toUpperCase(Locale.US); - } else { - showToast(res.getString(R.string.err_detail_open)); - finish(); - return; - } - } else if (uriHost.contains("opencaching.de")) { - if (StringUtils.startsWith(uriPath, "/oc")) { - geocode = uriPath.substring(1).toUpperCase(Locale.US); - } else { - geocode = uri.getQueryParameter("wp"); - if (StringUtils.isNotBlank(geocode)) { - geocode = geocode.toUpperCase(Locale.US); - } else { - showToast(res.getString(R.string.err_detail_open)); - finish(); - return; - } - } - } else { - showToast(res.getString(R.string.err_detail_open)); - finish(); - return; } } @@ -263,7 +251,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } // 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); + setCacheTitleBar(geocode, name, null); final LoadCacheHandler loadCacheHandler = new LoadCacheHandler(this, progress); @@ -275,7 +263,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc title = geocode; } progress.show(this, title, res.getString(R.string.cache_dialog_loading_details), true, loadCacheHandler.cancelMessage()); - } catch (final RuntimeException e) { + } catch (final RuntimeException ignored) { // nothing, we lost the window } @@ -293,8 +281,16 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (getPage(position) == Page.IMAGES) { loadCacheImages(); } + requireGeodata = getPage(position) == Page.DETAILS; + startOrStopGeoDataListener(); + + // cancel contextual actions on page change + if (currentActionMode != null) { + currentActionMode.finish(); + } } }); + requireGeodata = pageToOpen == 1; final String realGeocode = geocode; final String realGuid = guid; @@ -309,10 +305,11 @@ 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); + AndroidBeam.enable(this, this); } @Override + @Nullable public String getAndroidBeamUri() { return cache != null ? cache.getCgeoUrl() : null; } @@ -323,9 +320,17 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc outState.putInt(STATE_PAGE_INDEX, getCurrentItem()); } + private void startOrStopGeoDataListener() { + geoDataSubscription.unsubscribe(); + if (requireGeodata) { + geoDataSubscription = locationUpdater.start(GeoDirHandler.UPDATE_GEODATA); + } + } + @Override public void onResume() { - super.onResume(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA)); + super.onResume(); + startOrStopGeoDataListener(); if (refreshOnResume) { notifyDataSetChanged(); @@ -334,6 +339,12 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } @Override + public void onPause() { + geoDataSubscription.unsubscribe(); + super.onPause(); + } + + @Override public void onDestroy() { createSubscriptions.unsubscribe(); super.onDestroy(); @@ -389,17 +400,43 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return true; case R.id.menu_waypoint_duplicate: ensureSaved(); - if (cache.duplicateWaypoint(selectedWaypoint)) { - DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); - notifyDataSetChanged(); - } + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(final Void... params) { + if (cache.duplicateWaypoint(selectedWaypoint)) { + DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); + return true; + } + return false; + } + + @Override + protected void onPostExecute(final Boolean result) { + if (result) { + notifyDataSetChanged(); + } + } + }.execute(); return true; case R.id.menu_waypoint_delete: ensureSaved(); - if (cache.deleteWaypoint(selectedWaypoint)) { - DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); - notifyDataSetChanged(); - } + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(final Void... params) { + if (cache.deleteWaypoint(selectedWaypoint)) { + DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); + return true; + } + return false; + } + + @Override + protected void onPostExecute(final Boolean result) { + if (result) { + notifyDataSetChanged(); + } + } + }.execute(); return true; case R.id.menu_waypoint_navigate_default: if (selectedWaypoint != null) { @@ -413,17 +450,17 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return true; case R.id.menu_waypoint_caches_around: if (selectedWaypoint != null) { - CacheListActivity.startActivityCoordinates(this, selectedWaypoint.getCoords()); + CacheListActivity.startActivityCoordinates(this, selectedWaypoint.getCoords(), selectedWaypoint.getName()); } return true; case R.id.menu_waypoint_reset_cache_coords: ensureSaved(); if (ConnectorFactory.getConnector(cache).supportsOwnCoordinates()) { - createResetCacheCoordinatesDialog(cache, selectedWaypoint).show(); + createResetCacheCoordinatesDialog(selectedWaypoint).show(); } else { final ProgressDialog progressDialog = ProgressDialog.show(this, getString(R.string.cache), getString(R.string.waypoint_reset), true); final HandlerResetCoordinates handler = new HandlerResetCoordinates(this, progressDialog, false); - new ResetCoordsThread(cache, handler, selectedWaypoint, true, false, progressDialog).start(); + resetCoords(cache, handler, selectedWaypoint, true, false, progressDialog); } return true; case R.id.menu_calendar: @@ -441,13 +478,12 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public boolean onCreateOptionsMenu(final Menu menu) { CacheMenuHandler.addMenuItems(this, menu, cache); - MenuItem menuItem = menu.findItem(R.id.menu_default_navigation); + final MenuItem menuItem = menu.findItem(R.id.menu_default_navigation); final NavigationActionProvider navAction = (NavigationActionProvider) MenuItemCompat.getActionProvider(menuItem); if (navAction != null) { navAction.setNavigationSource(this); } - menuItem = menu.findItem(R.id.menu_navigate); - NavigationSelectionActionProvider.initialize(menuItem, cache); + NavigationSelectionActionProvider.initialize(menu.findItem(R.id.menu_navigate), cache); return true; } @@ -458,6 +494,18 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc menu.findItem(R.id.menu_store).setVisible(cache != null && !cache.isOffline()); menu.findItem(R.id.menu_delete).setVisible(cache != null && cache.isOffline()); menu.findItem(R.id.menu_refresh).setVisible(cache != null && cache.isOffline()); + menu.findItem(R.id.menu_gcvote).setVisible(cache != null && GCVote.isVotingPossible(cache)); + menu.findItem(R.id.menu_checker).setVisible(cache != null && StringUtils.isNotEmpty(CheckerUtils.getCheckerUrl(cache))); + // Comment out ignoring capabilities for this release, as this feature needs to be polished and accessible from + // other places as well as undo-able. + /* + if (cache != null) { + final IConnector connector = ConnectorFactory.getConnector(cache); + if (connector instanceof IgnoreCapability) { + menu.findItem(R.id.menu_ignore).setVisible(((IgnoreCapability) connector).canIgnoreCache(cache)); + } + } + */ return super.onPrepareOptionsMenu(menu); } @@ -479,6 +527,15 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc case R.id.menu_refresh: refreshCache(); return true; + case R.id.menu_gcvote: + showVoteDialog(); + return true; + case R.id.menu_checker: + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(CheckerUtils.getCheckerUrl(cache)))); + return true; + case R.id.menu_ignore: + ignoreCache(); + return true; default: if (NavigationAppFactory.onMenuItemSelected(item, this, cache)) { return true; @@ -492,6 +549,24 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return super.onOptionsItemSelected(item); } + private void ignoreCache() { + RxUtils.networkScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + ((IgnoreCapability) ConnectorFactory.getConnector(cache)).ignoreCache(cache); + } + }); + } + + private void showVoteDialog() { + GCVoteDialog.show(this, cache, new Runnable() { + @Override + public void run() { + notifyDataSetChanged(); + } + }); + } + private static final class CacheDetailsGeoDirHandler extends GeoDirHandler { private final WeakReference<CacheDetailActivity> activityRef; @@ -500,7 +575,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } @Override - public void updateGeoData(final IGeoData geo) { + public void updateGeoData(final GeoData geo) { final CacheDetailActivity activity = activityRef.get(); if (activity == null) { return; @@ -509,7 +584,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return; } - if (geo.getCoords() != null && activity.cache != null && activity.cache.getCoords() != null) { + if (activity.cache != null && activity.cache.getCoords() != null) { activity.cacheDistanceView.setText(Units.getDistanceFromKilometers(geo.getCoords().distanceTo(activity.cache.getCoords()))); activity.cacheDistanceView.bringToFront(); } @@ -570,7 +645,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } private void notifyDataSetChanged() { - // This might get called asynchronically when the activity is shut down + // This might get called asynchronous when the activity is shut down if (isFinishing()) { return; } @@ -591,7 +666,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(cache.getGeocode(), cache.getName(), cache.getType()); + setCacheTitleBar(cache); // reset imagesList so Images view page will be redrawn imagesList = null; @@ -604,19 +679,6 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc Settings.addCacheToHistory(cache.getGeocode()); } - 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)); - } - if (type != null) { - getSupportActionBar().setIcon(getResources().getDrawable(type.markerId)); - } else { - getSupportActionBar().setIcon(android.R.color.transparent); - } - } - /** * Tries to navigate to the {@link Geocache} of this activity using the default navigation tool. */ @@ -688,193 +750,13 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } } - private class AttributeViewBuilder { - private ViewGroup attributeIconsLayout; // layout for attribute icons - private ViewGroup attributeDescriptionsLayout; // layout for attribute descriptions - private boolean attributesShowAsIcons = true; // default: show icons - /** - * If the cache is from a non GC source, it might be without icons. Disable switching in those cases. - */ - private boolean noAttributeIconsFound = false; - private int attributeBoxMaxWidth; - - public void fillView(final LinearLayout attributeBox) { - // first ensure that the view is empty - attributeBox.removeAllViews(); - - // maximum width for attribute icons is screen width - paddings of parents - attributeBoxMaxWidth = Compatibility.getDisplayWidth(); - ViewParent child = attributeBox; - do { - if (child instanceof View) { - attributeBoxMaxWidth -= ((View) child).getPaddingLeft() + ((View) child).getPaddingRight(); - } - child = child.getParent(); - } while (child != null); - - // delete views holding description / icons - attributeDescriptionsLayout = null; - attributeIconsLayout = null; - - attributeBox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View v) { - // toggle between attribute icons and descriptions - toggleAttributeDisplay(attributeBox, attributeBoxMaxWidth); - } - }); - - // icons or text? - // - // also show icons when noAttributeImagesFound == true. Explanation: - // 1. no icons could be found in the first invocation of this method - // 2. user refreshes cache from web - // 3. now this method is called again - // 4. attributeShowAsIcons is false but noAttributeImagesFound is true - // => try to show them now - if (attributesShowAsIcons || noAttributeIconsFound) { - showAttributeIcons(attributeBox, attributeBoxMaxWidth); - } else { - showAttributeDescriptions(attributeBox); - } - } - - /** - * lazy-creates the layout holding the icons of the caches attributes - * and makes it visible - */ - private void showAttributeIcons(final LinearLayout attribBox, final int parentWidth) { - if (attributeIconsLayout == null) { - attributeIconsLayout = createAttributeIconsLayout(parentWidth); - // no matching icons found? show text - if (noAttributeIconsFound) { - showAttributeDescriptions(attribBox); - return; - } - } - attribBox.removeAllViews(); - attribBox.addView(attributeIconsLayout); - attributesShowAsIcons = true; - } - - /** - * lazy-creates the layout holding the descriptions of the caches attributes - * and makes it visible - */ - private void showAttributeDescriptions(final LinearLayout attribBox) { - if (attributeDescriptionsLayout == null) { - attributeDescriptionsLayout = createAttributeDescriptionsLayout(attribBox); - } - attribBox.removeAllViews(); - attribBox.addView(attributeDescriptionsLayout); - attributesShowAsIcons = false; - } - - /** - * toggle attribute descriptions and icons - */ - private void toggleAttributeDisplay(final LinearLayout attribBox, final int parentWidth) { - // Don't toggle when there are no icons to show. - if (noAttributeIconsFound) { - return; - } - - // toggle - if (attributesShowAsIcons) { - showAttributeDescriptions(attribBox); - } else { - showAttributeIcons(attribBox, parentWidth); - } - } - - private ViewGroup createAttributeIconsLayout(final int parentWidth) { - final LinearLayout rows = new LinearLayout(CacheDetailActivity.this); - rows.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - rows.setOrientation(LinearLayout.VERTICAL); - - LinearLayout attributeRow = newAttributeIconsRow(); - rows.addView(attributeRow); - - noAttributeIconsFound = true; - - for (final String attributeName : cache.getAttributes()) { - // check if another attribute icon fits in this row - attributeRow.measure(0, 0); - final int rowWidth = attributeRow.getMeasuredWidth(); - final FrameLayout fl = (FrameLayout) getLayoutInflater().inflate(R.layout.attribute_image, attributeRow, false); - final ImageView iv = (ImageView) fl.getChildAt(0); - if ((parentWidth - rowWidth) < iv.getLayoutParams().width) { - // make a new row - attributeRow = newAttributeIconsRow(); - rows.addView(attributeRow); - } - - final boolean strikeThrough = !CacheAttribute.isEnabled(attributeName); - final CacheAttribute attrib = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attributeName)); - if (attrib != null) { - noAttributeIconsFound = false; - Drawable d = res.getDrawable(attrib.drawableId); - iv.setImageDrawable(d); - // strike through? - if (strikeThrough) { - // generate strike through image with same properties as attribute image - final ImageView strikeThroughImage = new ImageView(CacheDetailActivity.this); - strikeThroughImage.setLayoutParams(iv.getLayoutParams()); - d = res.getDrawable(R.drawable.attribute__strikethru); - strikeThroughImage.setImageDrawable(d); - fl.addView(strikeThroughImage); - } - } else { - final Drawable d = res.getDrawable(R.drawable.attribute_unknown); - iv.setImageDrawable(d); - } - - attributeRow.addView(fl); - } - - return rows; - } - - private LinearLayout newAttributeIconsRow() { - final LinearLayout rowLayout = new LinearLayout(CacheDetailActivity.this); - rowLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT)); - rowLayout.setOrientation(LinearLayout.HORIZONTAL); - return rowLayout; - } - - private ViewGroup createAttributeDescriptionsLayout(final LinearLayout parentView) { - final LinearLayout descriptions = (LinearLayout) getLayoutInflater().inflate( - R.layout.attribute_descriptions, parentView, false); - final TextView attribView = (TextView) descriptions.getChildAt(0); - - final StringBuilder buffer = new StringBuilder(); - for (String attributeName : cache.getAttributes()) { - final boolean enabled = CacheAttribute.isEnabled(attributeName); - // search for a translation of the attribute - final CacheAttribute attrib = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attributeName)); - if (attrib != null) { - attributeName = attrib.getL10n(enabled); - } - if (buffer.length() > 0) { - buffer.append('\n'); - } - buffer.append(attributeName); - } - - attribView.setText(buffer); - - return descriptions; - } - } - private void refreshCache() { if (progress.isShowing()) { showToast(res.getString(R.string.err_detail_still_working)); return; } - if (!Network.isNetworkConnected(getApplicationContext())) { + if (!Network.isNetworkConnected()) { showToast(getString(R.string.err_server)); return; } @@ -883,7 +765,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc progress.show(this, res.getString(R.string.cache_dialog_refresh_title), res.getString(R.string.cache_dialog_refresh_message), true, refreshCacheHandler.cancelMessage()); - cache.refresh(refreshCacheHandler, RxUtils.networkScheduler); + cache.refresh(refreshCacheHandler, RxUtils.refreshScheduler); } private void dropCache() { @@ -893,7 +775,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } progress.show(this, res.getString(R.string.cache_dialog_offline_drop_title), res.getString(R.string.cache_dialog_offline_drop_message), true, null); - cache.drop(new ChangeNotificationHandler(this, progress), RxUtils.networkScheduler); + cache.drop(new ChangeNotificationHandler(this, progress)); } private void storeCache() { @@ -910,9 +792,9 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc public void call(final Integer selectedListId) { storeCache(selectedListId, new StoreCacheHandler(CacheDetailActivity.this, progress)); } - }, true, StoredList.TEMPORARY_LIST_ID); + }, true, StoredList.TEMPORARY_LIST.id); } else { - storeCache(StoredList.TEMPORARY_LIST_ID, new StoreCacheHandler(this, progress)); + storeCache(StoredList.TEMPORARY_LIST.id, new StoreCacheHandler(this, progress)); } } @@ -920,12 +802,9 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc * Creator for details-view. */ private class DetailsViewCreator extends AbstractCachingPageViewCreator<ScrollView> { - /** - * Reference to the details list, so that the helper-method can access it without an additional argument - */ + // Reference to the details list and favorite line, so that the helper-method can access them without an additional argument private LinearLayout detailsList; - - private Thread watchlistThread; + private ImmutablePair<RelativeLayout, TextView> favoriteLine; @Override public ScrollView getDispatchedView(final ViewGroup parentView) { @@ -937,7 +816,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc view = (ScrollView) getLayoutInflater().inflate(R.layout.cachedetail_details_page, parentView, false); // Start loading preview map - AndroidObservable.bindActivity(CacheDetailActivity.this, previewMap).subscribeOn(RxUtils.networkScheduler) + AppObservable.bindActivity(CacheDetailActivity.this, previewMap).subscribeOn(RxUtils.networkScheduler) .subscribe(new Action1<BitmapDrawable>() { @Override public void call(final BitmapDrawable image) { @@ -962,10 +841,10 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc span.setSpan(new ForegroundColorSpan(res.getColor(R.color.archived_cache_color)), 0, span.toString().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - addContextMenu(details.add(R.string.cache_name, span)); + addContextMenu(details.add(R.string.cache_name, span).right); details.add(R.string.cache_type, cache.getType().getL10n()); details.addSize(cache); - addContextMenu(details.add(R.string.cache_geocode, cache.getGeocode())); + addContextMenu(details.add(R.string.cache_geocode, cache.getGeocode()).right); details.addCacheState(cache); details.addDistance(cache, cacheDistanceView); @@ -976,9 +855,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc details.addRating(cache); // favorite count - if (cache.getFavoritePoints() > 0) { - details.add(R.string.cache_favorite, cache.getFavoritePoints() + "×"); - } + favoriteLine = details.add(R.string.cache_favorite, ""); // own rating if (cache.getMyVote() > 0) { @@ -987,7 +864,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // cache author if (StringUtils.isNotBlank(cache.getOwnerDisplayName()) || StringUtils.isNotBlank(cache.getOwnerUserId())) { - final TextView ownerView = details.add(R.string.cache_owner, ""); + final TextView ownerView = details.add(R.string.cache_owner, "").right; if (StringUtils.isNotBlank(cache.getOwnerDisplayName())) { ownerView.setText(cache.getOwnerDisplayName(), TextView.BufferType.SPANNABLE); } else { // OwnerReal guaranteed to be not blank based on above @@ -1009,17 +886,15 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // cache coordinates if (cache.getCoords() != null) { - final TextView valueView = details.add(R.string.cache_coordinates, cache.getCoords().toString()); + final TextView valueView = details.add(R.string.cache_coordinates, cache.getCoords().toString()).right; valueView.setOnClickListener(new CoordinatesFormatSwitcher(cache.getCoords())); addContextMenu(valueView); } // cache attributes - if (!cache.getAttributes().isEmpty()) { - final LinearLayout innerLayout = ButterKnife.findById(view, R.id.attributes_innerbox); - new AttributeViewBuilder().fillView(innerLayout); - view.findViewById(R.id.attributes_box).setVisibility(View.VISIBLE); - } + updateAttributesText(); + updateAttributesIcons(); + ButterKnife.findById(view, R.id.attributes_box).setVisibility(cache.getAttributes().isEmpty() ? View.GONE : View.VISIBLE); updateOfflineBox(view, cache, res, new RefreshCacheClickListener(), new DropCacheClickListener(), new StoreCacheClickListener()); @@ -1058,6 +933,61 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return view; } + private void updateAttributesIcons() { + final GridView gridView = ButterKnife.findById(view, R.id.attributes_grid); + final List<String> attributes = cache.getAttributes(); + if (attributes.isEmpty()) { + gridView.setVisibility(View.GONE); + return; + } + gridView.setAdapter(new AttributesGridAdapter(CacheDetailActivity.this, cache)); + gridView.setVisibility(View.VISIBLE); + gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(final android.widget.AdapterView<?> parent, final View view, final int position, final long id) { + toggleAttributesView(); + } + }); + } + + protected void toggleAttributesView() { + final View textView = ButterKnife.findById(view, R.id.attributes_text); + textView.setVisibility(textView.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); + final View gridView = ButterKnife.findById(view, R.id.attributes_grid); + gridView.setVisibility(gridView.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); + } + + private void updateAttributesText() { + final TextView attribView = ButterKnife.findById(view, R.id.attributes_text); + final List<String> attributes = cache.getAttributes(); + if (attributes.isEmpty()) { + attribView.setVisibility(View.GONE); + return; + } + final StringBuilder text = new StringBuilder(); + for (String attributeName : attributes) { + final boolean enabled = CacheAttribute.isEnabled(attributeName); + // search for a translation of the attribute + final CacheAttribute attrib = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attributeName)); + if (attrib != null) { + attributeName = attrib.getL10n(enabled); + } + if (text.length() > 0) { + text.append('\n'); + } + text.append(attributeName); + } + attribView.setText(text); + attribView.setVisibility(View.GONE); + attribView.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(final View v) { + toggleAttributesView(); + } + }); + } + private class StoreCacheClickListener implements View.OnClickListener { @Override public void onClick(final View arg0) { @@ -1083,164 +1013,133 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc /** * Abstract Listener for add / remove buttons for watchlist */ - private abstract class AbstractWatchlistClickListener implements View.OnClickListener { - public void doExecute(final int titleId, final int messageId, final Thread thread) { + private abstract class AbstractPropertyListener implements View.OnClickListener { + + private final SimpleCancellableHandler handler = new SimpleCancellableHandler(CacheDetailActivity.this, progress) { + @Override + public void handleRegularMessage(final Message message) { + super.handleRegularMessage(message); + updateWatchlistBox(); + updateFavPointBox(); + } + }; + + public void doExecute(final int titleId, final int messageId, final Action1<SimpleCancellableHandler> action) { if (progress.isShowing()) { showToast(res.getString(R.string.err_watchlist_still_managing)); return; } progress.show(CacheDetailActivity.this, res.getString(titleId), res.getString(messageId), true, null); - - if (watchlistThread != null) { - watchlistThread.interrupt(); - } - - watchlistThread = thread; - watchlistThread.start(); + RxUtils.networkScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + action.call(handler); + } + }); } } /** * Listener for "add to watchlist" button */ - private class AddToWatchlistClickListener extends AbstractWatchlistClickListener { + private class AddToWatchlistClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_watchlist_add_title, R.string.cache_dialog_watchlist_add_message, - new WatchlistAddThread(new SimpleUpdateHandler(CacheDetailActivity.this, progress))); + new Action1<SimpleCancellableHandler>() { + @Override + public void call(final SimpleCancellableHandler simpleCancellableHandler) { + watchListAdd(simpleCancellableHandler); + } + }); } } /** * Listener for "remove from watchlist" button */ - private class RemoveFromWatchlistClickListener extends AbstractWatchlistClickListener { + private class RemoveFromWatchlistClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_watchlist_remove_title, R.string.cache_dialog_watchlist_remove_message, - new WatchlistRemoveThread(new SimpleUpdateHandler(CacheDetailActivity.this, progress))); + new Action1<SimpleCancellableHandler>() { + @Override + public void call(final SimpleCancellableHandler simpleCancellableHandler) { + watchListRemove(simpleCancellableHandler); + } + }); } } - /** Thread to add this cache to the watchlist of the user */ - private class WatchlistAddThread extends Thread { - private final Handler handler; - - public WatchlistAddThread(final Handler handler) { - this.handler = handler; - } - - @Override - public void run() { - watchlistThread = null; - Message msg; - if (ConnectorFactory.getConnector(cache).addToWatchlist(cache)) { - msg = Message.obtain(handler, MESSAGE_SUCCEEDED); - } else { - msg = Message.obtain(handler, MESSAGE_FAILED); - final Bundle bundle = new Bundle(); - bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, res.getString(R.string.err_watchlist_failed)); - msg.setData(bundle); - } - handler.sendMessage(msg); + /** Add this cache to the watchlist of the user */ + private void watchListAdd(final SimpleCancellableHandler handler) { + if (ConnectorFactory.getConnector(cache).addToWatchlist(cache)) { + handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); + } else { + handler.sendTextMessage(MESSAGE_FAILED, R.string.err_watchlist_failed); } } - /** Thread to remove this cache from the watchlist of the user */ - private class WatchlistRemoveThread extends Thread { - private final Handler handler; - - public WatchlistRemoveThread(final Handler handler) { - this.handler = handler; - } - - @Override - public void run() { - watchlistThread = null; - Message msg; - if (ConnectorFactory.getConnector(cache).removeFromWatchlist(cache)) { - msg = Message.obtain(handler, MESSAGE_SUCCEEDED); - } else { - msg = Message.obtain(handler, MESSAGE_FAILED); - final Bundle bundle = new Bundle(); - bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, res.getString(R.string.err_watchlist_failed)); - msg.setData(bundle); - } - handler.sendMessage(msg); + /** Remove this cache from the watchlist of the user */ + private void watchListRemove(final SimpleCancellableHandler handler) { + if (ConnectorFactory.getConnector(cache).removeFromWatchlist(cache)) { + handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); + } else { + handler.sendTextMessage(MESSAGE_FAILED, R.string.err_watchlist_failed); } } - /** Thread to add this cache to the favorite list of the user */ - private class FavoriteAddThread extends Thread { - private final Handler handler; - - public FavoriteAddThread(final Handler handler) { - this.handler = handler; - } - - @Override - public void run() { - watchlistThread = null; - Message msg; - if (GCConnector.addToFavorites(cache)) { - msg = Message.obtain(handler, MESSAGE_SUCCEEDED); - } else { - msg = Message.obtain(handler, MESSAGE_FAILED); - final Bundle bundle = new Bundle(); - bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, res.getString(R.string.err_favorite_failed)); - msg.setData(bundle); - } - handler.sendMessage(msg); + /** Add this cache to the favorite list of the user */ + private void favoriteAdd(final SimpleCancellableHandler handler) { + if (GCConnector.addToFavorites(cache)) { + handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); + } else { + handler.sendTextMessage(MESSAGE_FAILED, R.string.err_favorite_failed); } } - /** Thread to remove this cache to the favorite list of the user */ - private class FavoriteRemoveThread extends Thread { - private final Handler handler; - - public FavoriteRemoveThread(final Handler handler) { - this.handler = handler; - } - - @Override - public void run() { - watchlistThread = null; - Message msg; - if (GCConnector.removeFromFavorites(cache)) { - msg = Message.obtain(handler, MESSAGE_SUCCEEDED); - } else { - msg = Message.obtain(handler, MESSAGE_FAILED); - final Bundle bundle = new Bundle(); - bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, res.getString(R.string.err_favorite_failed)); - msg.setData(bundle); - } - handler.sendMessage(msg); + /** Remove this cache to the favorite list of the user */ + private void favoriteRemove(final SimpleCancellableHandler handler) { + if (GCConnector.removeFromFavorites(cache)) { + handler.obtainMessage(MESSAGE_SUCCEEDED).sendToTarget(); + } else { + handler.sendTextMessage(MESSAGE_FAILED, R.string.err_favorite_failed); } } /** * Listener for "add to favorites" button */ - private class FavoriteAddClickListener extends AbstractWatchlistClickListener { + private class FavoriteAddClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_favorite_add_title, R.string.cache_dialog_favorite_add_message, - new FavoriteAddThread(new SimpleUpdateHandler(CacheDetailActivity.this, progress))); + new Action1<SimpleCancellableHandler>() { + @Override + public void call(final SimpleCancellableHandler simpleCancellableHandler) { + favoriteAdd(simpleCancellableHandler); + } + }); } } /** * Listener for "remove from favorites" button */ - private class FavoriteRemoveClickListener extends AbstractWatchlistClickListener { + private class FavoriteRemoveClickListener extends AbstractPropertyListener { @Override public void onClick(final View arg0) { doExecute(R.string.cache_dialog_favorite_remove_title, R.string.cache_dialog_favorite_remove_message, - new FavoriteRemoveThread(new SimpleUpdateHandler(CacheDetailActivity.this, progress))); + new Action1<SimpleCancellableHandler>() { + @Override + public void call(final SimpleCancellableHandler simpleCancellableHandler) { + favoriteRemove(simpleCancellableHandler); + } + }); } } @@ -1249,7 +1148,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc */ private class ChangeListClickListener implements View.OnClickListener { @Override - public void onClick(final View view) { + public void onClick(final View v) { new StoredList.UserInterface(CacheDetailActivity.this).promptForListSelection(R.string.list_title, new Action1<Integer>() { @Override @@ -1277,7 +1176,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } /** - * shows/hides buttons, sets text in watchlist box + * Show/hide buttons, set text in watchlist box */ private void updateWatchlistBox() { final LinearLayout layout = ButterKnife.findById(view, R.id.watchlist_box); @@ -1307,13 +1206,20 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc buttonRemove.setEnabled(false); buttonRemove.setVisibility(View.GONE); } - } /** - * shows/hides buttons, sets text in watchlist box + * Show/hide buttons, set text in favorite line and box */ private void updateFavPointBox() { + // Favorite counts + if (cache.getFavoritePoints() > 0) { + favoriteLine.left.setVisibility(View.VISIBLE); + favoriteLine.right.setText(cache.getFavoritePoints() + "×"); + } else { + favoriteLine.left.setVisibility(View.GONE); + } + final LinearLayout layout = ButterKnife.findById(view, R.id.favpoint_box); final boolean supportsFavoritePoints = cache.supportsFavoritePoints(); layout.setVisibility(supportsFavoritePoints ? View.VISIBLE : View.GONE); @@ -1356,12 +1262,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc // update text final TextView text = ButterKnife.findById(view, R.id.list_text); final StoredList list = DataStore.getList(cache.getListId()); - if (list != null) { - text.setText(res.getString(R.string.cache_list_text) + " " + list.title); - } else { - // this should not happen - text.setText(R.string.cache_list_unknown); - } + text.setText(res.getString(R.string.cache_list_text) + " " + list.title); } else { // hide box box.setVisibility(View.GONE); @@ -1395,6 +1296,11 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc }); + /** + * Reflect the (contextual) action mode of the action bar. + */ + protected ActionMode currentActionMode; + protected class DescriptionViewCreator extends AbstractCachingPageViewCreator<ScrollView> { @InjectView(R.id.personalnote) protected TextView personalNoteView; @@ -1517,19 +1423,26 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc return view; } - Thread currentThread; - private void uploadPersonalNote() { final SimpleCancellableHandler myHandler = new SimpleCancellableHandler(CacheDetailActivity.this, progress); final Message cancelMessage = myHandler.cancelMessage(res.getString(R.string.cache_personal_note_upload_cancelled)); progress.show(CacheDetailActivity.this, res.getString(R.string.cache_personal_note_uploading), res.getString(R.string.cache_personal_note_uploading), true, cancelMessage); - if (currentThread != null) { - currentThread.interrupt(); - } - currentThread = new UploadPersonalNoteThread(cache, myHandler); - currentThread.start(); + myHandler.unsubscribeIfCancelled(RxUtils.networkScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + final IConnector con = ConnectorFactory.getConnector(cache); + if (con.supportsPersonalNote()) { + con.uploadPersonalNote(cache); + } + final Message msg = Message.obtain(); + final Bundle bundle = new Bundle(); + bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, CgeoApplication.getInstance().getString(R.string.cache_personal_note_upload_done)); + msg.setData(bundle); + myHandler.sendMessage(msg); + } + })); } private void loadLongDescription() { @@ -1563,9 +1476,11 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (unknownTagsHandler.isProblematicDetected()) { final int startPos = description.length(); final IConnector connector = ConnectorFactory.getConnector(cache); - final Spanned tableNote = Html.fromHtml(res.getString(R.string.cache_description_table_note, "<a href=\"" + cache.getUrl() + "\">" + connector.getName() + "</a>")); - ((Editable) description).append("\n\n").append(tableNote); - ((Editable) description).setSpan(new StyleSpan(Typeface.ITALIC), startPos, description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (StringUtils.isNotEmpty(cache.getUrl())) { + final Spanned tableNote = Html.fromHtml(res.getString(R.string.cache_description_table_note, "<a href=\"" + cache.getUrl() + "\">" + connector.getName() + "</a>")); + ((Editable) description).append("\n\n").append(tableNote); + ((Editable) description).setSpan(new StyleSpan(Typeface.ITALIC), startPos, description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } } /** @@ -1597,13 +1512,13 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (null != loadingIndicatorView) { loadingIndicatorView.setVisibility(View.GONE); } - } catch (final Exception e) { + } catch (final Exception ignored) { showToast(res.getString(R.string.err_load_descr_failed)); } } private static void fixTextColor(final String descriptionString, final IndexOutOfBoundsAvoidingTextView descriptionView) { - int backcolor; + final int backcolor; if (Settings.isLightSkin()) { backcolor = color.white; @@ -1653,12 +1568,18 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (!cache.isOffline()) { showToast(getString(R.string.info_cache_saved)); cache.setListId(StoredList.STANDARD_LIST_ID); - DataStore.saveCache(cache, LoadFlags.SAVE_ALL); + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(final Void... params) { + DataStore.saveCache(cache, LoadFlags.SAVE_ALL); + return null; + } + }.execute(); } } private class WaypointsViewCreator extends AbstractCachingPageViewCreator<ListView> { - private final int VISITED_INSET = (int) (6.6f * CgeoApplication.getInstance().getResources().getDisplayMetrics().density + 0.5f); + private final int visitedInset = (int) (6.6f * CgeoApplication.getInstance().getResources().getDisplayMetrics().density + 0.5f); @Override public ListView getDispatchedView(final ViewGroup parentView) { @@ -1741,15 +1662,15 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } else { nameView.setText(res.getString(R.string.waypoint)); } - setWaypointIcon(res, nameView, wpt); + setWaypointIcon(nameView, wpt); // visited if (wpt.isVisited()) { - final TypedValue a = new TypedValue(); - getTheme().resolveAttribute(R.attr.text_color_grey, a, true); - if (a.type >= TypedValue.TYPE_FIRST_COLOR_INT && a.type <= TypedValue.TYPE_LAST_COLOR_INT) { + final TypedValue typedValue = new TypedValue(); + getTheme().resolveAttribute(R.attr.text_color_grey, typedValue, true); + if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { // really should be just a color! - nameView.setTextColor(a.data); + nameView.setTextColor(typedValue.data); } } @@ -1759,7 +1680,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc noteView.setOnClickListener(new DecryptTextClickListener(noteView)); noteView.setVisibility(View.VISIBLE); if (TextUtils.containsHtml(wpt.getNote())) { - noteView.setText(Html.fromHtml(wpt.getNote()), TextView.BufferType.SPANNABLE); + noteView.setText(Html.fromHtml(wpt.getNote(), new SmileyImage(cache.getGeocode(), noteView), new UnknownTagsHandler()), TextView.BufferType.SPANNABLE); } else { noteView.setText(wpt.getNote()); @@ -1805,15 +1726,15 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc }); } - private void setWaypointIcon(final Resources res, final TextView nameView, final Waypoint wpt) { + private void setWaypointIcon(final TextView nameView, final Waypoint wpt) { final WaypointType waypointType = wpt.getWaypointType(); final Drawable icon; if (wpt.isVisited()) { final LayerDrawable ld = new LayerDrawable(new Drawable[] { res.getDrawable(waypointType.markerId), res.getDrawable(R.drawable.tick) }); - ld.setLayerInset(0, 0, 0, VISITED_INSET, VISITED_INSET); - ld.setLayerInset(1, VISITED_INSET, VISITED_INSET, 0, 0); + ld.setLayerInset(0, 0, 0, visitedInset, visitedInset); + ld.setLayerInset(1, visitedInset, visitedInset, 0, 0); icon = ld; } else { icon = res.getDrawable(waypointType.markerId); @@ -1880,10 +1801,14 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public boolean onLongClick(final View v) { - startSupportActionMode(new ActionMode.Callback() { + currentActionMode = startSupportActionMode(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) { + return prepareClipboardActionMode(view, actionMode, menu); + } + + private boolean prepareClipboardActionMode(final View view, final ActionMode actionMode, final Menu menu) { switch (view.getId()) { case R.id.value: // coordinates, gc-code, name assert view instanceof TextView; @@ -1892,29 +1817,25 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc buildDetailsContextMenu(actionMode, menu, itemTitle, true); return true; case R.id.shortdesc: - assert view instanceof TextView; - clickedItemText = ((TextView) view).getText(); + clickedItemText = cache.getShortDescription(); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_description), false); return true; case R.id.longdesc: - assert view instanceof TextView; // combine short and long description final String shortDesc = cache.getShortDescription(); if (StringUtils.isBlank(shortDesc)) { - clickedItemText = ((TextView) view).getText(); + clickedItemText = cache.getDescription(); } else { - clickedItemText = shortDesc + "\n\n" + ((TextView) view).getText(); + clickedItemText = shortDesc + "\n\n" + cache.getDescription(); } buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_description), false); return true; case R.id.personalnote: - assert view instanceof TextView; - clickedItemText = ((TextView) view).getText(); + clickedItemText = cache.getPersonalNote(); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_personal_note), true); return true; case R.id.hint: - assert view instanceof TextView; - clickedItemText = ((TextView) view).getText(); + clickedItemText = cache.getHint(); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_hint), false); return true; case R.id.log: @@ -1923,8 +1844,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_logs), false); return true; case R.id.date: // event date - assert view instanceof TextView; - clickedItemText = ((TextView) view).getText(); + clickedItemText = Formatter.formatHiddenDate(cache); buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_event), true); menu.findItem(R.id.menu_calendar).setVisible(cache.canBeAddedToCalendar()); return true; @@ -1934,13 +1854,13 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public void onDestroyActionMode(final ActionMode actionMode) { - // do nothing + currentActionMode = null; } @Override public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) { actionMode.getMenuInflater().inflate(R.menu.details_context, menu); - + prepareClipboardActionMode(view, actionMode, menu); // Return true so that the action mode is shown return true; } @@ -1974,7 +1894,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc /** * A dialog to allow the user to select reseting coordinates local/remote/both. */ - private AlertDialog createResetCacheCoordinatesDialog(final Geocache cache, final Waypoint wpt) { + private AlertDialog createResetCacheCoordinatesDialog(final Waypoint wpt) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.waypoint_reset_cache_coords); @@ -1987,13 +1907,16 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc dialog.dismiss(); final ProgressDialog progressDialog = ProgressDialog.show(CacheDetailActivity.this, getString(R.string.cache), getString(R.string.waypoint_reset), true); final HandlerResetCoordinates handler = new HandlerResetCoordinates(CacheDetailActivity.this, progressDialog, which == 1); - new ResetCoordsThread(cache, handler, wpt, which == 0 || which == 1, which == 1, progressDialog).start(); + resetCoords(cache, handler, wpt, which == 0 || which == 1, which == 1, progressDialog); } }); return builder.create(); } private static class HandlerResetCoordinates extends WeakReferenceHandler<CacheDetailActivity> { + public static final int LOCAL = 0; + public static final int ON_WEBSITE = 1; + private boolean remoteFinished = false; private boolean localFinished = false; private final ProgressDialog progressDialog; @@ -2007,7 +1930,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public void handleMessage(final Message msg) { - if (msg.what == ResetCoordsThread.LOCAL) { + if (msg.what == LOCAL) { localFinished = true; } else { remoteFinished = true; @@ -2024,94 +1947,53 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc } - private class ResetCoordsThread extends Thread { - - private final Geocache cache; - private final Handler handler; - private final boolean local; - private final boolean remote; - private final Waypoint wpt; - private final ProgressDialog progress; - public static final int LOCAL = 0; - public static final int ON_WEBSITE = 1; - - public ResetCoordsThread(final Geocache cache, final Handler handler, final Waypoint wpt, final boolean local, final boolean remote, final ProgressDialog progress) { - this.cache = cache; - this.handler = handler; - this.local = local; - this.remote = remote; - this.wpt = wpt; - this.progress = progress; - } - - @Override - public void run() { - - if (local) { - runOnUiThread(new Runnable() { - @Override - public void run() { - progress.setMessage(res.getString(R.string.waypoint_reset_cache_coords)); - } - }); - cache.setCoords(wpt.getCoords()); - cache.setUserModifiedCoords(false); - cache.deleteWaypointForce(wpt); - DataStore.saveChangedCache(cache); - handler.sendEmptyMessage(LOCAL); - } - - final IConnector con = ConnectorFactory.getConnector(cache); - if (remote && con.supportsOwnCoordinates()) { - runOnUiThread(new Runnable() { - @Override - public void run() { - progress.setMessage(res.getString(R.string.waypoint_coordinates_being_reset_on_website)); - } - }); - - final boolean result = con.deleteModifiedCoordinates(cache); - - runOnUiThread(new Runnable() { + private void resetCoords(final Geocache cache, final Handler handler, final Waypoint wpt, final boolean local, final boolean remote, final ProgressDialog progress) { + RxUtils.networkScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + if (local) { + runOnUiThread(new Runnable() { + @Override + public void run() { + progress.setMessage(res.getString(R.string.waypoint_reset_cache_coords)); + } + }); + cache.setCoords(wpt.getCoords()); + cache.setUserModifiedCoords(false); + cache.deleteWaypointForce(wpt); + DataStore.saveChangedCache(cache); + handler.sendEmptyMessage(HandlerResetCoordinates.LOCAL); + } - @Override - public void run() { - if (result) { - showToast(getString(R.string.waypoint_coordinates_has_been_reset_on_website)); - } else { - showToast(getString(R.string.waypoint_coordinates_upload_error)); + final IConnector con = ConnectorFactory.getConnector(cache); + if (remote && con.supportsOwnCoordinates()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + progress.setMessage(res.getString(R.string.waypoint_coordinates_being_reset_on_website)); } - handler.sendEmptyMessage(ON_WEBSITE); - notifyDataSetChanged(); - } + }); - }); + final boolean result = con.deleteModifiedCoordinates(cache); - } - } - } + runOnUiThread(new Runnable() { - private static class UploadPersonalNoteThread extends Thread { - private Geocache cache = null; - private CancellableHandler handler = null; + @Override + public void run() { + if (result) { + showToast(getString(R.string.waypoint_coordinates_has_been_reset_on_website)); + } else { + showToast(getString(R.string.waypoint_coordinates_upload_error)); + } + handler.sendEmptyMessage(HandlerResetCoordinates.ON_WEBSITE); + notifyDataSetChanged(); + } - public UploadPersonalNoteThread(final Geocache cache, final CancellableHandler handler) { - this.cache = cache; - this.handler = handler; - } + }); - @Override - public void run() { - final IConnector con = ConnectorFactory.getConnector(cache); - if (con.supportsPersonalNote()) { - con.uploadPersonalNote(cache); + } } - final Message msg = Message.obtain(); - final Bundle bundle = new Bundle(); - bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, CgeoApplication.getInstance().getString(R.string.cache_personal_note_upload_done)); - msg.setData(bundle); - handler.sendMessage(msg); - } + }); } @Override @@ -2186,7 +2068,7 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc if (cache.isOffline()) { final long diff = (System.currentTimeMillis() / (60 * 1000)) - (cache.getDetailedUpdate() / (60 * 1000)); // minutes - String ago; + final String ago; if (diff < 15) { ago = res.getString(R.string.cache_offline_time_mins_few); } else if (diff < 50) { @@ -2232,7 +2114,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); } } } @@ -2248,7 +2130,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); } } } @@ -2261,27 +2143,11 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public void handleMessage(final Message msg) { - notifyDatasetChanged(activityRef); - } - } - - private static final class SimpleUpdateHandler extends SimpleHandler { - - public SimpleUpdateHandler(final CacheDetailActivity activity, final Progress progress) { - super(activity, progress); - } - - @Override - public void handleMessage(final Message msg) { - 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(); @@ -2308,18 +2174,29 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @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(); + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(final Void... params) { + cache.setPersonalNote(note); + cache.parseWaypointsFromNote(); + DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); + return null; + } + + @Override + protected void onPostExecute(final Void v) { + notifyDataSetChanged(); + } + }.execute(); } private static void setPersonalNote(final TextView personalNoteView, final String personalNote) { personalNoteView.setText(personalNote, TextView.BufferType.SPANNABLE); if (StringUtils.isNotBlank(personalNote)) { personalNoteView.setVisibility(View.VISIBLE); + Linkify.addLinks(personalNoteView, Linkify.ALL); } else { personalNoteView.setVisibility(View.GONE); } @@ -2337,6 +2214,10 @@ public class CacheDetailActivity extends AbstractViewPagerActivity<CacheDetailAc @Override public void cachesAround() { - CacheListActivity.startActivityCoordinates(this, cache.getCoords()); + CacheListActivity.startActivityCoordinates(this, cache.getCoords(), cache.getName()); + } + + public void setNeedsRefresh() { + refreshOnResume = true; } } diff --git a/main/src/cgeo/geocaching/CacheListActivity.java b/main/src/cgeo/geocaching/CacheListActivity.java index 522004e..bedb737 100644 --- a/main/src/cgeo/geocaching/CacheListActivity.java +++ b/main/src/cgeo/geocaching/CacheListActivity.java @@ -9,7 +9,9 @@ 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.apps.cachelist.CacheListApp; +import cgeo.geocaching.apps.cachelist.CacheListApps; +import cgeo.geocaching.apps.cachelist.ListNavigationSelectionActionProvider; import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.connector.gc.RecaptchaHandler; import cgeo.geocaching.enumerations.CacheListType; @@ -19,15 +21,14 @@ import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.export.FieldnoteExport; import cgeo.geocaching.export.GpxExport; import cgeo.geocaching.files.GPXImporter; -import cgeo.geocaching.filter.FilterUserInterface; +import cgeo.geocaching.filter.FilterActivity; import cgeo.geocaching.filter.IFilter; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.list.AbstractList; +import cgeo.geocaching.list.ListNameMemento; import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.loaders.AbstractSearchLoader; import cgeo.geocaching.loaders.AbstractSearchLoader.CacheListLoaderType; -import cgeo.geocaching.loaders.AddressGeocacheListLoader; import cgeo.geocaching.loaders.CoordsGeocacheListLoader; import cgeo.geocaching.loaders.FinderGeocacheListLoader; import cgeo.geocaching.loaders.HistoryGeocacheListLoader; @@ -36,14 +37,15 @@ import cgeo.geocaching.loaders.NextPageGeocacheListLoader; import cgeo.geocaching.loaders.OfflineGeocacheListLoader; import cgeo.geocaching.loaders.OwnerGeocacheListLoader; import cgeo.geocaching.loaders.PocketGeocacheListLoader; -import cgeo.geocaching.loaders.RemoveFromHistoryLoader; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.CGeoMap; import cgeo.geocaching.network.Cookies; +import cgeo.geocaching.network.DownloadProgress; import cgeo.geocaching.network.Network; -import cgeo.geocaching.network.Parameters; -import cgeo.geocaching.sensors.DirectionProvider; +import cgeo.geocaching.network.Send2CgeoDownloader; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.SettingsActivity; import cgeo.geocaching.sorting.CacheComparator; @@ -52,25 +54,33 @@ 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.CalendarUtils; import cgeo.geocaching.utils.CancellableHandler; -import cgeo.geocaching.utils.DateUtils; import cgeo.geocaching.utils.Log; - -import ch.boye.httpclientandroidlib.HttpResponse; +import cgeo.geocaching.utils.RxUtils; 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; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Scheduler.Worker; +import rx.Subscriber; import rx.Subscription; +import rx.functions.Action0; import rx.functions.Action1; +import rx.functions.Func1; import rx.schedulers.Schedulers; +import rx.subjects.ReplaySubject; +import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; import android.app.Activity; import android.app.AlertDialog; @@ -83,6 +93,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -104,21 +115,15 @@ import android.widget.TextView; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; public class CacheListActivity extends AbstractListActivity implements FilteredActivity, LoaderManager.LoaderCallbacks<SearchResult> { private static final int MAX_LIST_ITEMS = 1000; - private static final int MSG_DONE = -1; - private static final int MSG_SERVER_FAIL = -2; - private static final int MSG_NO_REGISTRATION = -3; - private static final int MSG_WAITING = 0; - private static final int MSG_LOADING = 1; - private static final int MSG_LOADED = 2; - private static final int REQUEST_CODE_IMPORT_GPX = 1; private CacheListType type = null; @@ -132,30 +137,28 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA private final Progress progress = new Progress(); private String title = ""; private int detailTotal = 0; - private int detailProgress = 0; + private final AtomicInteger detailProgress = new AtomicInteger(0); private long detailProgressTime = 0L; - private int listId = StoredList.TEMPORARY_LIST_ID; // Only meaningful for the OFFLINE type + private int listId = StoredList.TEMPORARY_LIST.id; // Only meaningful for the OFFLINE type private final GeoDirHandler geoDirHandler = new GeoDirHandler() { @Override public void updateDirection(final float direction) { if (Settings.isLiveList()) { - adapter.setActualHeading(DirectionProvider.getDirectionNow(direction)); + adapter.setActualHeading(AngleUtils.getDirectionNow(direction)); } } @Override - public void updateGeoData(final IGeoData geoData) { - final Geopoint coords = geoData.getCoords(); - if (coords != null) { - adapter.setActualCoordinates(coords); - } + public void updateGeoData(final GeoData geoData) { + adapter.setActualCoordinates(geoData.getCoords()); } }; private ContextMenuInfo lastMenuInfo; private String contextMenuGeocode = ""; private Subscription resumeSubscription; + private final ListNameMemento listNameMemento = new ListNameMemento(); // FIXME: This method has mostly been replaced by the loaders. But it still contains a license agreement check. public void handleCachesLoaded() { @@ -276,42 +279,49 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA refreshSpinnerAdapter(); } - private final CancellableHandler loadDetailsHandler = new CancellableHandler() { + private class LoadDetailsHandler extends CancellableHandler { @Override public void handleRegularMessage(final Message msg) { updateAdapter(); - if (msg.what > -1) { - cacheList.get(msg.what).setStatusChecked(false); + if (msg.what == DownloadProgress.MSG_LOADED) { + ((Geocache) msg.obj).setStatusChecked(false); adapter.notifyDataSetChanged(); + final int dp = detailProgress.get(); final int secondsElapsed = (int) ((System.currentTimeMillis() - detailProgressTime) / 1000); - final int minutesRemaining = ((detailTotal - detailProgress) * secondsElapsed / ((detailProgress > 0) ? detailProgress : 1) / 60); + final int minutesRemaining = ((detailTotal - dp) * secondsElapsed / ((dp > 0) ? dp : 1) / 60); - progress.setProgress(detailProgress); + progress.setProgress(dp); if (minutesRemaining < 1) { progress.setMessage(res.getString(R.string.caches_downloading) + " " + res.getString(R.string.caches_eta_ltm)); } else { progress.setMessage(res.getString(R.string.caches_downloading) + " " + res.getQuantityString(R.plurals.caches_eta_mins, minutesRemaining, minutesRemaining)); } } else { - if (search != null) { - final Set<Geocache> cacheListTmp = search.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB); - if (CollectionUtils.isNotEmpty(cacheListTmp)) { - cacheList.clear(); - cacheList.addAll(cacheListTmp); + new AsyncTask<Void, Void, Set<Geocache>>() { + @Override + protected Set<Geocache> doInBackground(final Void... params) { + return search != null ? search.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB) : null; } - } - setAdapterCurrentCoordinates(false); + @Override + protected void onPostExecute(final Set<Geocache> result) { + if (CollectionUtils.isNotEmpty(result)) { + cacheList.clear(); + cacheList.addAll(result); + } + setAdapterCurrentCoordinates(false); - showProgress(false); - progress.dismiss(); + showProgress(false); + progress.dismiss(); + } + }.execute(); } } - }; + } /** * TODO Possibly parts should be a Thread not a Handler @@ -324,22 +334,22 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA adapter.notifyDataSetChanged(); switch (msg.what) { - case MSG_WAITING: //no caches + case DownloadProgress.MSG_WAITING: //no caches progress.setMessage(res.getString(R.string.web_import_waiting)); break; - case MSG_LOADING: //cache downloading + case DownloadProgress.MSG_LOADING: //cache downloading progress.setMessage(res.getString(R.string.web_downloading) + " " + msg.obj + '…'); break; - case MSG_LOADED: //Cache downloaded + case DownloadProgress.MSG_LOADED: //Cache downloaded progress.setMessage(res.getString(R.string.web_downloaded) + " " + msg.obj + '…'); refreshCurrentList(); break; - case MSG_SERVER_FAIL: + case DownloadProgress.MSG_SERVER_FAIL: progress.dismiss(); showToast(res.getString(R.string.sendToCgeo_download_fail)); finish(); break; - case MSG_NO_REGISTRATION: + case DownloadProgress.MSG_NO_REGISTRATION: progress.dismiss(); showToast(res.getString(R.string.sendToCgeo_no_registration)); finish(); @@ -374,7 +384,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } }; private AbstractSearchLoader currentLoader; - private String newListName = StringUtils.EMPTY; public CacheListActivity() { super(true); @@ -392,8 +401,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 { @@ -405,6 +413,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA coords = Geopoint.ZERO; } } + if (type == CacheListType.NEAREST) { + coords = Sensors.getInstance().currentGeo().getCoords(); + } setTitle(title); @@ -513,7 +524,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA public void onResume() { super.onResume(); - resumeSubscription = geoDirHandler.start(GeoDirHandler.UPDATE_GEODATA | GeoDirHandler.UPDATE_DIRECTION); + resumeSubscription = geoDirHandler.start(GeoDirHandler.UPDATE_GEODATA | GeoDirHandler.UPDATE_DIRECTION | GeoDirHandler.LOW_POWER, 250, TimeUnit.MILLISECONDS); adapter.setSelectMode(false); setAdapterCurrentCoordinates(true); @@ -533,12 +544,9 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } private void setAdapterCurrentCoordinates(final boolean forceSort) { - final Geopoint coordsNow = app.currentGeo().getCoords(); - if (coordsNow != null) { - adapter.setActualCoordinates(coordsNow); - if (forceSort) { - adapter.forceSort(); - } + adapter.setActualCoordinates(Sensors.getInstance().currentGeo().getCoords()); + if (forceSort) { + adapter.forceSort(); } } @@ -552,8 +560,8 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.cache_list_options, menu); - CacheListAppFactory.addMenuItems(menu, this, res); sortProvider = (SortActionProvider) MenuItemCompat.getActionProvider(menu.findItem(R.id.menu_sort)); + assert sortProvider != null; // We set it in the XML file sortProvider.setSelection(adapter.getCacheComparator()); sortProvider.setClickListener(new Action1<CacheComparator>() { @@ -572,6 +580,15 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA sortProvider.setSelection(selectedComparator); } }); + + ListNavigationSelectionActionProvider.initialize(menu.findItem(R.id.menu_cache_list_app_provider), new ListNavigationSelectionActionProvider.Callback() { + + @Override + public void onListNavigationSelected(final CacheListApp app) { + app.invoke(cacheList, CacheListActivity.this, getFilteredSearch()); + } + }); + return true; } @@ -632,6 +649,11 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA setMenuItemLabel(menu, R.id.menu_remove_from_history, R.string.cache_remove_from_history, R.string.cache_clear_history); menu.findItem(R.id.menu_import_android).setVisible(Compatibility.isStorageAccessFrameworkAvailable() && isOffline); + + final List<CacheListApp> listNavigationApps = CacheListApps.getActiveApps(); + menu.findItem(R.id.menu_cache_list_app_provider).setVisible(listNavigationApps.size() > 1); + menu.findItem(R.id.menu_cache_list_app).setVisible(listNavigationApps.size() == 1); + } catch (final RuntimeException e) { Log.e("CacheListActivity.onPrepareOptionsMenu", e); } @@ -641,7 +663,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA private boolean containsPastEvents() { for (final Geocache cache : adapter.getCheckedOrAllCaches()) { - if (DateUtils.isPastEvent(cache)) { + if (CalendarUtils.isPastEvent(cache)) { return true; } } @@ -672,9 +694,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA @Override public boolean onOptionsItemSelected(final MenuItem item) { - if (super.onOptionsItemSelected(item)) { - return true; - } switch (item.getItemId()) { case R.id.menu_show_on_map: goMap(); @@ -698,23 +717,23 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA case R.id.menu_import_android: importGpxFromAndroid(); invalidateOptionsMenuCompatible(); - return false; + return true; case R.id.menu_create_list: - new StoredList.UserInterface(this).promptForListCreation(getListSwitchingRunnable(), newListName); + new StoredList.UserInterface(this).promptForListCreation(getListSwitchingRunnable(), listNameMemento.getTerm()); refreshSpinnerAdapter(); invalidateOptionsMenuCompatible(); - return false; + return true; case R.id.menu_drop_list: removeList(false); invalidateOptionsMenuCompatible(); - return false; + return true; case R.id.menu_rename_list: renameList(); - return false; + return true; case R.id.menu_invert_selection: adapter.invertSelection(); invalidateOptionsMenuCompatible(); - return false; + return true; case R.id.menu_filter: showFilterMenu(null); return true; @@ -730,7 +749,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA case R.id.menu_remove_from_history: removeFromHistoryCheck(); invalidateOptionsMenuCompatible(); - return false; + return true; case R.id.menu_move_to_list: moveCachesToOtherList(); invalidateOptionsMenuCompatible(); @@ -744,13 +763,12 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA invalidateOptionsMenuCompatible(); return true; case R.id.menu_cache_list_app: - if (!cacheToShow()) { - return false; + if (cacheToShow()) { + CacheListApps.getActiveApps().get(0).invoke(cacheList, this, getFilteredSearch()); } - return CacheListAppFactory.onMenuItemSelected(item, cacheList, this, getFilteredSearch()); - default: - return CacheListAppFactory.onMenuItemSelected(item, cacheList, this, search); + return true; } + return super.onOptionsItemSelected(item); } private boolean cacheToShow() { @@ -762,26 +780,28 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } private SearchResult getFilteredSearch() { - final Set<String> geocodes = new HashSet<>(); - for (final Geocache cache : adapter.getFilteredList()) { - geocodes.add(cache.getGeocode()); - } - return new SearchResult(geocodes); + return new SearchResult(Geocache.getGeocodes(adapter.getFilteredList())); } - public void deletePastEvents() { + private void deletePastEvents() { final List<Geocache> deletion = new ArrayList<>(); for (final Geocache cache : adapter.getCheckedOrAllCaches()) { - if (DateUtils.isPastEvent(cache)) { + if (CalendarUtils.isPastEvent(cache)) { deletion.add(cache); } } - new DropDetailsTask().execute(deletion.toArray(new Geocache[deletion.size()])); + new DropDetailsTask(0).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()); + clearOfflineLogs(clearOfflineLogsHandler, adapter.getCheckedOrAllCaches()); + } + }); } /** @@ -789,12 +809,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA */ @Override public void showFilterMenu(final View view) { - new FilterUserInterface(this).selectFilter(new Action1<IFilter>() { - @Override - public void call(@Nullable final IFilter selectedFilter) { - setFilter(selectedFilter); - } - }); + FilterActivity.selectFilter(this); } private void setComparator(final CacheComparator comparator) { @@ -886,13 +901,15 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA CacheDetailActivity.startActivity(this, cache.getGeocode(), cache.getName()); break; case R.id.menu_drop_cache: + final int lastListPosition = getListView().getFirstVisiblePosition(); cache.drop(new Handler() { @Override public void handleMessage(final Message msg) { adapter.notifyDataSetChanged(); refreshCurrentList(); + getListView().setSelection(lastListPosition); } - }, Schedulers.io()); + }); break; case R.id.menu_move_to_list: new StoredList.UserInterface(this).promptForListSelection(R.string.cache_menu_move_list, new Action1<Integer>() { @@ -903,7 +920,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA adapter.setSelectMode(false); refreshCurrentList(); } - }, true, listId, newListName); + }, true, listId, listNameMemento); break; case R.id.menu_store_cache: case R.id.menu_refresh: @@ -1037,8 +1054,14 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA new GPXImporter(this, listId, importGpxAttachementFinishedHandler).importGPX(uri, null, getDisplayName(uri)); } } + else if (requestCode == FilterActivity.REQUEST_SELECT_FILTER && resultCode == Activity.RESULT_OK) { + final int[] filterIndex = data.getIntArrayExtra(FilterActivity.EXTRA_FILTER_RESULT); + setFilter(FilterActivity.getFilterFromPosition(filterIndex[0], filterIndex[1])); + } - refreshCurrentList(); + if (type == CacheListType.OFFLINE) { + refreshCurrentList(); + } } private String getDisplayName(final Uri uri) { @@ -1062,7 +1085,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } - if (!Network.isNetworkConnected(getApplicationContext())) { + if (!Network.isNetworkConnected()) { showToast(getString(R.string.err_server)); return; } @@ -1079,11 +1102,11 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } refreshStoredInternal(caches); } - }, true, StoredList.TEMPORARY_LIST_ID, newListName); + }, true, StoredList.TEMPORARY_LIST.id, listNameMemento); } else { if (type != CacheListType.OFFLINE) { for (final Geocache geocache : caches) { - if (geocache.getListId() == StoredList.TEMPORARY_LIST_ID) { + if (geocache.getListId() == StoredList.TEMPORARY_LIST.id) { geocache.setListId(StoredList.STANDARD_LIST_ID); } } @@ -1093,25 +1116,25 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } private void refreshStoredInternal(final List<Geocache> caches) { - detailProgress = 0; + detailProgress.set(0); showProgress(false); final int etaTime = ((detailTotal * 25) / 60); - String message; + final String message; if (etaTime < 1) { message = res.getString(R.string.caches_downloading) + " " + res.getString(R.string.caches_eta_ltm); } else { message = res.getString(R.string.caches_downloading) + " " + res.getQuantityString(R.plurals.caches_eta_mins, etaTime, etaTime); } + final LoadDetailsHandler loadDetailsHandler = new LoadDetailsHandler(); progress.show(this, null, message, ProgressDialog.STYLE_HORIZONTAL, loadDetailsHandler.cancelMessage()); progress.setMaxProgressAndReset(detailTotal); detailProgressTime = System.currentTimeMillis(); - final LoadDetailsThread threadDetails = new LoadDetailsThread(loadDetailsHandler, caches); - threadDetails.start(); + loadDetails(loadDetailsHandler, caches); } public void removeFromHistoryCheck() { @@ -1126,18 +1149,17 @@ 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++) { geocodes[i] = caches.get(i).getGeocode(); } - final Bundle b = new Bundle(); - b.putStringArray(Intents.EXTRA_CACHELIST, geocodes); - getSupportLoaderManager().initLoader(CacheListLoaderType.REMOVE_FROM_HISTORY.getLoaderId(), b, this); + DataStore.clearVisitDate(geocodes); + refreshCurrentList(); } - 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() { @@ -1150,146 +1172,92 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return; } - detailProgress = 0; + detailProgress.set(0); showProgress(false); final DownloadFromWebHandler downloadFromWebHandler = new DownloadFromWebHandler(); progress.show(this, null, res.getString(R.string.web_import_waiting), true, downloadFromWebHandler.cancelMessage()); - - final LoadFromWebThread threadWeb = new LoadFromWebThread(downloadFromWebHandler, listId); - threadWeb.start(); + Send2CgeoDownloader.loadFromWeb(downloadFromWebHandler, listId); } - 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()); + final int count = adapter.getCheckedOrAllCount(); + final String message = res.getQuantityString(adapter.getCheckedCount() > 0 ? R.plurals.caches_remove_selected_confirm : R.plurals.caches_remove_all_confirm, count, count); Dialogs.confirmYesNo(this, titleId, message, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int id) { final List<Geocache> selected = adapter.getCheckedOrAllCaches(); - new DropDetailsTask().execute(selected.toArray(new Geocache[selected.size()])); + final int lastListPosition = getListView().getFirstVisiblePosition(); + new DropDetailsTask(lastListPosition).execute(selected.toArray(new Geocache[selected.size()])); dialog.cancel(); } }); } /** - * Thread to refresh the cache details. + * Method to asynchronously refresh the caches details. */ - private class LoadDetailsThread extends Thread { - - final private CancellableHandler handler; - final private List<Geocache> caches; - - public LoadDetailsThread(final CancellableHandler handler, final List<Geocache> caches) { - this.handler = handler; - this.caches = caches; - } - - @Override - public void run() { - // First refresh caches that do not yet have static maps to get them a chance to get a copy - // before the limit expires, unless we do not want to store offline maps. - final List<Geocache> allCaches = Settings.isStoreOfflineMaps() ? - ListUtils.union(ListUtils.selectRejected(caches, Geocache.hasStaticMap), - ListUtils.select(caches, Geocache.hasStaticMap)) : - caches; - - for (final Geocache cache : allCaches) { - if (!refreshCache(cache)) { - break; - } - } - - handler.sendEmptyMessage(MSG_DONE); - } - - /** - * Refreshes the cache information. - * - * @param cache - * The cache to refresh - * @return - * <code>false</code> if the storing was interrupted, <code>true</code> otherwise - */ - private boolean refreshCache(final Geocache cache) { - try { - if (handler.isCancelled()) { - throw new InterruptedException("Stopped storing process."); - } - detailProgress++; - cache.refreshSynchronous(null); - handler.sendEmptyMessage(cacheList.indexOf(cache)); - } catch (final InterruptedException e) { - Log.i(e.getMessage()); - return false; - } catch (final Exception e) { - Log.e("CacheListActivity.LoadDetailsThread", e); - } - - return true; - } - } - - private static class LoadFromWebThread extends Thread { - - final private CancellableHandler handler; - final private int listIdLFW; - - public LoadFromWebThread(final CancellableHandler handler, final int listId) { - this.handler = handler; - listIdLFW = StoredList.getConcreteList(listId); - } - - @Override - public void run() { - long baseTime = System.currentTimeMillis(); - - final String deviceCode = StringUtils.defaultString(Settings.getWebDeviceCode()); - final Parameters params = new Parameters("code", deviceCode); - while (!handler.isCancelled() && System.currentTimeMillis() - baseTime < 3 * 60000) { // maximum: 3 minutes - // Download new code - final HttpResponse responseFromWeb = Network.getRequest("http://send2.cgeo.org/read.html", params); - - if (responseFromWeb != null && responseFromWeb.getStatusLine().getStatusCode() == 200) { - final String response = Network.getResponseData(responseFromWeb); - if (response != null && response.length() > 2) { - handler.sendMessage(handler.obtainMessage(MSG_LOADING, response)); - - Geocache.storeCache(null, response, listIdLFW, false, null); - - handler.sendMessage(handler.obtainMessage(MSG_LOADED, response)); - baseTime = System.currentTimeMillis(); - } else if ("RG".equals(response)) { - //Server returned RG (registration) and this device no longer registered. - Settings.setWebNameCode(null, null); - handler.sendEmptyMessage(MSG_NO_REGISTRATION); - handler.cancel(); - break; - } else { - try { - sleep(5000); // Wait for 5s if no cache found - } catch (final InterruptedException e) { + private void loadDetails(final CancellableHandler handler, final List<Geocache> caches) { + final Observable<Geocache> allCaches; + final Subscription generator; + if (Settings.isStoreOfflineMaps()) { + // The list of caches will be generated in the background, putting the caches without static maps first. + final ReplaySubject<Geocache> withStaticMaps = ReplaySubject.create(caches.size()); + final ReplaySubject<Geocache> withoutStaticMaps = ReplaySubject.create(caches.size()); + final Worker worker = Schedulers.io().createWorker(); + generator = worker.schedule(new Action0() { + @Override + public void call() { + for (final Geocache cache : caches) { + if (worker.isUnsubscribed()) { + // Do not continue to check for static maps if the user pressed cancel. + return; + } + if (cache.hasStaticMap()) { + withStaticMaps.onNext(cache); + } else { + withoutStaticMaps.onNext(cache); } - handler.sendEmptyMessage(MSG_WAITING); } - } else { - handler.sendEmptyMessage(MSG_SERVER_FAIL); - handler.cancel(); - break; + withStaticMaps.onCompleted(); + withoutStaticMaps.onCompleted(); } - } - - handler.sendEmptyMessage(MSG_DONE); + }); + allCaches = Observable.concat(withoutStaticMaps, withStaticMaps); + } else { + allCaches = Observable.from(caches); + generator = Subscriptions.empty(); } + final Observable<Geocache> loaded = allCaches.flatMap(new Func1<Geocache, Observable<Geocache>>() { + @Override + public Observable<Geocache> call(final Geocache cache) { + return Observable.create(new OnSubscribe<Geocache>() { + @Override + public void call(final Subscriber<? super Geocache> subscriber) { + cache.refreshSynchronous(null); + detailProgress.incrementAndGet(); + handler.obtainMessage(DownloadProgress.MSG_LOADED, cache).sendToTarget(); + subscriber.onCompleted(); + } + }).subscribeOn(RxUtils.refreshScheduler); + } + }).doOnCompleted(new Action0() { + @Override + public void call() { + handler.sendEmptyMessage(DownloadProgress.MSG_DONE); + } + }); + handler.unsubscribeIfCancelled(new CompositeSubscription(generator, loaded.subscribe())); } private class DropDetailsTask extends AsyncTaskWithProgress<Geocache, Void> { + private final int lastListPosition; - public DropDetailsTask() { + public DropDetailsTask(final int lastListPosition) { super(CacheListActivity.this, null, res.getString(R.string.caches_remove_progress), true); + this.lastListPosition = lastListPosition; } @Override @@ -1303,25 +1271,19 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA adapter.setSelectMode(false); refreshCurrentList(); replaceCacheListFromSearch(); + getListView().setSelection(lastListPosition); } } - private class ClearOfflineLogsThread extends Thread { - - final private Handler handler; - final private List<Geocache> selected; - - public ClearOfflineLogsThread(final Handler handlerIn) { - handler = handlerIn; - selected = adapter.getCheckedOrAllCaches(); - } - - @Override - public void run() { - DataStore.clearLogsOffline(selected); - handler.sendEmptyMessage(MSG_DONE); - } + private static void clearOfflineLogs(final Handler handler, final List<Geocache> selectedCaches) { + Schedulers.io().createWorker().schedule(new Action0() { + @Override + public void call() { + DataStore.clearLogsOffline(selectedCaches); + handler.sendEmptyMessage(DownloadProgress.MSG_DONE); + } + }); } private class MoreCachesListener implements View.OnClickListener { @@ -1355,13 +1317,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; } @@ -1371,9 +1333,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA title = res.getString(R.string.list_all_lists); } else { final StoredList list = DataStore.getList(id); - if (list == null) { - return; - } listId = list.id; title = list.title; } @@ -1437,12 +1396,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA // apply filter settings (if there's a filter) final SearchResult searchToUse = getFilteredSearch(); - final int count = searchToUse.getCount(); - String mapTitle = title; - if (count > 0) { - mapTitle = title + " [" + count + "]"; - } - CGeoMap.startActivitySearch(this, searchToUse, mapTitle); + CGeoMap.startActivitySearch(this, searchToUse, title); } private void refreshCurrentList() { @@ -1452,7 +1406,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); } @@ -1461,7 +1415,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); } @@ -1479,7 +1433,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); } @@ -1500,37 +1454,39 @@ 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); } - public static void startActivityCoordinates(final AbstractActivity context, final Geopoint coords) { + /** + * start list activity, by searching around the given point. + * + * @param name + * name of coordinates, will lead to a title like "Around ..." instead of directly showing the + * coordinates as title + */ + public static void startActivityCoordinates(final AbstractActivity context, final Geopoint coords, @Nullable final String name) { if (!isValidCoords(context, coords)) { 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); + if (StringUtils.isNotEmpty(name)) { + cachesIntent.putExtra(Intents.EXTRA_TITLE, context.getString(R.string.around, name)); + } context.startActivity(cachesIntent); } @@ -1548,15 +1504,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); } @@ -1567,7 +1523,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); @@ -1592,7 +1548,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA } if (listId == PseudoList.ALL_LIST.id) { title = res.getString(R.string.list_all_lists); - } else if (listId <= StoredList.TEMPORARY_LIST_ID) { + } else if (listId <= StoredList.TEMPORARY_LIST.id) { listId = StoredList.STANDARD_LIST_ID; title = res.getString(R.string.stored_caches_button); } else { @@ -1605,7 +1561,7 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA title = list.title; } - loader = new OfflineGeocacheListLoader(this.getBaseContext(), coords, listId); + loader = new OfflineGeocacheListLoader(getBaseContext(), coords, listId); break; case HISTORY: @@ -1623,31 +1579,26 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA break; case KEYWORD: final String keyword = extras.getString(Intents.EXTRA_KEYWORD); - rememberTerm(keyword); + title = listNameMemento.rememberTerm(keyword); loader = new KeywordGeocacheListLoader(app, keyword); break; case ADDRESS: final String address = extras.getString(Intents.EXTRA_ADDRESS); if (StringUtils.isNotBlank(address)) { - rememberTerm(address); + title = listNameMemento.rememberTerm(address); } else { title = coords.toString(); } - if (coords != null) { - loader = new CoordsGeocacheListLoader(app, coords); - } - else { - loader = new AddressGeocacheListLoader(app, address); - } + loader = new CoordsGeocacheListLoader(app, coords); break; case FINDER: final String username = extras.getString(Intents.EXTRA_USERNAME); - rememberTerm(username); + title = listNameMemento.rememberTerm(username); loader = new FinderGeocacheListLoader(app, username); break; case OWNER: final String ownerName = extras.getString(Intents.EXTRA_USERNAME); - rememberTerm(ownerName); + title = listNameMemento.rememberTerm(ownerName); loader = new OwnerGeocacheListLoader(app, ownerName); break; case MAP: @@ -1657,10 +1608,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA replaceCacheListFromSearch(); loadCachesHandler.sendMessage(Message.obtain()); break; - case REMOVE_FROM_HISTORY: - title = res.getString(R.string.caches_history); - loader = new RemoveFromHistoryLoader(app, extras.getStringArray(Intents.EXTRA_CACHELIST), coords); - break; case NEXT_PAGE: loader = new NextPageGeocacheListLoader(app, search); break; @@ -1670,6 +1617,10 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA loader = new PocketGeocacheListLoader(app, guid); break; } + // if there is a title given in the activity start request, use this one instead of the default + if (extras != null && StringUtils.isNotBlank(extras.getString(Intents.EXTRA_TITLE))) { + title = extras.getString(Intents.EXTRA_TITLE); + } updateTitle(); showProgress(true); showFooterLoadingCaches(); @@ -1680,13 +1631,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA return loader; } - private void rememberTerm(final String term) { - // set the title of the activity - title = term; - // and remember this term for potential use in list creation - newListName = term; - } - @Override public void onLoadFinished(final Loader<SearchResult> arg0, final SearchResult searchIn) { // The database search was moved into the UI call intentionally. If this is done before the runOnUIThread, @@ -1716,8 +1660,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA /** * Allow the title bar spinner to show the same subtitle like the activity itself would show. * - * @param list - * @return */ public CharSequence getCacheListSubtitle(@NonNull final AbstractList list) { // if this is the current list, be aware of filtering @@ -1735,7 +1677,6 @@ public class CacheListActivity extends AbstractListActivity implements FilteredA /** * Calculate the subtitle of the current list depending on (optional) filters. * - * @return */ private CharSequence getCurrentSubtitle() { final ArrayList<String> numbers = new ArrayList<>(); diff --git a/main/src/cgeo/geocaching/CacheMenuHandler.java b/main/src/cgeo/geocaching/CacheMenuHandler.java index 0dc6444..fbd8771 100644 --- a/main/src/cgeo/geocaching/CacheMenuHandler.java +++ b/main/src/cgeo/geocaching/CacheMenuHandler.java @@ -3,8 +3,11 @@ package cgeo.geocaching; import cgeo.calendar.CalendarAddon; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; import cgeo.geocaching.apps.cache.navi.NavigationSelectionActionProvider; +import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.AbstractUIFactory; +import org.eclipse.jdt.annotation.NonNull; + import android.app.Activity; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; @@ -18,7 +21,11 @@ import android.view.MenuItem; * TODO: replace by a fragment * */ -public class CacheMenuHandler extends AbstractUIFactory { +public final class CacheMenuHandler extends AbstractUIFactory { + + private CacheMenuHandler() { + // utility class + } /** * Methods to be implemented by the activity to react to the cache menu selections. @@ -33,7 +40,7 @@ public class CacheMenuHandler extends AbstractUIFactory { } - public static boolean onMenuItemSelected(final MenuItem item, final CacheMenuHandler.ActivityInterface activityInterface, final Geocache cache) { + public static boolean onMenuItemSelected(final MenuItem item, @NonNull final CacheMenuHandler.ActivityInterface activityInterface, final Geocache cache) { assert activityInterface instanceof Activity || activityInterface instanceof Fragment; final Activity activity; if (activityInterface instanceof Activity) { @@ -85,7 +92,8 @@ public class CacheMenuHandler extends AbstractUIFactory { menu.findItem(R.id.menu_navigate).setVisible(hasCoords); menu.findItem(R.id.menu_caches_around).setVisible(hasCoords && cache.supportsCachesAround()); menu.findItem(R.id.menu_calendar).setVisible(cache.canBeAddedToCalendar()); - menu.findItem(R.id.menu_show_in_browser).setVisible(cache.canOpenInBrowser()); + menu.findItem(R.id.menu_log_visit).setVisible(cache.supportsLogging() && !Settings.getLogOffline()); + menu.findItem(R.id.menu_log_visit_offline).setVisible(cache.supportsLogging() && Settings.getLogOffline()); menu.findItem(R.id.menu_default_navigation).setTitle(NavigationAppFactory.getDefaultNavigationApplication().getName()); diff --git a/main/src/cgeo/geocaching/CachePopupFragment.java b/main/src/cgeo/geocaching/CachePopupFragment.java index b2af12c..ff4348a 100644 --- a/main/src/cgeo/geocaching/CachePopupFragment.java +++ b/main/src/cgeo/geocaching/CachePopupFragment.java @@ -2,8 +2,8 @@ package cgeo.geocaching; import cgeo.geocaching.activity.Progress; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.network.Network; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.CacheDetailsCreator; @@ -17,13 +17,12 @@ import rx.functions.Action0; import rx.functions.Action1; import rx.schedulers.Schedulers; -import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -135,20 +134,25 @@ public class CachePopupFragment extends AbstractDialogFragment { public void call(final Integer selectedListId) { storeCache(selectedListId); } - }, true, StoredList.TEMPORARY_LIST_ID); + }, true, StoredList.TEMPORARY_LIST.id); } else { - storeCache(StoredList.TEMPORARY_LIST_ID); + storeCache(StoredList.TEMPORARY_LIST.id); } } protected void storeCache(final int listId) { final StoreCacheHandler storeCacheHandler = new StoreCacheHandler(R.string.cache_dialog_offline_save_message); - progress.show(getActivity(), res.getString(R.string.cache_dialog_offline_save_title), res.getString(R.string.cache_dialog_offline_save_message), true, storeCacheHandler.cancelMessage()); - Schedulers.io().createWorker().schedule(new Action0() { + final FragmentActivity activity = getActivity(); + progress.show(activity, res.getString(R.string.cache_dialog_offline_save_title), res.getString(R.string.cache_dialog_offline_save_message), true, storeCacheHandler.cancelMessage()); + RxUtils.andThenOnUi(Schedulers.io(), new Action0() { @Override public void call() { cache.store(listId, storeCacheHandler); - getActivity().supportInvalidateOptionsMenu(); + } + }, new Action0() { + @Override + public void call() { + activity.supportInvalidateOptionsMenu(); } }); } @@ -162,7 +166,7 @@ public class CachePopupFragment extends AbstractDialogFragment { return; } - if (!Network.isNetworkConnected(getActivity())) { + if (!Network.isNetworkConnected()) { showToast(getString(R.string.err_server)); return; } @@ -183,7 +187,7 @@ public class CachePopupFragment extends AbstractDialogFragment { final DropCacheHandler dropCacheHandler = new DropCacheHandler(); progress.show(getActivity(), res.getString(R.string.cache_dialog_offline_drop_title), res.getString(R.string.cache_dialog_offline_drop_message), true, null); - cache.drop(dropCacheHandler, Schedulers.io()); + cache.drop(dropCacheHandler); } } @@ -212,12 +216,6 @@ public class CachePopupFragment extends AbstractDialogFragment { getActivity().finish(); } - public static void startActivity(final Context context, final String geocode) { - final Intent popupIntent = new Intent(context, CachePopup.class); - popupIntent.putExtra(Intents.EXTRA_GEOCODE, geocode); - context.startActivity(popupIntent); - } - @Override protected Geopoint getCoordinates() { if (cache == null) { diff --git a/main/src/cgeo/geocaching/CgeoApplication.java b/main/src/cgeo/geocaching/CgeoApplication.java index a81319d..d74acea 100644 --- a/main/src/cgeo/geocaching/CgeoApplication.java +++ b/main/src/cgeo/geocaching/CgeoApplication.java @@ -1,15 +1,15 @@ package cgeo.geocaching; -import cgeo.geocaching.sensors.DirectionProvider; -import cgeo.geocaching.sensors.GeoDataProvider; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.OOMDumpingUncaughtExceptionHandler; +import cgeo.geocaching.utils.RxUtils; -import rx.Observable; -import rx.functions.Action1; -import rx.observables.ConnectableObservable; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; + +import org.eclipse.jdt.annotation.NonNull; import android.app.Application; import android.view.ViewConfiguration; @@ -22,10 +22,7 @@ public class CgeoApplication extends Application { public boolean showLoginToast = true; //login toast shown just once. private boolean liveMapHintShownInThisSession = false; // livemap hint has been shown private static CgeoApplication instance; - private Observable<IGeoData> geoDataObservable; - private Observable<Float> directionObservable; - private volatile IGeoData currentGeo = null; - private volatile float currentDirection = 0.0f; + private boolean isGooglePlayServicesAvailable = false; public static void dumpOnOutOfMemory(final boolean enable) { @@ -45,7 +42,7 @@ public class CgeoApplication extends Application { setInstance(this); } - private static void setInstance(final CgeoApplication application) { + private static void setInstance(@NonNull final CgeoApplication application) { instance = application; } @@ -60,12 +57,7 @@ public class CgeoApplication extends Application { final Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); menuKeyField.setAccessible(true); menuKeyField.setBoolean(config, false); - } catch (final IllegalArgumentException e) { - // ignore - } catch (final NoSuchFieldException e) { - // ignore - } catch (final IllegalAccessException e) { - // ignore + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException ignored) { } // Set language to English if the user decided so. @@ -73,55 +65,32 @@ public class CgeoApplication extends Application { // ensure initialization of lists DataStore.getLists(); - } - - @Override - public void onLowMemory() { - Log.i("Cleaning applications cache."); - DataStore.removeAllFromCache(); - } - public synchronized Observable<IGeoData> geoDataObservable() { - if (geoDataObservable == null) { - final ConnectableObservable<IGeoData> onDemand = GeoDataProvider.create(this).replay(1); - onDemand.subscribe(new Action1<IGeoData>() { - @Override - public void call(final IGeoData geoData) { - currentGeo = geoData; - } - }); - geoDataObservable = onDemand.refCount(); + // Check if Google Play services is available + if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) { + isGooglePlayServicesAvailable = true; } - return geoDataObservable; - } + Log.i("Google Play services are " + (isGooglePlayServicesAvailable ? "" : "not ") + "available"); + final Sensors sensors = Sensors.getInstance(); + sensors.setupGeoDataObservables(Settings.useGooglePlayServices(), Settings.useLowPowerMode()); + sensors.setupDirectionObservable(Settings.useLowPowerMode()); - public synchronized Observable<Float> directionObservable() { - if (directionObservable == null) { - final ConnectableObservable<Float> onDemand = DirectionProvider.create(this).replay(1); - onDemand.subscribe(new Action1<Float>() { - @Override - public void call(final Float direction) { - currentDirection = direction; - } - }); - directionObservable = onDemand.refCount(); - } - return directionObservable; + // Attempt to acquire an initial location before any real activity happens. + sensors.geoDataObservable(true).subscribeOn(RxUtils.looperCallbacksScheduler).first().subscribe(); } - public IGeoData currentGeo() { - return currentGeo != null ? currentGeo : geoDataObservable().toBlocking().first(); - } - public Float distanceNonBlocking(final ICoordinates target) { - if (currentGeo == null || target.getCoords() == null) { - return null; - } - return currentGeo.getCoords().distanceTo(target); + @Override + public void onLowMemory() { + onTrimMemory(TRIM_MEMORY_COMPLETE); } - public float currentDirection() { - return currentDirection; + @Override + public void onTrimMemory(final int level) { + if (level >= TRIM_MEMORY_MODERATE) { + Log.i("Cleaning applications cache to trim memory"); + DataStore.removeAllFromCache(); + } } public boolean isLiveMapHintShownInThisSession() { @@ -150,4 +119,8 @@ public class CgeoApplication extends Application { forceRelog = true; } + public boolean isGooglePlayServicesAvailable() { + return isGooglePlayServicesAvailable; + } + } diff --git a/main/src/cgeo/geocaching/CompassActivity.java b/main/src/cgeo/geocaching/CompassActivity.java index 025d938..3f6fe6a 100644 --- a/main/src/cgeo/geocaching/CompassActivity.java +++ b/main/src/cgeo/geocaching/CompassActivity.java @@ -4,28 +4,35 @@ import butterknife.ButterKnife; import butterknife.InjectView; import cgeo.geocaching.activity.AbstractActionBarActivity; +import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.enumerations.LoadFlags; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; import cgeo.geocaching.maps.CGeoMap; -import cgeo.geocaching.sensors.DirectionProvider; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.GpsStatusProvider.Status; +import cgeo.geocaching.sensors.Sensors; 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; +import com.github.amlcurran.showcaseview.targets.ActionItemTarget; + import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; + import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.hardware.Sensor; -import android.hardware.SensorManager; import android.media.AudioManager; import android.os.Bundle; import android.view.Menu; @@ -34,14 +41,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; @@ -52,73 +55,70 @@ public class CompassActivity extends AbstractActionBarActivity { @InjectView(R.id.destination) protected TextView destinationTextView; @InjectView(R.id.cacheinfo) protected TextView cacheInfoView; - private static final String EXTRAS_COORDS = "coords"; - 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). + * Destination cache, may be null + */ + private Geocache cache = null; + /** + * Destination waypoint, may be null */ - private @Nullable Geocache cache = null; + private Waypoint waypoint = null; private Geopoint dstCoords = null; private float cacheHeading = 0; - private String title = null; - private String info = null; - private boolean hasMagneticFieldSensor; + private String description; @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.compass_activity); - - final SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); - hasMagneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null; - if (!hasMagneticFieldSensor) { - Settings.setUseCompass(false); - } + ButterKnife.inject(this); // get parameters final Bundle extras = getIntent().getExtras(); - if (extras != null) { - final String geocode = extras.getString(EXTRAS_GEOCODE); - if (StringUtils.isNotEmpty(geocode)) { - cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); - } - title = geocode; - final String name = extras.getString(EXTRAS_NAME); - dstCoords = extras.getParcelable(EXTRAS_COORDS); - info = extras.getString(EXTRAS_CACHE_INFO); - - if (StringUtils.isNotBlank(name)) { - if (StringUtils.isNotBlank(title)) { - title += ": " + name; - } else { - title = name; - } - } - } else { - final Intent pointIntent = new Intent(this, NavigateAnyPointActivity.class); - startActivity(pointIntent); - + if (extras == null) { finish(); return; } - // set header - setTitle(); - setDestCoords(); - setCacheInfo(); + // cache must exist, except for "any point navigation" + final String geocode = extras.getString(Intents.EXTRA_GEOCODE); + if (geocode != null) { + cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); + } - ButterKnife.inject(this); + // find the wanted navigation target + if (extras.containsKey(Intents.EXTRA_WAYPOINT_ID)) { + final int waypointId = extras.getInt(Intents.EXTRA_WAYPOINT_ID); + final Waypoint waypoint = DataStore.loadWaypoint(waypointId); + if (waypoint != null) { + setTarget(waypoint); + } + } + else if (extras.containsKey(Intents.EXTRA_COORDS)) { + setTarget(extras.<Geopoint>getParcelable(Intents.EXTRA_COORDS), extras.getString(Intents.EXTRA_DESCRIPTION)); + } + else { + setTarget(cache); + } + + // set activity title just once, independent of what target is switched to + if (cache != null) { + setCacheTitleBar(cache); + } + else { + setTitle(StringUtils.defaultIfBlank(extras.getString(Intents.EXTRA_NAME), res.getString(R.string.navigation))); + } // make sure we can control the TTS volume setVolumeControlStream(AudioManager.STREAM_MUSIC); + + presentShowcase(); } @Override public void onResume() { - super.onResume(geoDirHandler.start(GeoDirHandler.UPDATE_GEODIR)); + super.onResume(geoDirHandler.start(GeoDirHandler.UPDATE_GEODIR), + Sensors.getInstance().gpsStatusObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(gpsStatusHandler)); + forceRefresh(); } @Override @@ -134,38 +134,43 @@ public class CompassActivity extends AbstractActionBarActivity { setContentView(R.layout.compass_activity); ButterKnife.inject(this); + setTarget(dstCoords, description); - 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()); - } + final Sensors sensors = Sensors.getInstance(); + geoDirHandler.updateGeoDir(sensors.currentGeo(), sensors.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); - } + menu.findItem(R.id.menu_compass_sensor).setVisible(Sensors.getInstance().hasCompassCapabilities()); 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); @@ -177,6 +182,8 @@ public class CompassActivity extends AbstractActionBarActivity { } menu.findItem(R.id.menu_tts_start).setVisible(!SpeechService.isRunning()); menu.findItem(R.id.menu_tts_stop).setVisible(SpeechService.isRunning()); + menu.findItem(R.id.menu_compass_cache).setVisible(cache != null); + menu.findItem(R.id.menu_hint).setVisible(cache != null); return true; } @@ -185,7 +192,15 @@ public class CompassActivity extends AbstractActionBarActivity { final int id = item.getItemId(); switch (id) { case R.id.menu_map: - CGeoMap.startActivityCoords(this, dstCoords, null, null); + if (waypoint != null) { + CGeoMap.startActivityCoords(this, waypoint.getCoords(), waypoint.getWaypointType(), waypoint.getName()); + } + else if (cache != null) { + CGeoMap.startActivityGeoCode(this, cache.getGeocode()); + } + else { + CGeoMap.startActivityCoords(this, dstCoords, null, null); + } return true; case R.id.menu_compass_sensor_gps: Settings.setUseCompass(false); @@ -203,36 +218,53 @@ public class CompassActivity extends AbstractActionBarActivity { SpeechService.stopService(this); invalidateOptionsMenuCompatible(); return true; + case R.id.menu_compass_cache: + setTarget(cache); + return true; + case R.id.menu_hint: + cache.showHintToast(this); + return true; default: 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) { + setTarget(waypoint); + return true; + } } } return super.onOptionsItemSelected(item); } - private void setTitle() { - if (StringUtils.isNotBlank(title)) { - setTitle(title); - } else { - setTitle(res.getString(R.string.navigation)); - } + @Override + public ShowcaseViewBuilder getShowcase() { + return new ShowcaseViewBuilder(this) + .setTarget(new ActionItemTarget(this, R.id.menu_hint)) + .setContent(R.string.showcase_compass_hint_title, R.string.showcase_compass_hint_text); } - private void setDestCoords() { + private void setTarget(@NonNull final Geopoint coords, final String newDescription) { + setDestCoords(coords); + setTargetDescription(newDescription); + updateDistanceInfo(Sensors.getInstance().currentGeo()); + + Log.d("destination set: " + newDescription + " (" + dstCoords + ")"); + } + + private void setTarget(final @NonNull Waypoint waypointIn) { + waypoint = waypointIn; + setTarget(waypointIn.getCoords(), waypointIn.getName()); + } + + private void setTarget(final Geocache cache) { + setTarget(cache.getCoords(), Formatter.formatCacheInfoShort(cache)); + } + + private void setDestCoords(final Geopoint coords) { + dstCoords = coords; if (dstCoords == null) { return; } @@ -240,17 +272,18 @@ public class CompassActivity extends AbstractActionBarActivity { destinationTextView.setText(dstCoords.toString()); } - private void setCacheInfo() { - if (info == null) { + private void setTargetDescription(final @Nullable String newDescription) { + description = newDescription; + if (this.description == null) { cacheInfoView.setVisibility(View.GONE); return; } cacheInfoView.setVisibility(View.VISIBLE); - cacheInfoView.setText(info); + cacheInfoView.setText(this.description); } - private void updateDistanceInfo(final IGeoData geo) { - if (geo.getCoords() == null || dstCoords == null) { + private void updateDistanceInfo(final GeoData geo) { + if (dstCoords == null) { return; } @@ -259,36 +292,36 @@ public class CompassActivity extends AbstractActionBarActivity { headingView.setText(Math.round(cacheHeading) + "°"); } + private final Action1<Status> gpsStatusHandler = new Action1<Status>() { + @Override + public void call(final Status gpsStatus) { + if (gpsStatus.satellitesVisible >= 0) { + navSatellites.setText(res.getString(R.string.loc_sat) + ": " + gpsStatus.satellitesFixed + "/" + gpsStatus.satellitesVisible); + } else { + navSatellites.setText(""); + } + } + }; + private final GeoDirHandler geoDirHandler = new GeoDirHandler() { @Override - public void updateGeoDir(final IGeoData geo, final float dir) { + public void updateGeoDir(final GeoData geo, final float dir) { try { - if (geo.getCoords() != null) { - if (geo.getSatellitesVisible() >= 0) { - navSatellites.setText(res.getString(R.string.loc_sat) + ": " + geo.getSatellitesFixed() + "/" + geo.getSatellitesVisible()); - } else { - navSatellites.setText(""); - } - navType.setText(res.getString(geo.getLocationProvider().resourceId)); + navType.setText(res.getString(geo.getLocationProvider().resourceId)); - if (geo.getAccuracy() >= 0) { - navAccuracy.setText("±" + Units.getDistanceFromMeters(geo.getAccuracy())); - } else { - navAccuracy.setText(null); - } - - navLocation.setText(geo.getCoords().toString()); - - updateDistanceInfo(geo); + if (geo.getAccuracy() >= 0) { + navAccuracy.setText("±" + Units.getDistanceFromMeters(geo.getAccuracy())); } else { - navType.setText(null); navAccuracy.setText(null); - navLocation.setText(res.getString(R.string.loc_trying)); } - updateNorthHeading(DirectionProvider.getDirectionNow(dir)); + navLocation.setText(geo.getCoords().toString()); + + updateDistanceInfo(geo); + + updateNorthHeading(AngleUtils.getDirectionNow(dir)); } catch (final RuntimeException e) { - Log.w("Failed to LocationUpdater location."); + Log.w("Failed to update location", e); } } }; @@ -299,34 +332,24 @@ 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, - final String info) { - coordinates.clear(); - if (coordinatesWithType != null) { - for (final IWaypoint coordinate : coordinatesWithType) { - if (coordinate != null) { - coordinates.add(coordinate); - } - } - } - + public static void startActivityWaypoint(final Context context, final Waypoint waypoint) { final Intent navigateIntent = new Intent(context, CompassActivity.class); - navigateIntent.putExtra(EXTRAS_COORDS, coords); - navigateIntent.putExtra(EXTRAS_GEOCODE, geocode); - if (null != displayedName) { - navigateIntent.putExtra(EXTRAS_NAME, displayedName); - } - navigateIntent.putExtra(EXTRAS_CACHE_INFO, info); + navigateIntent.putExtra(Intents.EXTRA_GEOCODE, waypoint.getGeocode()); + navigateIntent.putExtra(Intents.EXTRA_WAYPOINT_ID, waypoint.getId()); 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 startActivityPoint(final Context context, final Geopoint coords, final String displayedName) { + final Intent navigateIntent = new Intent(context, CompassActivity.class); + navigateIntent.putExtra(Intents.EXTRA_COORDS, coords); + navigateIntent.putExtra(Intents.EXTRA_NAME, displayedName); + context.startActivity(navigateIntent); } - public static void startActivity(final Context context, final Geocache cache) { - startActivity(context, cache.getGeocode(), cache.getName(), cache.getCoords(), null, - Formatter.formatCacheInfoShort(cache)); + public static void startActivityCache(final Context context, final Geocache cache) { + final Intent navigateIntent = new Intent(context, CompassActivity.class); + navigateIntent.putExtra(Intents.EXTRA_GEOCODE, cache.getGeocode()); + context.startActivity(navigateIntent); } } diff --git a/main/src/cgeo/geocaching/CreateShortcutActivity.java b/main/src/cgeo/geocaching/CreateShortcutActivity.java index ffcf81b..ecb7dc4 100644 --- a/main/src/cgeo/geocaching/CreateShortcutActivity.java +++ b/main/src/cgeo/geocaching/CreateShortcutActivity.java @@ -3,17 +3,53 @@ 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 +59,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.list_title, R.drawable.main_stored, null); + shortcuts.add(offlineShortcut); + final Intent allIntent = new Intent(this, CacheListActivity.class); + allIntent.putExtra(Intents.EXTRA_LIST_ID, PseudoList.ALL_LIST.id); + shortcuts.add(new Shortcut(R.string.list_all_lists, R.drawable.main_stored, allIntent)); + 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); } - }, false, PseudoList.HISTORY_LIST.id); + }, true, PseudoList.NEW_LIST.id); } - protected Intent createShortcut(int listId) { + protected void createOfflineListShortcut(final int listId) { final StoredList list = DataStore.getList(listId); - if (list == null) { - return null; - } // 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 e404b22..b7ca577 100644 --- a/main/src/cgeo/geocaching/DataStore.java +++ b/main/src/cgeo/geocaching/DataStore.java @@ -11,29 +11,34 @@ import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.files.LocalStorage; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.list.AbstractList; import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.search.SearchSuggestionCursor; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.FileUtils; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.Version; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; -import rx.android.observables.AndroidObservable; +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.android.app.AppObservable; +import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func0; import rx.functions.Func1; import rx.schedulers.Schedulers; -import rx.util.async.Async; import android.app.Activity; import android.app.ProgressDialog; @@ -58,6 +63,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -68,6 +74,7 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; public class DataStore { @@ -111,35 +118,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; @@ -147,7 +151,7 @@ public class DataStore { /** * holds the column indexes of the cache table to avoid lookups */ - private static CacheCache cacheCache = new CacheCache(); + private static final CacheCache cacheCache = new CacheCache(); private static SQLiteDatabase database = null; private static final int dbVersion = 68; public static final int customListIdOffset = 10; @@ -162,7 +166,7 @@ public class DataStore { private static final @NonNull String dbTableLogImages = "cg_logImages"; private static final @NonNull String dbTableLogsOffline = "cg_logs_offline"; private static final @NonNull String dbTableTrackables = "cg_trackables"; - private static final @NonNull String dbTableSearchDestionationHistory = "cg_search_destination_history"; + private static final @NonNull String dbTableSearchDestinationHistory = "cg_search_destination_history"; private static final @NonNull String dbCreateCaches = "" + "create table " + dbTableCaches + " (" + "_id integer primary key autoincrement, " @@ -183,7 +187,6 @@ public class DataStore { + "size text, " + "difficulty float, " + "terrain float, " - + "latlon text, " + "location text, " + "direction double, " + "distance double, " @@ -214,9 +217,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 + " (" @@ -235,7 +236,6 @@ public class DataStore { + "prefix text, " + "lookup text, " + "name text, " - + "latlon text, " + "latitude double, " + "longitude double, " + "note text, " @@ -303,13 +303,23 @@ public class DataStore { + "); "; private static final String dbCreateSearchDestinationHistory = "" - + "create table " + dbTableSearchDestionationHistory + " (" + + "create table " + dbTableSearchDestinationHistory + " (" + "_id integer primary key autoincrement, " + "date long not null, " + "latitude double, " + "longitude double " + "); "; + private static final Observable<Integer> allCachesCountObservable = Observable.create(new OnSubscribe<Integer>() { + @Override + public void call(final Subscriber<? super Integer> subscriber) { + if (isInitialized()) { + subscriber.onNext(getAllCachesCount()); + subscriber.onCompleted(); + } + } + }).timeout(500, TimeUnit.MILLISECONDS).retry(10).subscribeOn(Schedulers.io()); + private static boolean newlyCreatedDatabase = false; private static boolean databaseCleaned = false; @@ -358,11 +368,12 @@ public class DataStore { } cacheCache.removeAllFromCache(); - PreparedStatements.clearPreparedStatements(); + PreparedStatement.clearPreparedStatements(); database.close(); database = null; } + @NonNull public static File getBackupFileInternal() { return new File(LocalStorage.getStorage(), "cgeo.sqlite"); } @@ -391,16 +402,15 @@ public class DataStore { * Move the database to/from external cgdata in a new thread, * showing a progress window * - * @param fromActivity */ public static void moveDatabase(final Activity fromActivity) { final ProgressDialog dialog = ProgressDialog.show(fromActivity, fromActivity.getString(R.string.init_dbmove_dbmove), fromActivity.getString(R.string.init_dbmove_running), true, false); - AndroidObservable.bindActivity(fromActivity, Async.fromCallable(new Func0<Boolean>() { + AppObservable.bindActivity(fromActivity, Observable.defer(new Func0<Observable<Boolean>>() { @Override - public Boolean call() { + public Observable<Boolean> call() { if (!LocalStorage.isExternalStorageAvailable()) { Log.w("Database was not moved: external memory not available"); - return false; + return Observable.just(false); } closeDb(); @@ -409,7 +419,7 @@ public class DataStore { if (!LocalStorage.copy(source, target)) { Log.e("Database could not be moved to " + target); init(); - return false; + return Observable.just(false); } if (!FileUtils.delete(source)) { Log.e("Original database could not be deleted during move"); @@ -418,7 +428,7 @@ public class DataStore { Log.i("Database was moved to " + target); init(); - return true; + return Observable.just(true); } })).subscribeOn(Schedulers.io()).subscribe(new Action1<Boolean>() { @Override @@ -430,14 +440,17 @@ public class DataStore { }); } + @NonNull private static File databasePath(final boolean internal) { return new File(internal ? LocalStorage.getInternalDbDirectory() : LocalStorage.getExternalDbDirectory(), dbName); } + @NonNull private static File databasePath() { return databasePath(!Settings.isDbOnSDCard()); } + @NonNull private static File databaseAlternatePath() { return databasePath(Settings.isDbOnSDCard()); } @@ -552,7 +565,7 @@ public class DataStore { try { db.execSQL(dbCreateSearchDestinationHistory); - Log.i("Added table " + dbTableSearchDestionationHistory + "."); + Log.i("Added table " + dbTableSearchDestinationHistory + "."); } catch (final Exception e) { Log.e("Failed to upgrade to ver. 52", e); } @@ -638,7 +651,6 @@ public class DataStore { + "size text, " + "difficulty float, " + "terrain float, " - + "latlon text, " + "location text, " + "direction double, " + "distance double, " @@ -665,7 +677,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); @@ -681,13 +693,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); @@ -823,7 +834,7 @@ public class DataStore { private static void sanityChecks(final SQLiteDatabase db) { // Check that the history of searches is well formed as some dates seem to be missing according // to NPE traces. - final int staleHistorySearches = db.delete(dbTableSearchDestionationHistory, "date is null", null); + final int staleHistorySearches = db.delete(dbTableSearchDestinationHistory, "date is null", null); if (staleHistorySearches > 0) { Log.w(String.format(Locale.getDefault(), "DataStore.dbHelper.onOpen: removed %d bad search history entries", staleHistorySearches)); } @@ -871,15 +882,17 @@ public class DataStore { final File[] files = LocalStorage.getStorage().listFiles(); if (ArrayUtils.isNotEmpty(files)) { final Pattern oldFilePattern = Pattern.compile("^[GC|TB|EC|GK|O][A-Z0-9]{4,7}$"); - final SQLiteStatement select = db.compileStatement("select count(*) from " + dbTableCaches + " where geocode = ?"); + final SQLiteStatement select = PreparedStatement.CHECK_IF_PRESENT.getStatement(); final ArrayList<File> toRemove = new ArrayList<>(files.length); for (final File file : files) { if (file.isDirectory()) { final String geocode = file.getName(); if (oldFilePattern.matcher(geocode).find()) { - select.bindString(1, geocode); - if (select.simpleQueryForLong() == 0) { - toRemove.add(file); + synchronized (select) { + select.bindString(1, geocode); + if (select.simpleQueryForLong() == 0) { + toRemove.add(file); + } } } } @@ -887,15 +900,15 @@ public class DataStore { // Use a background thread for the real removal to avoid keeping the database locked // if we are called from within a transaction. - new Thread(new Runnable() { + Schedulers.io().createWorker().schedule(new Action0() { @Override - public void run() { + public void call() { for (final File dir : toRemove) { Log.i("Removing obsolete cache directory for " + dir.getName()); - LocalStorage.deleteDirectory(dir); + FileUtils.deleteDirectory(dir); } } - }).start(); + }); } } @@ -933,7 +946,7 @@ public class DataStore { int dataDetailed = 0; try { - Cursor cursor; + final Cursor cursor; if (StringUtils.isNotBlank(geocode)) { cursor = database.query( @@ -1000,18 +1013,18 @@ public class DataStore { final SQLiteStatement listId; final String value; if (StringUtils.isNotBlank(geocode)) { - listId = PreparedStatements.getListIdOfGeocode(); + listId = PreparedStatement.LIST_ID_OF_GEOCODE.getStatement(); value = geocode; } else { - listId = PreparedStatements.getListIdOfGuid(); + listId = PreparedStatement.LIST_ID_OF_GUID.getStatement(); value = guid; } synchronized (listId) { listId.bindString(1, value); - return listId.simpleQueryForLong() != StoredList.TEMPORARY_LIST_ID; + return listId.simpleQueryForLong() != StoredList.TEMPORARY_LIST.id; } - } catch (final SQLiteDoneException e) { + } catch (final SQLiteDoneException ignored) { // Do nothing, it only means we have no information on the cache } catch (final Exception e) { Log.e("DataStore.isOffline", e); @@ -1020,6 +1033,7 @@ public class DataStore { return false; } + @Nullable public static String getGeocodeForGuid(final String guid) { if (StringUtils.isBlank(guid)) { return null; @@ -1027,12 +1041,12 @@ public class DataStore { init(); try { - final SQLiteStatement description = PreparedStatements.getGeocodeOfGuid(); + final SQLiteStatement description = PreparedStatement.GEOCODE_OF_GUID.getStatement(); synchronized (description) { description.bindString(1, guid); return description.simpleQueryForString(); } - } catch (final SQLiteDoneException e) { + } catch (final SQLiteDoneException ignored) { // Do nothing, it only means we have no information on the cache } catch (final Exception e) { Log.e("DataStore.getGeocodeForGuid", e); @@ -1041,36 +1055,14 @@ public class DataStore { return null; } - public static String getCacheidForGeocode(final String geocode) { - if (StringUtils.isBlank(geocode)) { - return null; - } - init(); - - try { - final SQLiteStatement description = PreparedStatements.getCacheIdOfGeocode(); - synchronized (description) { - description.bindString(1, geocode); - return description.simpleQueryForString(); - } - } catch (final SQLiteDoneException e) { - // Do nothing, it only means we have no information on the cache - } catch (final Exception e) { - Log.e("DataStore.getCacheidForGeocode", e); - } - - return null; - } - /** * Save/store a cache to the CacheCache * * @param cache * the Cache to save in the CacheCache/DB - * @param saveFlags * */ - public static void saveCache(final Geocache cache, final EnumSet<LoadFlags.SaveFlag> saveFlags) { + public static void saveCache(final Geocache cache, final Set<LoadFlags.SaveFlag> saveFlags) { saveCaches(Collections.singletonList(cache), saveFlags); } @@ -1079,10 +1071,9 @@ public class DataStore { * * @param caches * the caches to save in the CacheCache/DB - * @param saveFlags * */ - public static void saveCaches(final Collection<Geocache> caches, final EnumSet<LoadFlags.SaveFlag> saveFlags) { + public static void saveCaches(final Collection<Geocache> caches, final Set<LoadFlags.SaveFlag> saveFlags) { if (CollectionUtils.isEmpty(caches)) { return; } @@ -1116,7 +1107,9 @@ public class DataStore { for (final Geocache cache : caches) { final String geocode = cache.getGeocode(); final Geocache existingCache = existingCaches.get(geocode); - final boolean dbUpdateRequired = !cache.gatherMissingFrom(existingCache) || cacheCache.getCacheFromCache(geocode) != null; + boolean dbUpdateRequired = !cache.gatherMissingFrom(existingCache) || cacheCache.getCacheFromCache(geocode) != null; + // parse the note AFTER merging the local information in + dbUpdateRequired |= cache.parseWaypointsFromNote(); cache.addStorageLocation(StorageLocation.CACHE); cacheCache.putCacheInCache(cache); @@ -1162,7 +1155,7 @@ public class DataStore { values.put("hidden", hiddenDate.getTime()); } values.put("hint", cache.getHint()); - values.put("size", cache.getSize() == null ? "" : cache.getSize().id); + values.put("size", cache.getSize().id); values.put("difficulty", cache.getDifficulty()); values.put("terrain", cache.getTerrain()); values.put("location", cache.getLocation()); @@ -1228,7 +1221,7 @@ public class DataStore { if (attributes.isEmpty()) { return; } - final SQLiteStatement statement = PreparedStatements.getInsertAttribute(); + final SQLiteStatement statement = PreparedStatement.INSERT_ATTRIBUTE.getStatement(); final long timestamp = System.currentTimeMillis(); for (final String attribute : attributes) { statement.bindString(1, geocode); @@ -1249,9 +1242,12 @@ public class DataStore { init(); database.beginTransaction(); - try { - final SQLiteStatement insertDestination = PreparedStatements.getInsertSearchDestination(destination); + final SQLiteStatement insertDestination = PreparedStatement.INSERT_SEARCH_DESTINATION.getStatement(); + insertDestination.bindLong(1, destination.getDate()); + final Geopoint coords = destination.getCoords(); + insertDestination.bindDouble(2, coords.getLatitude()); + insertDestination.bindDouble(3, coords.getLongitude()); insertDestination.executeInsert(); database.setTransactionSuccessful(); } catch (final Exception e) { @@ -1264,7 +1260,6 @@ public class DataStore { public static boolean saveWaypoints(final Geocache cache) { init(); database.beginTransaction(); - try { saveWaypointsWithoutTransaction(cache); database.setTransactionSuccessful(); @@ -1294,7 +1289,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); @@ -1315,7 +1309,6 @@ public class DataStore { /** * remove all waypoints of the given cache, where the id is not in the given list * - * @param cache * @param remainingWaypointIds * ids of waypoints which shall not be deleted */ @@ -1329,7 +1322,7 @@ public class DataStore { * * @param values * a ContentValues to save coordinates in - * @param oneWaypoint + * @param coords * coordinates to save, or null to save empty coordinates */ private static void putCoords(final ContentValues values, final Geopoint coords) { @@ -1348,6 +1341,7 @@ public class DataStore { * index of the longitude column * @return the coordinates, or null if latitude or longitude is null or the coordinates are invalid */ + @Nullable private static Geopoint getCoords(final Cursor cursor, final int indexLat, final int indexLon) { if (cursor.isNull(indexLat) || cursor.isNull(indexLon)) { return null; @@ -1373,7 +1367,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); @@ -1410,7 +1403,7 @@ public class DataStore { final List<Image> spoilers = cache.getSpoilers(); if (CollectionUtils.isNotEmpty(spoilers)) { - final SQLiteStatement insertSpoiler = PreparedStatements.getInsertSpoiler(); + final SQLiteStatement insertSpoiler = PreparedStatement.INSERT_SPOILER.getStatement(); final long timestamp = System.currentTimeMillis(); for (final Image spoiler : spoilers) { insertSpoiler.bindString(1, geocode); @@ -1420,8 +1413,7 @@ public class DataStore { final String description = spoiler.getDescription(); if (description != null) { insertSpoiler.bindString(5, description); - } - else { + } else { insertSpoiler.bindNull(5); } insertSpoiler.executeInsert(); @@ -1429,11 +1421,21 @@ public class DataStore { } } - public static void saveLogsWithoutTransaction(final String geocode, final Iterable<LogEntry> logs) { + public static void saveLogs(final String geocode, final Iterable<LogEntry> logs) { + database.beginTransaction(); + try { + saveLogsWithoutTransaction(geocode, logs); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + + private static void saveLogsWithoutTransaction(final String geocode, final Iterable<LogEntry> logs) { // TODO delete logimages referring these logs database.delete(dbTableLogs, "geocode = ?", new String[]{geocode}); - final SQLiteStatement insertLog = PreparedStatements.getInsertLog(); + final SQLiteStatement insertLog = PreparedStatement.INSERT_LOG.getStatement(); final long timestamp = System.currentTimeMillis(); for (final LogEntry log : logs) { insertLog.bindString(1, geocode); @@ -1446,7 +1448,7 @@ public class DataStore { insertLog.bindLong(8, log.friend ? 1 : 0); final long logId = insertLog.executeInsert(); if (log.hasLogImages()) { - final SQLiteStatement insertImage = PreparedStatements.getInsertLogImage(); + final SQLiteStatement insertImage = PreparedStatement.INSERT_LOG_IMAGE.getStatement(); for (final Image img : log.getLogImages()) { insertImage.bindLong(1, logId); insertImage.bindString(2, img.getTitle()); @@ -1464,7 +1466,7 @@ public class DataStore { final Map<LogType, Integer> logCounts = cache.getLogCounts(); if (MapUtils.isNotEmpty(logCounts)) { final Set<Entry<LogType, Integer>> logCountsItems = logCounts.entrySet(); - final SQLiteStatement insertLogCounts = PreparedStatements.getInsertLogCounts(); + final SQLiteStatement insertLogCounts = PreparedStatement.INSERT_LOG_COUNTS.getStatement(); final long timestamp = System.currentTimeMillis(); for (final Entry<LogType, Integer> pair : logCountsItems) { insertLogCounts.bindString(1, geocode); @@ -1526,6 +1528,7 @@ public class DataStore { } } + @Nullable public static Viewport getBounds(final Set<String> geocodes) { if (CollectionUtils.isEmpty(geocodes)) { return null; @@ -1542,6 +1545,7 @@ public class DataStore { * The Geocode GCXXXX * @return the loaded cache (if found). Can be null */ + @Nullable public static Geocache loadCache(final String geocode, final EnumSet<LoadFlag> loadFlags) { if (StringUtils.isBlank(geocode)) { throw new IllegalArgumentException("geocode must not be empty"); @@ -1554,15 +1558,15 @@ public class DataStore { /** * Load caches. * - * @param geocodes * @return Set of loaded caches. Never null. */ + @NonNull public static Set<Geocache> loadCaches(final Collection<String> geocodes, final EnumSet<LoadFlag> loadFlags) { if (CollectionUtils.isEmpty(geocodes)) { 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)) { @@ -1609,10 +1613,9 @@ public class DataStore { /** * Load caches. * - * @param geocodes - * @param loadFlags * @return Set of loaded caches. Never null. */ + @NonNull private static Set<Geocache> loadCachesFromGeocodes(final Set<String> geocodes, final EnumSet<LoadFlag> loadFlags) { if (CollectionUtils.isEmpty(geocodes)) { return Collections.emptySet(); @@ -1640,7 +1643,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())); @@ -1699,11 +1702,9 @@ public class DataStore { /** * Builds a where for a viewport with the size enhanced by 50%. * - * @param dbTable - * @param viewport - * @return */ + @NonNull private static StringBuilder buildCoordinateWhere(final String dbTable, final Viewport viewport) { return viewport.resize(1.5).sqlWhere(dbTable); } @@ -1711,9 +1712,9 @@ public class DataStore { /** * creates a Cache from the cursor. Doesn't next. * - * @param cursor * @return Cache from DB */ + @NonNull private static Geocache createCacheFromDatabaseContent(final Cursor cursor) { final Geocache cache = new Geocache(); @@ -1750,31 +1751,32 @@ 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"); return cache; } + @Nullable public static List<String> loadAttributes(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -1792,6 +1794,7 @@ public class DataStore { GET_STRING_0); } + @Nullable public static Waypoint loadWaypoint(final int id) { if (id == 0) { return null; @@ -1818,6 +1821,7 @@ public class DataStore { return waypoint; } + @Nullable public static List<Waypoint> loadWaypoints(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -1840,6 +1844,7 @@ public class DataStore { }); } + @NonNull private static Waypoint createWaypointFromDatabaseContent(final Cursor cursor) { final String name = cursor.getString(cursor.getColumnIndex("name")); final WaypointType type = WaypointType.findById(cursor.getString(cursor.getColumnIndex("type"))); @@ -1850,13 +1855,13 @@ 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"))); return waypoint; } + @Nullable private static List<Image> loadSpoilers(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -1885,8 +1890,9 @@ public class DataStore { * * @return A list of previously entered destinations or an empty list. */ + @NonNull public static List<Destination> loadHistoryOfSearchedLocations() { - return queryToColl(dbTableSearchDestionationHistory, + return queryToColl(dbTableSearchDestinationHistory, new String[]{"_id", "date", "latitude", "longitude"}, "latitude IS NOT NULL AND longitude IS NOT NULL", null, @@ -1908,7 +1914,7 @@ public class DataStore { database.beginTransaction(); try { - database.delete(dbTableSearchDestionationHistory, null, null); + database.delete(dbTableSearchDestinationHistory, null, null); database.setTransactionSuccessful(); return true; } catch (final Exception e) { @@ -1921,7 +1927,6 @@ public class DataStore { } /** - * @param geocode * @return an immutable, non null list of logs */ @NonNull @@ -1935,7 +1940,7 @@ public class DataStore { init(); final Cursor cursor = database.rawQuery( - /* 0 1 2 3 4 5 6 7 8 9 10 */ + // 0 1 2 3 4 5 6 7 8 9 10 "SELECT cg_logs._id as cg_logs_id, type, author, log, date, found, friend, " + dbTableLogImages + "._id as cg_logImages_id, log_id, title, url" + " FROM " + dbTableLogs + " LEFT OUTER JOIN " + dbTableLogImages + " ON ( cg_logs._id = log_id ) WHERE geocode = ? ORDER BY date desc, cg_logs._id asc", new String[]{geocode}); @@ -1963,6 +1968,7 @@ public class DataStore { return Collections.unmodifiableList(logs); } + @Nullable public static Map<LogType, Integer> loadLogCounts(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -1970,7 +1976,7 @@ public class DataStore { init(); - final Map<LogType, Integer> logCounts = new HashMap<>(); + final Map<LogType, Integer> logCounts = new EnumMap<>(LogType.class); final Cursor cursor = database.query( dbTableLogCount, @@ -1991,6 +1997,7 @@ public class DataStore { return logCounts; } + @Nullable private static List<Trackable> loadInventory(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -2019,6 +2026,7 @@ public class DataStore { return trackables; } + @Nullable public static Trackable loadTrackable(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -2043,6 +2051,7 @@ public class DataStore { return trackable; } + @NonNull private static Trackable createTrackableFromDatabaseContent(final Cursor cursor) { final Trackable trackable = new Trackable(); trackable.setGeocode(cursor.getString(cursor.getColumnIndex("tbcode"))); @@ -2067,9 +2076,6 @@ public class DataStore { /** * Number of caches stored for a given type and/or list * - * @param cacheType - * @param list - * @return */ public static int getAllStoredCachesCount(final CacheType cacheType, final int list) { if (cacheType == null) { @@ -2081,37 +2087,29 @@ public class DataStore { init(); try { - final StringBuilder sql = new StringBuilder("select count(_id) from " + dbTableCaches + " where detailed = 1"); - String typeKey; - int reasonIndex; - if (cacheType != CacheType.ALL) { - sql.append(" and type = ?"); - typeKey = cacheType.id; - reasonIndex = 2; - } - else { - typeKey = "all_types"; - reasonIndex = 1; - } - String listKey; - if (list == PseudoList.ALL_LIST.id) { - sql.append(" and reason > 0"); - listKey = "all_list"; - } else { - sql.append(" and reason = ?"); - listKey = "list"; - } - - final String key = "CountCaches_" + typeKey + "_" + listKey; + final SQLiteStatement compiledStmnt; + synchronized (PreparedStatement.COUNT_TYPE_LIST) { + // All the statements here are used only once and are protected through the current synchronized block + if (list == PseudoList.ALL_LIST.id) { + if (cacheType == CacheType.ALL) { + compiledStmnt = PreparedStatement.COUNT_ALL_TYPES_ALL_LIST.getStatement(); + } else { + compiledStmnt = PreparedStatement.COUNT_TYPE_ALL_LIST.getStatement(); + compiledStmnt.bindString(1, cacheType.id); + } + } else { + if (cacheType == CacheType.ALL) { + compiledStmnt = PreparedStatement.COUNT_ALL_TYPES_LIST.getStatement(); + compiledStmnt.bindLong(1, list); + } else { + compiledStmnt = PreparedStatement.COUNT_TYPE_LIST.getStatement(); + compiledStmnt.bindString(1, cacheType.id); + compiledStmnt.bindLong(1, list); + } + } - final SQLiteStatement compiledStmnt = PreparedStatements.getStatement(key, sql.toString()); - if (cacheType != CacheType.ALL) { - compiledStmnt.bindString(1, cacheType.id); - } - if (list != PseudoList.ALL_LIST.id) { - compiledStmnt.bindLong(reasonIndex, list); + return (int) compiledStmnt.simpleQueryForLong(); } - return (int) compiledStmnt.simpleQueryForLong(); } catch (final Exception e) { Log.e("DataStore.loadAllStoredCachesCount", e); } @@ -2123,7 +2121,7 @@ public class DataStore { init(); try { - return (int) PreparedStatements.getCountHistoryCaches().simpleQueryForLong(); + return (int) PreparedStatement.HISTORY_COUNT.simpleQueryForLong(); } catch (final Exception e) { Log.e("DataStore.getAllHistoricCachesCount", e); } @@ -2131,6 +2129,7 @@ public class DataStore { return 0; } + @NonNull private static<T, U extends Collection<? super T>> U queryToColl(@NonNull final String table, final String[] columns, final String selection, @@ -2162,10 +2161,9 @@ public class DataStore { * * @param coords * the current coordinates to sort by distance, or null to sort by geocode - * @param cacheType - * @param listId * @return a non-null set of geocodes */ + @NonNull private static Set<String> loadBatchOfStoredGeocodes(final Geopoint coords, final CacheType cacheType, final int listId) { if (cacheType == null) { throw new IllegalArgumentException("cacheType must not be null"); @@ -2212,6 +2210,7 @@ public class DataStore { } } + @NonNull private static Set<String> loadBatchOfHistoricGeocodes(final boolean detailedOnly, final CacheType cacheType) { final StringBuilder selection = new StringBuilder("visiteddate > 0"); @@ -2243,11 +2242,13 @@ public class DataStore { } /** Retrieve all stored caches from DB */ + @NonNull public static SearchResult loadCachedInViewport(final Viewport viewport, final CacheType cacheType) { return loadInViewport(false, viewport, cacheType); } /** Retrieve stored caches from DB with listId >= 1 */ + @NonNull public static SearchResult loadStoredInViewport(final Viewport viewport, final CacheType cacheType) { return loadInViewport(true, viewport, cacheType); } @@ -2255,15 +2256,12 @@ public class DataStore { /** * Loads the geocodes of caches in a viewport from CacheCache and/or Database * - * @param stored - * True - query only stored caches, False - query cached ones as well - * @param centerLat - * @param centerLon - * @param spanLat - * @param spanLon - * @param cacheType - * @return Set with geocodes + * @param stored {@code true} to query caches stored in the database, {@code false} to also use the CacheCache + * @param viewport the viewport defining the area to scan + * @param cacheType the cache type + * @return the matching caches */ + @NonNull private static SearchResult loadInViewport(final boolean stored, final Viewport viewport, final CacheType cacheType) { final Set<String> geocodes = new HashSet<>(); @@ -2306,72 +2304,78 @@ public class DataStore { } /** - * Remove caches with listId = 0 - * - * @param more - * true = all caches false = caches stored 3 days or more before + * Remove caches with listId = 0 in the background. Once it has been executed once it will not do anything. + * This must be called from the UI thread to ensure synchronization of an internal variable. */ - public static void clean(final boolean more) { + public static void cleanIfNeeded(final Context context) { if (databaseCleaned) { return; } + databaseCleaned = true; - Log.d("Database clean: started"); + Schedulers.io().createWorker().schedule(new Action0() { + @Override + public void call() { + Log.d("Database clean: started"); + try { + final int version = Version.getVersionCode(context); + final Set<String> geocodes = new HashSet<>(); + if (version != Settings.getVersion()) { + queryToColl(dbTableCaches, + new String[]{"geocode"}, + "reason = 0", + null, + null, + null, + null, + null, + geocodes, + GET_STRING_0); + } else { + final long timestamp = System.currentTimeMillis() - DAYS_AFTER_CACHE_IS_DELETED; + final String timestampString = Long.toString(timestamp); + queryToColl(dbTableCaches, + new String[]{"geocode"}, + "reason = 0 and detailed < ? and detailedupdate < ? and visiteddate < ?", + new String[]{timestampString, timestampString, timestampString}, + null, + null, + null, + null, + geocodes, + GET_STRING_0); + } - try { - Set<String> geocodes = new HashSet<>(); - if (more) { - queryToColl(dbTableCaches, - new String[]{"geocode"}, - "reason = 0", - null, - null, - null, - null, - null, - geocodes, - GET_STRING_0); - } else { - final long timestamp = System.currentTimeMillis() - DAYS_AFTER_CACHE_IS_DELETED; - final String timestampString = Long.toString(timestamp); - queryToColl(dbTableCaches, - new String[]{"geocode"}, - "reason = 0 and detailed < ? and detailedupdate < ? and visiteddate < ?", - new String[]{timestampString, timestampString, timestampString}, - null, - null, - null, - null, - geocodes, - GET_STRING_0); - } + final Set<String> withoutOfflineLogs = exceptCachesWithOfflineLog(geocodes); + Log.d("Database clean: removing " + withoutOfflineLogs.size() + " geocaches from listId=0"); + removeCaches(withoutOfflineLogs, LoadFlags.REMOVE_ALL); - geocodes = exceptCachesWithOfflineLog(geocodes); + // This cleanup needs to be kept in place for about one year so that older log images records are + // cleaned. TO BE REMOVED AFTER 2015-03-24. + Log.d("Database clean: removing obsolete log images records"); + database.delete(dbTableLogImages, "log_id NOT IN (SELECT _id FROM " + dbTableLogs + ")", null); - if (!geocodes.isEmpty()) { - Log.d("Database clean: removing " + geocodes.size() + " geocaches from listId=0"); - removeCaches(geocodes, LoadFlags.REMOVE_ALL); - } + // Remove the obsolete "_others" directory where the user avatar used to be stored. + FileUtils.deleteDirectory(LocalStorage.getStorageDir("_others")); - // This cleanup needs to be kept in place for about one year so that older log images records are - // cleaned. TO BE REMOVED AFTER 2015-03-24. - Log.d("Database clean: removing obsolete log images records"); - database.delete(dbTableLogImages, "log_id NOT IN (SELECT _id FROM " + dbTableLogs + ")", null); - } catch (final Exception e) { - Log.w("DataStore.clean", e); - } + if (version > -1) { + Settings.setVersion(version); + } + } catch (final Exception e) { + Log.w("DataStore.clean", e); + } - Log.d("Database clean: finished"); - databaseCleaned = true; + Log.d("Database clean: finished"); + } + }); } /** * remove all geocodes from the given list of geocodes where an offline log exists * - * @param geocodes - * @return */ - private static Set<String> exceptCachesWithOfflineLog(final Set<String> geocodes) { + @NonNull + private static Set<String> exceptCachesWithOfflineLog(@NonNull final Set<String> geocodes) { if (geocodes.isEmpty()) { return geocodes; } @@ -2448,7 +2452,7 @@ public class DataStore { // Delete cache directories for (final String geocode : geocodes) { - LocalStorage.deleteDirectory(LocalStorage.getStorageDir(geocode)); + FileUtils.deleteDirectory(LocalStorage.getStorageDir(geocode)); } } } @@ -2480,6 +2484,7 @@ public class DataStore { return id != -1; } + @Nullable public static LogEntry loadLogOffline(final String geocode) { if (StringUtils.isBlank(geocode)) { return null; @@ -2528,13 +2533,11 @@ public class DataStore { init(); - final Set<String> geocodes = new HashSet<>(caches.size()); for (final Geocache cache : caches) { - geocodes.add(cache.getGeocode()); cache.setLogOffline(false); } - database.execSQL(String.format("DELETE FROM %s where %s", dbTableLogsOffline, whereGeocodeIn(geocodes))); + database.execSQL(String.format("DELETE FROM %s where %s", dbTableLogsOffline, whereGeocodeIn(Geocache.getGeocodes(caches)))); } public static boolean hasLogOffline(final String geocode) { @@ -2544,7 +2547,7 @@ public class DataStore { init(); try { - final SQLiteStatement logCount = PreparedStatements.getLogCountOfGeocode(); + final SQLiteStatement logCount = PreparedStatement.LOG_COUNT_OF_GEOCODE.getStatement(); synchronized (logCount) { logCount.bindString(1, geocode); return logCount.simpleQueryForLong() > 0; @@ -2565,8 +2568,7 @@ public class DataStore { database.beginTransaction(); try { - final SQLiteStatement setVisit = PreparedStatements.getUpdateVisitDate(); - + final SQLiteStatement setVisit = PreparedStatement.UPDATE_VISIT_DATE.getStatement(); for (final String geocode : geocodes) { setVisit.bindLong(1, visitedDate); setVisit.bindString(2, geocode); @@ -2584,7 +2586,7 @@ public class DataStore { final Resources res = CgeoApplication.getInstance().getResources(); final List<StoredList> lists = new ArrayList<>(); - lists.add(new StoredList(StoredList.STANDARD_LIST_ID, res.getString(R.string.list_inbox), (int) PreparedStatements.getCountCachesOnStandardList().simpleQueryForLong())); + lists.add(new StoredList(StoredList.STANDARD_LIST_ID, res.getString(R.string.list_inbox), (int) PreparedStatement.COUNT_CACHES_ON_STANDARD_LIST.simpleQueryForLong())); try { final String query = "SELECT l._id as _id, l.title as title, COUNT(c._id) as count" + @@ -2600,6 +2602,7 @@ public class DataStore { return lists; } + @NonNull private static ArrayList<StoredList> getListsFromCursor(final Cursor cursor) { final int indexId = cursor.getColumnIndex("_id"); final int indexTitle = cursor.getColumnIndex("title"); @@ -2613,6 +2616,7 @@ public class DataStore { }); } + @NonNull public static StoredList getList(final int id) { init(); if (id >= customListIdOffset) { @@ -2636,15 +2640,20 @@ public class DataStore { } // fall back to standard list in case of invalid list id - if (id == StoredList.STANDARD_LIST_ID || id >= customListIdOffset) { - return new StoredList(StoredList.STANDARD_LIST_ID, res.getString(R.string.list_inbox), (int) PreparedStatements.getCountCachesOnStandardList().simpleQueryForLong()); - } - - return null; + return new StoredList(StoredList.STANDARD_LIST_ID, res.getString(R.string.list_inbox), (int) PreparedStatement.COUNT_CACHES_ON_STANDARD_LIST.simpleQueryForLong()); } public static int getAllCachesCount() { - return (int) PreparedStatements.getCountAllCaches().simpleQueryForLong(); + return (int) PreparedStatement.COUNT_ALL_CACHES.simpleQueryForLong(); + } + + /** + * Count all caches in the background. + * + * @return an observable containing a unique element if the caches could be counted, or an error otherwise + */ + public static Observable<Integer> getAllCachesCountObservable() { + return allCachesCountObservable; } /** @@ -2710,7 +2719,6 @@ public class DataStore { /** * Remove a list. Caches in the list are moved to the standard list. * - * @param listId * @return true if the list got deleted, false else */ public static boolean removeList(final int listId) { @@ -2727,7 +2735,7 @@ public class DataStore { if (cnt > 0) { // move caches from deleted list to standard list - final SQLiteStatement moveToStandard = PreparedStatements.getMoveToStandardList(); + final SQLiteStatement moveToStandard = PreparedStatement.MOVE_TO_STANDARD_LIST.getStatement(); moveToStandard.bindLong(1, listId); moveToStandard.execute(); @@ -2759,7 +2767,7 @@ public class DataStore { } init(); - final SQLiteStatement move = PreparedStatements.getMoveToList(); + final SQLiteStatement move = PreparedStatement.MOVE_TO_LIST.getStatement(); database.beginTransaction(); try { @@ -2787,7 +2795,7 @@ public class DataStore { database.beginTransaction(); try { - database.delete(dbTableSearchDestionationHistory, "_id = " + destination.getId(), null); + database.delete(dbTableSearchDestinationHistory, "_id = " + destination.getId(), null); database.setTransactionSuccessful(); return true; } catch (final Exception e) { @@ -2802,9 +2810,8 @@ public class DataStore { /** * Load the lazily initialized fields of a cache and return them as partial cache (all other fields unset). * - * @param geocode - * @return */ + @NonNull public static Geocache loadCacheTexts(final String geocode) { final Geocache partial = new Geocache(); @@ -2835,7 +2842,7 @@ public class DataStore { } cursor.close(); - } catch (final SQLiteDoneException e) { + } catch (final SQLiteDoneException ignored) { // Do nothing, it only means we have no information on the cache } catch (final Exception e) { Log.e("DataStore.getCacheDescription", e); @@ -2862,11 +2869,12 @@ public class DataStore { * Creates the WHERE clause for matching multiple geocodes. This automatically converts all given codes to * UPPERCASE. */ + @NonNull private static StringBuilder whereGeocodeIn(final Collection<String> geocodes) { 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; } @@ -2878,12 +2886,9 @@ public class DataStore { /** * Loads all Waypoints in the coordinate rectangle. * - * @param excludeDisabled - * @param excludeMine - * @param type - * @return */ + @NonNull public static Set<Waypoint> loadWaypoints(final Viewport viewport, final boolean excludeMine, final boolean excludeDisabled, final CacheType type) { final StringBuilder where = buildCoordinateWhere(dbTableWaypoints, viewport); if (excludeMine) { @@ -2914,103 +2919,71 @@ public class DataStore { } public static void saveChangedCache(final Geocache cache) { - DataStore.saveCache(cache, cache.getStorageLocation().contains(StorageLocation.DATABASE) ? LoadFlags.SAVE_ALL : EnumSet.of(SaveFlag.CACHE)); + DataStore.saveCache(cache, cache.inDatabase() ? LoadFlags.SAVE_ALL : EnumSet.of(SaveFlag.CACHE)); } - private static class PreparedStatements { - - private static HashMap<String, SQLiteStatement> statements = new HashMap<>(); - - public static SQLiteStatement getMoveToStandardList() { - return getStatement("MoveToStandardList", "UPDATE " + dbTableCaches + " SET reason = " + StoredList.STANDARD_LIST_ID + " WHERE reason = ?"); - } + private static enum PreparedStatement { - public static SQLiteStatement getMoveToList() { - return getStatement("MoveToList", "UPDATE " + dbTableCaches + " SET reason = ? WHERE geocode = ?"); - } + HISTORY_COUNT("SELECT COUNT(_id) FROM " + dbTableCaches + " WHERE visiteddate > 0"), + MOVE_TO_STANDARD_LIST("UPDATE " + dbTableCaches + " SET reason = " + StoredList.STANDARD_LIST_ID + " WHERE reason = ?"), + MOVE_TO_LIST("UPDATE " + dbTableCaches + " SET reason = ? WHERE geocode = ?"), + UPDATE_VISIT_DATE("UPDATE " + dbTableCaches + " SET visiteddate = ? WHERE geocode = ?"), + INSERT_LOG_IMAGE("INSERT INTO " + dbTableLogImages + " (log_id, title, url) VALUES (?, ?, ?)"), + INSERT_LOG_COUNTS("INSERT INTO " + dbTableLogCount + " (geocode, updated, type, count) VALUES (?, ?, ?, ?)"), + INSERT_SPOILER("INSERT INTO " + dbTableSpoilers + " (geocode, updated, url, title, description) VALUES (?, ?, ?, ?, ?)"), + LOG_COUNT_OF_GEOCODE("SELECT count(_id) FROM " + DataStore.dbTableLogsOffline + " WHERE geocode = ?"), + COUNT_CACHES_ON_STANDARD_LIST("SELECT count(_id) FROM " + dbTableCaches + " WHERE reason = " + StoredList.STANDARD_LIST_ID), + COUNT_ALL_CACHES("SELECT count(_id) FROM " + dbTableCaches + " WHERE reason >= " + StoredList.STANDARD_LIST_ID), + INSERT_LOG("INSERT INTO " + dbTableLogs + " (geocode, updated, type, author, log, date, found, friend) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"), + INSERT_ATTRIBUTE("INSERT INTO " + dbTableAttributes + " (geocode, updated, attribute) VALUES (?, ?, ?)"), + LIST_ID_OF_GEOCODE("SELECT reason FROM " + dbTableCaches + " WHERE geocode = ?"), + LIST_ID_OF_GUID("SELECT reason FROM " + dbTableCaches + " WHERE guid = ?"), + GEOCODE_OF_GUID("SELECT geocode FROM " + dbTableCaches + " WHERE guid = ?"), + INSERT_SEARCH_DESTINATION("INSERT INTO " + dbTableSearchDestinationHistory + " (date, latitude, longitude) VALUES (?, ?, ?)"), + COUNT_TYPE_ALL_LIST("SELECT COUNT(_id) FROM " + dbTableCaches + " WHERE detailed = 1 AND type = ? AND reason > 0"), // See use of COUNT_TYPE_LIST for synchronization + COUNT_ALL_TYPES_ALL_LIST("SELECT COUNT(_id) FROM " + dbTableCaches + " WHERE detailed = 1 AND reason > 0"), // See use of COUNT_TYPE_LIST for synchronization + COUNT_TYPE_LIST("SELECT COUNT(_id) FROM " + dbTableCaches + " WHERE detailed = 1 AND type = ? AND reason = ?"), + COUNT_ALL_TYPES_LIST("SELECT COUNT(_id) FROM " + dbTableCaches + " WHERE detailed = 1 AND reason = ?"), // See use of COUNT_TYPE_LIST for synchronization + CHECK_IF_PRESENT("SELECT COUNT(*) FROM " + dbTableCaches + " WHERE geocode = ?"); - public static SQLiteStatement getUpdateVisitDate() { - return getStatement("UpdateVisitDate", "UPDATE " + dbTableCaches + " SET visiteddate = ? WHERE geocode = ?"); - } + private static final List<PreparedStatement> statements = new ArrayList<>(); - public static SQLiteStatement getInsertLogImage() { - return getStatement("InsertLogImage", "INSERT INTO " + dbTableLogImages + " (log_id, title, url) VALUES (?, ?, ?)"); - } + @Nullable + private volatile SQLiteStatement statement = null; // initialized lazily + final String query; - public static SQLiteStatement getInsertLogCounts() { - return getStatement("InsertLogCounts", "INSERT INTO " + dbTableLogCount + " (geocode, updated, type, count) VALUES (?, ?, ?, ?)"); + PreparedStatement(final String query) { + this.query = query; } - public static SQLiteStatement getInsertSpoiler() { - return getStatement("InsertSpoiler", "INSERT INTO " + dbTableSpoilers + " (geocode, updated, url, title, description) VALUES (?, ?, ?, ?, ?)"); + public long simpleQueryForLong() { + return getStatement().simpleQueryForLong(); } - public static SQLiteStatement getInsertSearchDestination(final Destination destination) { - final SQLiteStatement statement = getStatement("InsertSearch", "INSERT INTO " + dbTableSearchDestionationHistory + " (date, latitude, longitude) VALUES (?, ?, ?)"); - statement.bindLong(1, destination.getDate()); - final Geopoint coords = destination.getCoords(); - statement.bindDouble(2, coords.getLatitude()); - statement.bindDouble(3, coords.getLongitude()); + private SQLiteStatement getStatement() { + if (statement == null) { + synchronized (statements) { + if (statement == null) { + init(); + statement = database.compileStatement(query); + statements.add(this); + } + } + } return statement; } private static void clearPreparedStatements() { - for (final SQLiteStatement statement : statements.values()) { - statement.close(); + for (final PreparedStatement preparedStatement : statements) { + final SQLiteStatement statement = preparedStatement.statement; + if (statement != null) { + statement.close(); + preparedStatement.statement = null; + } } statements.clear(); } - private static synchronized SQLiteStatement getStatement(final String key, final String query) { - SQLiteStatement statement = statements.get(key); - if (statement == null) { - init(); - statement = database.compileStatement(query); - statements.put(key, statement); - } - return statement; - } - - public static SQLiteStatement getCountHistoryCaches() { - return getStatement("HistoryCount", "select count(_id) from " + dbTableCaches + " where visiteddate > 0"); - } - - private static SQLiteStatement getLogCountOfGeocode() { - return getStatement("LogCountFromGeocode", "SELECT count(_id) FROM " + DataStore.dbTableLogsOffline + " WHERE geocode = ?"); - } - - private static SQLiteStatement getCountCachesOnStandardList() { - return getStatement("CountStandardList", "SELECT count(_id) FROM " + dbTableCaches + " WHERE reason = " + StoredList.STANDARD_LIST_ID); - } - - private static SQLiteStatement getCountAllCaches() { - return getStatement("CountAllLists", "SELECT count(_id) FROM " + dbTableCaches + " WHERE reason >= " + StoredList.STANDARD_LIST_ID); - } - - private static SQLiteStatement getInsertLog() { - return getStatement("InsertLog", "INSERT INTO " + dbTableLogs + " (geocode, updated, type, author, log, date, found, friend) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); - } - - private static SQLiteStatement getInsertAttribute() { - return getStatement("InsertAttribute", "INSERT INTO " + dbTableAttributes + " (geocode, updated, attribute) VALUES (?, ?, ?)"); - } - - private static SQLiteStatement getListIdOfGeocode() { - return getStatement("listFromGeocode", "SELECT reason FROM " + dbTableCaches + " WHERE geocode = ?"); - } - - private static SQLiteStatement getListIdOfGuid() { - return getStatement("listFromGeocode", "SELECT reason FROM " + dbTableCaches + " WHERE guid = ?"); - } - - private static SQLiteStatement getCacheIdOfGeocode() { - return getStatement("cacheIdFromGeocode", "SELECT cacheid FROM " + dbTableCaches + " WHERE geocode = ?"); - } - - private static SQLiteStatement getGeocodeOfGuid() { - return getStatement("geocodeFromGuid", "SELECT geocode FROM " + dbTableCaches + " WHERE guid = ?"); - } - } public static void saveVisitDate(final String geocode) { @@ -3018,9 +2991,10 @@ public class DataStore { } public static void markDropped(final List<Geocache> caches) { - moveToList(caches, StoredList.TEMPORARY_LIST_ID); + moveToList(caches, StoredList.TEMPORARY_LIST.id); } + @Nullable public static Viewport getBounds(final String geocode) { if (geocode == null) { return null; @@ -3033,11 +3007,13 @@ public class DataStore { setVisitDate(Arrays.asList(selected), 0); } + @NonNull public static SearchResult getBatchOfStoredCaches(final Geopoint coords, final CacheType cacheType, final int listId) { final Set<String> geocodes = DataStore.loadBatchOfStoredGeocodes(coords, cacheType, listId); return new SearchResult(geocodes, DataStore.getAllStoredCachesCount(cacheType, listId)); } + @NonNull public static SearchResult getHistoryOfCaches(final boolean detailedOnly, final CacheType cacheType) { final Set<String> geocodes = DataStore.loadBatchOfHistoricGeocodes(detailedOnly, cacheType); return new SearchResult(geocodes, DataStore.getAllHistoryCachesCount()); @@ -3051,6 +3027,7 @@ public class DataStore { return false; } + @NonNull public static Set<String> getCachedMissingFromSearch(final SearchResult searchResult, final Set<Tile> tiles, final IConnector connector, final int maxZoom) { // get cached CacheListActivity @@ -3081,6 +3058,7 @@ public class DataStore { return missingFromSearch; } + @Nullable public static Cursor findSuggestions(final String searchTerm) { // require 3 characters, otherwise there are to many results if (StringUtils.length(searchTerm) < 3) { @@ -3116,6 +3094,7 @@ public class DataStore { cursor.close(); } + @NonNull private static String getSuggestionArgument(final String input) { return "%" + StringUtils.trim(input) + "%"; } @@ -3143,6 +3122,7 @@ public class DataStore { cursor.close(); } + @NonNull public static String[] getSuggestions(final String table, final String column, final String input) { try { final Cursor cursor = database.rawQuery("SELECT DISTINCT " + column @@ -3152,26 +3132,31 @@ public class DataStore { 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]; + return ArrayUtils.EMPTY_STRING_ARRAY; } } + @NonNull public static String[] getSuggestionsOwnerName(final String input) { return getSuggestions(dbTableCaches, "owner_real", input); } + @NonNull public static String[] getSuggestionsTrackableCode(final String input) { return getSuggestions(dbTableTrackables, "tbcode", input); } + @NonNull public static String[] getSuggestionsFinderName(final String input) { return getSuggestions(dbTableLogs, "author", input); } + @NonNull public static String[] getSuggestionsGeocode(final String input) { return getSuggestions(dbTableCaches, "geocode", input); } + @NonNull public static String[] getSuggestionsKeyword(final String input) { return getSuggestions(dbTableCaches, "name", input); } @@ -3180,6 +3165,7 @@ public class DataStore { * * @return list of last caches opened in the details view, ordered by most recent first */ + @NonNull public static ArrayList<Geocache> getLastOpenedCaches() { final List<String> geocodes = Settings.getLastOpenedCaches(); final Set<Geocache> cachesSet = DataStore.loadCaches(geocodes, LoadFlags.LOAD_CACHE_OR_DB); diff --git a/main/src/cgeo/geocaching/Destination.java b/main/src/cgeo/geocaching/Destination.java index 10d51be..1990d5d 100644 --- a/main/src/cgeo/geocaching/Destination.java +++ b/main/src/cgeo/geocaching/Destination.java @@ -1,6 +1,6 @@ package cgeo.geocaching; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; public final class Destination implements ICoordinates { diff --git a/main/src/cgeo/geocaching/EditWaypointActivity.java b/main/src/cgeo/geocaching/EditWaypointActivity.java index d49308e..ce279bf 100644 --- a/main/src/cgeo/geocaching/EditWaypointActivity.java +++ b/main/src/cgeo/geocaching/EditWaypointActivity.java @@ -7,11 +7,12 @@ import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.WaypointType; -import cgeo.geocaching.geopoint.DistanceParser; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; +import cgeo.geocaching.location.DistanceParser; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.dialog.CoordinatesInputDialog; import cgeo.geocaching.ui.dialog.Dialogs; @@ -66,11 +67,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C @ViewById(R.id.modify_cache_coordinates_local) protected RadioButton modifyLocal; @Extra(Intents.EXTRA_GEOCODE) protected String geocode = null; - @Extra(Intents.EXTRA_WAYPOINT_ID) protected int id = -1; - /** - * number of waypoints that the corresponding cache has until now - */ - @Extra(Intents.EXTRA_COUNT) protected int wpCount = 0; + @Extra(Intents.EXTRA_WAYPOINT_ID) protected int waypointId = -1; @InstanceState protected int waypointTypeSelectorPosition = -1; @@ -89,14 +86,14 @@ 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); - id = -1; + Log.d("No waypoint loaded to edit. id= " + waypointId); + waypointId = -1; } else { geocode = waypoint.getGeocode(); prefix = waypoint.getPrefix(); @@ -119,14 +116,23 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C } Dialogs.moveCursorToEnd(note); } - final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_ONLY); - setCoordsModificationVisibility(ConnectorFactory.getConnector(geocode), cache); + new AsyncTask<Void, Void, Geocache>() { + @Override + protected Geocache doInBackground(final Void... params) { + return DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_ONLY); + } + + @Override + protected void onPostExecute(final Geocache cache) { + setCoordsModificationVisibility(ConnectorFactory.getConnector(geocode), cache); + } + }.execute(); } if (own) { initializeWaypointTypeSelector(); } - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.e("EditWaypointActivity.loadWaypointHandler", e); } finally { if (waitDialog != null) { @@ -138,17 +144,17 @@ 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) { + if (StringUtils.isBlank(geocode) && waypointId <= 0) { showToast(res.getString(R.string.err_waypoint_cache_unknown)); finish(); return; } - if (id <= 0) { + if (waypointId <= 0) { setTitle(res.getString(R.string.waypoint_add_title)); } else { setTitle(res.getString(R.string.waypoint_edit_title)); @@ -159,11 +165,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) { @@ -174,7 +180,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); setCoordsModificationVisibility(ConnectorFactory.getConnector(geocode), cache); } - if (id > 0) { // existing waypoint + if (waypointId > 0) { // existing waypoint waitDialog = ProgressDialog.show(this, null, res.getString(R.string.waypoint_loading), true); waitDialog.setCancelable(true); @@ -189,7 +195,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 +211,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) { } }); @@ -249,17 +260,13 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C private void initializeDistanceUnitSelector() { distanceUnits = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.distance_units))); if (initViews) { - distanceUnitSelector.setSelection(Settings.isUseImperialUnits() ? 2 : 0); //0:m, 2:ft + distanceUnitSelector.setSelection(Settings.useImperialUnits() ? 2 : 0); //0:m, 2:ft } } final private GeoDirHandler geoDirHandler = new GeoDirHandler() { @Override - public void updateGeoData(final IGeoData geo) { - if (geo.getCoords() == null) { - return; - } - + public void updateGeoData(final GeoData geo) { try { buttonLat.setHint(geo.getCoords().format(GeopointFormatter.Format.LAT_DECMINUTE_RAW)); buttonLon.setHint(geo.getCoords().format(GeopointFormatter.Format.LON_DECMINUTE_RAW)); @@ -274,10 +281,10 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C @Override public void run() { try { - waypoint = DataStore.loadWaypoint(id); + waypoint = DataStore.loadWaypoint(waypointId); loadWaypointHandler.sendMessage(Message.obtain()); - } catch (Exception e) { + } catch (final Exception e) { Log.e("EditWaypointActivity.loadWaypoint.run", e); } } @@ -286,28 +293,71 @@ 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 ignored) { // button text is blank when creating new waypoint } - Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); - CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(cache, gp, app.currentGeo()); - coordsDialog.setCancelable(true); - coordsDialog.show(getSupportFragmentManager(),"wpeditdialog"); + final Geopoint geopoint = gp; + new AsyncTask<Void, Void, Geocache>() { + @Override + protected Geocache doInBackground(final Void... params) { + return DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); + } + + @Override + protected void onPostExecute(final Geocache cache) { + final CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(cache, geopoint, Sensors.getInstance().currentGeo()); + coordsDialog.setCancelable(true); + coordsDialog.show(getSupportFragmentManager(), "wpeditdialog"); + } + }.execute(); } } @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 + * + */ + 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, which is highest found index + 1 + int max = 0; + for (int i = 0; i < 30; i++) { + if (wpNames.contains(type.getL10n() + " " + i)) { + max = i; + } + } + return type.getL10n() + " " + (max + 1); + } + + 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 +368,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,17 +386,12 @@ 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; } } else { - final IGeoData geo = app.currentGeo(); - if (geo.getCoords() == null) { - showToast(res.getString(R.string.err_point_curr_position_unavailable)); - return; - } - coords = geo.getCoords(); + coords = Sensors.getInstance().currentGeo().getCoords(); } if (StringUtils.isNotBlank(bearingText) && StringUtils.isNotBlank(distanceText)) { @@ -354,7 +399,7 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C double bearing; try { bearing = Double.parseDouble(bearingText); - } catch (NumberFormatException e) { + } catch (final NumberFormatException ignored) { Dialogs.message(EditWaypointActivity.this, R.string.err_point_bear_and_dist_title, R.string.err_point_bear_and_dist); return; } @@ -362,8 +407,8 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C double distance; try { distance = DistanceParser.parseDistance(distanceText, - !Settings.isUseImperialUnits()); - } catch (NumberFormatException e) { + !Settings.useImperialUnits()); + } catch (final NumberFormatException ignored) { showToast(res.getString(R.string.err_parse_dist)); return; } @@ -371,25 +416,22 @@ 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.waypoint), getString(R.string.waypoint_being_saved), true); final Handler finishHandler = new Handler() { @Override - public void handleMessage(Message msg) { - // TODO: The order of showToast, progress.dismiss and finish is different in these cases. Why? + public void handleMessage(final Message msg) { switch (msg.what) { case UPLOAD_SUCCESS: - showToast(getString(R.string.waypoint_coordinates_has_been_modified_on_website, coordsToSave)); progress.dismiss(); finish(); + showToast(getString(R.string.waypoint_coordinates_has_been_modified_on_website, coordsToSave)); break; case SUCCESS: progress.dismiss(); @@ -422,7 +464,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); @@ -430,14 +472,14 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C waypoint.setCoords(coordsToSave); waypoint.setNote(noteText); waypoint.setVisited(visited); - waypoint.setId(id); + waypoint.setId(waypointId); - 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(waypointId); if (cache.addOrChangeWaypoint(waypoint, true)) { DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); if (!StaticMapsProvider.hasAllStaticMapsForWaypoint(geocode, waypoint)) { @@ -460,7 +502,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)); @@ -485,10 +527,10 @@ public class EditWaypointActivity extends AbstractActionBarActivity implements C } public static void startActivityEditWaypoint(final Context context, final Geocache cache, final int waypointId) { - EditWaypointActivity_.intent(context).geocode(cache.getGeocode()).id(waypointId).start(); + EditWaypointActivity_.intent(context).geocode(cache.getGeocode()).waypointId(waypointId).start(); } public static void startActivityAddWaypoint(final Context context, final Geocache cache) { - EditWaypointActivity_.intent(context).geocode(cache.getGeocode()).wpCount(cache.getWaypoints().size()).start(); + EditWaypointActivity_.intent(context).geocode(cache.getGeocode()).start(); } } diff --git a/main/src/cgeo/geocaching/Geocache.java b/main/src/cgeo/geocaching/Geocache.java index d9b2856..033196c 100644 --- a/main/src/cgeo/geocaching/Geocache.java +++ b/main/src/cgeo/geocaching/Geocache.java @@ -12,22 +12,20 @@ import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.GCConstants; import cgeo.geocaching.connector.gc.Tile; import cgeo.geocaching.connector.gc.UncertainProperty; -import cgeo.geocaching.enumerations.CacheAttribute; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; -import cgeo.geocaching.enumerations.LoadFlags.LoadFlag; import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.files.GPXParser; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.CalendarUtils; import cgeo.geocaching.utils.CancellableHandler; -import cgeo.geocaching.utils.DateUtils; import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.LazyInitializedList; import cgeo.geocaching.utils.Log; @@ -40,7 +38,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; -import org.apache.commons.collections4.Predicate; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -50,18 +47,17 @@ import org.eclipse.jdt.annotation.Nullable; import rx.Scheduler; import rx.Subscription; import rx.functions.Action0; +import rx.schedulers.Schedulers; 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; @@ -69,10 +65,9 @@ import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.EnumMap; import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -80,16 +75,18 @@ import java.util.Set; import java.util.regex.Pattern; /** - * Internal c:geo representation of a "cache" + * Internal representation of a "cache" */ -public class Geocache implements ICache, IWaypoint { +public class Geocache implements IWaypoint { private static final int OWN_WP_PREFIX_OFFSET = 17; private long updated = 0; private long detailedUpdate = 0; private long visitedDate = 0; - private int listId = StoredList.TEMPORARY_LIST_ID; + private int listId = StoredList.TEMPORARY_LIST.id; private boolean detailed = false; + + @NonNull private String geocode = ""; private String cacheId = ""; private String guid = ""; @@ -102,7 +99,7 @@ public class Geocache implements ICache, IWaypoint { * lazy initialized */ private String hint = null; - private CacheSize size = CacheSize.UNKNOWN; + @NonNull private CacheSize size = CacheSize.UNKNOWN; private float difficulty = 0; private float terrain = 0; private Float direction = null; @@ -137,19 +134,19 @@ public class Geocache implements ICache, IWaypoint { private final LazyInitializedList<String> attributes = new LazyInitializedList<String>() { @Override public List<String> call() { - return DataStore.loadAttributes(geocode); + return inDatabase() ? DataStore.loadAttributes(geocode) : new LinkedList<String>(); } }; private final LazyInitializedList<Waypoint> waypoints = new LazyInitializedList<Waypoint>() { @Override public List<Waypoint> call() { - return DataStore.loadWaypoints(geocode); + return inDatabase() ? DataStore.loadWaypoints(geocode) : new LinkedList<Waypoint>(); } }; private List<Image> spoilers = null; private List<Trackable> inventory = null; - private Map<LogType, Integer> logCounts = new HashMap<>(); + private Map<LogType, Integer> logCounts = new EnumMap<>(LogType.class); private boolean userModifiedCoords = false; // temporary values private boolean statusChecked = false; @@ -163,10 +160,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 String[] NO_EXTERNAL = new String[]{"geocheck.org"}; - /** * Create a new cache. To be used everywhere except for the GPX parser */ @@ -178,7 +171,7 @@ public class Geocache implements ICache, IWaypoint { * Cache constructor to be used by the GPX parser only. This constructor explicitly sets several members to empty * lists. * - * @param gpxParser + * @param gpxParser ignored parameter allowing to select this constructor */ public Geocache(final GPXParser gpxParser) { setReliableLatLon(true); @@ -220,7 +213,7 @@ public class Geocache implements ICache, IWaypoint { // if parsed cache is not yet detailed and stored is, the information of // the parsed cache will be overwritten if (!detailed && other.detailed) { - detailed = other.detailed; + detailed = true; detailedUpdate = other.detailedUpdate; // boolean values must be enumerated here. Other types are assigned outside this if-statement reliableLatLon = other.reliableLatLon; @@ -251,7 +244,7 @@ public class Geocache implements ICache, IWaypoint { if (visitedDate == 0) { visitedDate = other.visitedDate; } - if (listId == StoredList.TEMPORARY_LIST_ID) { + if (listId == StoredList.TEMPORARY_LIST.id) { listId = other.listId; } if (StringUtils.isBlank(geocode)) { @@ -279,7 +272,7 @@ public class Geocache implements ICache, IWaypoint { if (!detailed && StringUtils.isBlank(getHint())) { hint = other.getHint(); } - if (size == null || CacheSize.UNKNOWN == size) { + if (size == CacheSize.UNKNOWN) { size = other.size; } if (difficulty == 0) { @@ -455,11 +448,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) { @@ -491,11 +480,15 @@ public class Geocache implements ICache, IWaypoint { notifyChange(); } + @NonNull public List<LogType> getPossibleLogTypes() { return getConnector().getPossibleLogTypes(this); } public void openInBrowser(final Activity fromActivity) { + if (getUrl() == null) { + return; + } final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getLongUrl())); // Check if cgeo is the default, show the chooser to let the user choose a browser @@ -514,19 +507,11 @@ public class Geocache implements ICache, IWaypoint { } } - - private String getCacheUrl() { - return getConnector().getCacheUrl(this); - } - + @NonNull private IConnector getConnector() { return ConnectorFactory.getConnector(this); } - public boolean canOpenInBrowser() { - return getCacheUrl() != null; - } - public boolean supportsRefresh() { return getConnector() instanceof ISearchByGeocode; } @@ -551,11 +536,11 @@ public class Geocache implements ICache, IWaypoint { return getConnector().supportsOwnCoordinates(); } + @NonNull public ILoggingManager getLoggingManager(final LogCacheActivity activity) { return getConnector().getLoggingManager(activity, this); } - @Override public float getDifficulty() { return difficulty; } @@ -565,35 +550,30 @@ public class Geocache implements ICache, IWaypoint { return geocode; } - @Override + /** + * @return displayed owner, might differ from the real owner + */ public String getOwnerDisplayName() { return ownerDisplayName; } - @Override + @NonNull public CacheSize getSize() { - if (size == null) { - return CacheSize.UNKNOWN; - } return size; } - @Override public float getTerrain() { return terrain; } - @Override public boolean isArchived() { return BooleanUtils.isTrue(archived); } - @Override public boolean isDisabled() { return BooleanUtils.isTrue(disabled); } - @Override public boolean isPremiumMembersOnly() { return BooleanUtils.isTrue(premiumMembersOnly); } @@ -602,20 +582,27 @@ public class Geocache implements ICache, IWaypoint { this.premiumMembersOnly = members; } - @Override + /** + * + * @return {@code true} if the user is the owner of the cache, {@code false} otherwise + */ public boolean isOwner() { return getConnector().isOwner(this); } - @Override + /** + * @return GC username of the (actual) owner, might differ from the owner. Never empty. + */ + @NonNull public String getOwnerUserId() { return ownerUserId; } /** * Attention, calling this method may trigger a database access for the cache! + * + * @return the decrypted hint */ - @Override public String getHint() { initializeCacheTexts(); assertTextNotNull(hint, "Hint"); @@ -635,7 +622,6 @@ public class Geocache implements ICache, IWaypoint { /** * Attention, calling this method may trigger a database access for the cache! */ - @Override public String getDescription() { initializeCacheTexts(); assertTextNotNull(description, "Description"); @@ -647,18 +633,25 @@ public class Geocache implements ICache, IWaypoint { */ private void initializeCacheTexts() { if (description == null || shortdesc == null || hint == null || location == null) { - final Geocache partial = DataStore.loadCacheTexts(this.getGeocode()); - if (description == null) { - setDescription(partial.getDescription()); - } - if (shortdesc == null) { - setShortDescription(partial.getShortDescription()); - } - if (hint == null) { - setHint(partial.getHint()); - } - if (location == null) { - setLocation(partial.getLocation()); + if (inDatabase()) { + final Geocache partial = DataStore.loadCacheTexts(this.getGeocode()); + if (description == null) { + setDescription(partial.getDescription()); + } + if (shortdesc == null) { + setShortDescription(partial.getShortDescription()); + } + if (hint == null) { + setHint(partial.getHint()); + } + if (location == null) { + setLocation(partial.getLocation()); + } + } else { + description = StringUtils.defaultString(description); + shortdesc = StringUtils.defaultString(shortdesc); + hint = StringUtils.defaultString(hint); + location = StringUtils.defaultString(location); } } } @@ -666,7 +659,6 @@ public class Geocache implements ICache, IWaypoint { /** * Attention, calling this method may trigger a database access for the cache! */ - @Override public String getShortDescription() { initializeCacheTexts(); assertTextNotNull(shortdesc, "Short description"); @@ -678,7 +670,6 @@ public class Geocache implements ICache, IWaypoint { return name; } - @Override public String getCacheId() { if (StringUtils.isBlank(cacheId) && getConnector().equals(GCConnector.getInstance())) { return String.valueOf(GCConstants.gccodeToGCId(geocode)); @@ -687,7 +678,6 @@ public class Geocache implements ICache, IWaypoint { return cacheId; } - @Override public String getGuid() { return guid; } @@ -695,14 +685,12 @@ public class Geocache implements ICache, IWaypoint { /** * Attention, calling this method may trigger a database access for the cache! */ - @Override public String getLocation() { initializeCacheTexts(); assertTextNotNull(location, "Location"); return location; } - @Override public String getPersonalNote() { // non premium members have no personal notes, premium members have an empty string by default. // map both to null, so other code doesn't need to differentiate @@ -713,15 +701,13 @@ public class Geocache implements ICache, IWaypoint { return getConnector() instanceof ISearchByCenter; } - public void shareCache(final Activity fromActivity, final Resources res) { - if (geocode == null) { - return; - } - + public void shareCache(@NonNull final Activity fromActivity, final Resources res) { final Intent intent = getShareIntent(); fromActivity.startActivity(Intent.createChooser(intent, res.getText(R.string.cache_menu_share))); } + + @NonNull public Intent getShareIntent() { final StringBuilder subject = new StringBuilder("Geocache "); subject.append(geocode); @@ -732,20 +718,25 @@ public class Geocache implements ICache, IWaypoint { final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject.toString()); - intent.putExtra(Intent.EXTRA_TEXT, getUrl()); + intent.putExtra(Intent.EXTRA_TEXT, StringUtils.defaultString(getUrl())); return intent; } + @Nullable public String getUrl() { return getConnector().getCacheUrl(this); } + @Nullable public String getLongUrl() { return getConnector().getLongCacheUrl(this); } - public String getCgeoUrl() { return getConnector().getCacheUrl(this); } + @Nullable + public String getCgeoUrl() { + return getConnector().getCacheUrl(this); + } public boolean supportsGCVote() { return StringUtils.startsWithIgnoreCase(geocode, "GC"); @@ -755,12 +746,14 @@ public class Geocache implements ICache, IWaypoint { this.description = description; } - @Override public boolean isFound() { return BooleanUtils.isTrue(found); } - @Override + /** + * + * @return {@code true} if the user has put a favorite point onto this cache + */ public boolean isFavorite() { return BooleanUtils.isTrue(favorite); } @@ -769,18 +762,16 @@ public class Geocache implements ICache, IWaypoint { this.favorite = favorite; } - @Override @Nullable public Date getHiddenDate() { return hidden; } - @Override + @NonNull public List<String> getAttributes() { return attributes.getUnderlyingList(); } - @Override public List<Trackable> getInventory() { return inventory; } @@ -792,22 +783,25 @@ public class Geocache implements ICache, IWaypoint { spoilers.add(spoiler); } - @Override + @NonNull public List<Image> getSpoilers() { return ListUtils.unmodifiableList(ListUtils.emptyIfNull(spoilers)); } - @Override + /** + * @return a statistic how often the caches has been found, disabled, archived etc. + */ public Map<LogType, Integer> getLogCounts() { return logCounts; } - @Override public int getFavoritePoints() { return favoritePoints; } - @Override + /** + * @return the normalized cached name to be used for sorting, taking into account the numerical parts in the name + */ public String getNameForSorting() { if (null == nameForSorting) { nameForSorting = name; @@ -908,8 +902,6 @@ public class Geocache implements ICache, IWaypoint { /** * Set reliable coordinates - * - * @param coords */ public void setCoords(final Geopoint coords) { this.coords = new UncertainProperty<>(coords); @@ -917,9 +909,6 @@ public class Geocache implements ICache, IWaypoint { /** * Set unreliable coordinates from a certain map zoom level - * - * @param coords - * @param zoomlevel */ public void setCoords(final Geopoint coords, final int zoomlevel) { this.coords = new UncertainProperty<>(coords, zoomlevel); @@ -976,7 +965,9 @@ public class Geocache implements ICache, IWaypoint { this.inventoryItems = inventoryItems; } - @Override + /** + * @return {@code true} if the cache is on the user's watchlist, {@code false} otherwise + */ public boolean isOnWatchlist() { return BooleanUtils.isTrue(onWatchlist); } @@ -990,6 +981,7 @@ public class Geocache implements ICache, IWaypoint { * * @return always non <code>null</code> */ + @NonNull public List<Waypoint> getWaypoints() { return waypoints.getUnderlyingList(); } @@ -1028,7 +1020,7 @@ public class Geocache implements ICache, IWaypoint { */ @NonNull public List<LogEntry> getLogs() { - return DataStore.loadLogs(geocode); + return inDatabase() ? DataStore.loadLogs(geocode) : Collections.<LogEntry>emptyList(); } /** @@ -1069,7 +1061,7 @@ public class Geocache implements ICache, IWaypoint { this.directionImg = directionImg; } - public void setGeocode(final String geocode) { + public void setGeocode(@NonNull final String geocode) { this.geocode = StringUtils.upperCase(geocode); } @@ -1097,13 +1089,8 @@ public class Geocache implements ICache, IWaypoint { this.hint = hint; } - public void setSize(final CacheSize size) { - if (size == null) { - this.size = CacheSize.UNKNOWN; - } - else { - this.size = size; - } + public void setSize(@NonNull final CacheSize size) { + this.size = size; } public void setDifficulty(final float difficulty) { @@ -1160,7 +1147,6 @@ public class Geocache implements ICache, IWaypoint { * * @returns Never null */ - @Override public CacheType getType() { return cacheType.getValue(); } @@ -1203,6 +1189,13 @@ public class Geocache implements ICache, IWaypoint { } /** + * Check if this cache instance comes from or has been stored into the database. + */ + public boolean inDatabase() { + return storageLocation.contains(StorageLocation.DATABASE); + } + + /** * @param waypoint * Waypoint to add to the cache * @param saveToDatabase @@ -1214,7 +1207,9 @@ public class Geocache implements ICache, IWaypoint { waypoint.setGeocode(geocode); if (waypoint.getId() < 0) { // this is a new waypoint - assignUniquePrefix(waypoint); + if (StringUtils.isBlank(waypoint.getPrefix())) { + assignUniquePrefix(waypoint); + } waypoints.add(waypoint); if (waypoint.isFinalWithCoords()) { finalDefined = true; @@ -1249,13 +1244,14 @@ public class Geocache implements ICache, IWaypoint { } for (int i = OWN_WP_PREFIX_OFFSET; i < 100; i++) { - final String prefixCandidate = StringUtils.leftPad(String.valueOf(i), 2, '0'); + final String prefixCandidate = String.valueOf(i); if (!assignedPrefixes.contains(prefixCandidate)) { waypoint.setPrefix(prefixCandidate); - break; + return; } } + throw new IllegalStateException("too many waypoints, unable to assign unique prefix"); } public boolean hasWaypoints() { @@ -1341,8 +1337,6 @@ public class Geocache implements ICache, IWaypoint { /** * deletes any waypoint - * - * @param waypoint */ public void deleteWaypointForce(final Waypoint waypoint) { @@ -1389,12 +1383,15 @@ public class Geocache implements ICache, IWaypoint { /** * Detect coordinates in the personal note and convert them to user defined waypoints. Works by rule of thumb. */ - public void parseWaypointsFromNote() { + public boolean parseWaypointsFromNote() { + boolean changed = false; for (final Waypoint waypoint : Waypoint.parseWaypointsFromNote(StringUtils.defaultString(getPersonalNote()))) { if (!hasIdenticalWaypoint(waypoint.getCoords())) { addOrChangeWaypoint(waypoint, false); + changed = true; } } + return changed; } private boolean hasIdenticalWaypoint(final Geopoint point) { @@ -1432,7 +1429,7 @@ public class Geocache implements ICache, IWaypoint { } public void store(final CancellableHandler handler) { - store(StoredList.TEMPORARY_LIST_ID, handler); + store(StoredList.TEMPORARY_LIST.id, handler); } public void store(final int listId, final CancellableHandler handler) { @@ -1457,8 +1454,8 @@ public class Geocache implements ICache, IWaypoint { return "cache"; } - public Subscription drop(final Handler handler, final Scheduler scheduler) { - return scheduler.createWorker().schedule(new Action0() { + public Subscription drop(final Handler handler) { + return Schedulers.io().createWorker().schedule(new Action0() { @Override public void call() { try { @@ -1476,54 +1473,37 @@ public class Geocache implements ICache, IWaypoint { DataStore.removeCache(getGeocode(), EnumSet.of(RemoveFlag.CACHE)); } - public void checkFields() { - if (StringUtils.isBlank(getGeocode())) { - Log.w("geo code not parsed correctly for " + geocode); - } - if (StringUtils.isBlank(getName())) { - Log.w("name not parsed correctly for " + geocode); - } - if (StringUtils.isBlank(getGuid())) { - Log.w("guid not parsed correctly for " + geocode); - } - if (getTerrain() == 0.0) { - Log.w("terrain not parsed correctly for " + geocode); - } - if (getDifficulty() == 0.0) { - Log.w("difficulty not parsed correctly for " + geocode); - } - if (StringUtils.isBlank(getOwnerDisplayName())) { - Log.w("owner display name not parsed correctly for " + geocode); - } - if (StringUtils.isBlank(getOwnerUserId())) { - Log.w("owner user id real not parsed correctly for " + geocode); - } - if (getHiddenDate() == null) { - Log.w("hidden not parsed correctly for " + geocode); - } - if (getFavoritePoints() < 0) { - Log.w("favoriteCount not parsed correctly for " + geocode); - } - if (getSize() == null) { - Log.w("size not parsed correctly for " + geocode); - } - if (getType() == null || getType() == CacheType.UNKNOWN) { - Log.w("type not parsed correctly for " + geocode); - } - if (getCoords() == null) { - Log.w("coordinates not parsed correctly for " + geocode); - } - if (StringUtils.isBlank(getLocation())) { - Log.w("location not parsed correctly for " + geocode); + private void warnIncorrectParsingIf(final boolean incorrect, final String field) { + if (incorrect) { + Log.w(field + " not parsed correctly for " + geocode); } } + private void warnIncorrectParsingIfBlank(final String str, final String field) { + warnIncorrectParsingIf(StringUtils.isBlank(str), field); + } + + public void checkFields() { + warnIncorrectParsingIfBlank(getGeocode(), "geo"); + warnIncorrectParsingIfBlank(getName(), "name"); + warnIncorrectParsingIfBlank(getGuid(), "guid"); + warnIncorrectParsingIf(getTerrain() == 0.0, "terrain"); + warnIncorrectParsingIf(getDifficulty() == 0.0, "difficulty"); + warnIncorrectParsingIfBlank(getOwnerDisplayName(), "owner"); + warnIncorrectParsingIfBlank(getOwnerUserId(), "owner"); + warnIncorrectParsingIf(getHiddenDate() == null, "hidden"); + warnIncorrectParsingIf(getFavoritePoints() < 0, "favoriteCount"); + warnIncorrectParsingIf(getSize() == CacheSize.UNKNOWN, "size"); + warnIncorrectParsingIf(getType() == null || getType() == CacheType.UNKNOWN, "type"); + warnIncorrectParsingIf(getCoords() == null, "coordinates"); + warnIncorrectParsingIfBlank(getLocation(), "location"); + } + public Subscription refresh(final CancellableHandler handler, final Scheduler scheduler) { return scheduler.createWorker().schedule(new Action0() { @Override public void call() { refreshSynchronous(handler); - handler.sendEmptyMessage(CancellableHandler.DONE); } }); } @@ -1614,7 +1594,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); @@ -1627,7 +1607,7 @@ public class Geocache implements ICache, IWaypoint { return null; } - if (!forceReload && listId == StoredList.TEMPORARY_LIST_ID && (DataStore.isOffline(geocode, guid) || DataStore.isThere(geocode, guid, true, true))) { + if (!forceReload && listId == StoredList.TEMPORARY_LIST.id && (DataStore.isOffline(geocode, guid) || DataStore.isThere(geocode, guid, true, true))) { final SearchResult search = new SearchResult(); final String realGeocode = StringUtils.isNotBlank(geocode) ? geocode : DataStore.getGeocodeForGuid(guid); search.addGeocode(realGeocode); @@ -1635,7 +1615,7 @@ public class Geocache implements ICache, IWaypoint { } // if we have no geocode, we can't dynamically select the handler, but must explicitly use GC - if (geocode == null && guid != null) { + if (geocode == null) { return GCConnector.getInstance().searchByGeocode(null, guid, handler); } @@ -1655,9 +1635,9 @@ public class Geocache implements ICache, IWaypoint { * * @return start time in minutes after midnight */ - public String guessEventTimeMinutes() { + public int guessEventTimeMinutes() { if (!isEventCache()) { - return null; + return -1; } final String hourLocalized = CgeoApplication.getInstance().getString(R.string.cache_time_full_hours); @@ -1669,7 +1649,7 @@ public class Geocache implements ICache, IWaypoint { // 17 - 20 o'clock patterns.add(Pattern.compile("\\b(\\d{1,2})(?:\\.00)?" + "\\s*(?:-|[a-z]+)\\s*" + "(?:\\d{1,2})(?:\\.00)?" + "\\s+" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); // 12 o'clock, 12.00 o'clock - patterns.add(Pattern.compile("\\b(\\d{1,2})(?:\\.00)?\\s+" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); + patterns.add(Pattern.compile("\\b(\\d{1,2})(?:\\.(00|15|30|45))?\\s+" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE)); } final String searchText = getShortDescription() + ' ' + getDescription(); @@ -1679,64 +1659,25 @@ public class Geocache implements ICache, IWaypoint { try { final int hours = Integer.parseInt(matcher.group(1)); int minutes = 0; - if (matcher.groupCount() >= 2) { + if (matcher.groupCount() >= 2 && !StringUtils.isEmpty(matcher.group(2))) { minutes = Integer.parseInt(matcher.group(2)); } if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { - return String.valueOf(hours * 60 + minutes); + return hours * 60 + minutes; } - } catch (final NumberFormatException e) { + } catch (final NumberFormatException ignored) { // cannot happen, but static code analysis doesn't know } } } - return null; - } - - /** - * check whether the cache has a given attribute - * - * @param attribute - * @param yes - * true if we are looking for the attribute_yes version, false for the attribute_no version - * @return - */ - public boolean hasAttribute(final CacheAttribute attribute, final boolean yes) { - Geocache fullCache = DataStore.loadCache(getGeocode(), EnumSet.of(LoadFlag.ATTRIBUTES)); - if (fullCache == null) { - fullCache = this; - } - return fullCache.getAttributes().contains(attribute.getAttributeName(yes)); + return -1; } public boolean hasStaticMap() { return StaticMapsProvider.hasStaticMap(this); } - public static final Predicate<Geocache> hasStaticMap = new Predicate<Geocache>() { - @Override - public boolean evaluate(final Geocache cache) { - return cache.hasStaticMap(); - } - }; - - 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); - } - + @NonNull public Collection<Image> getImages() { final LinkedList<Image> result = new LinkedList<>(); result.addAll(getSpoilers()); @@ -1744,22 +1685,23 @@ 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, geocode, getShortDescription(), getDescription()); return result; } - // Add spoilers stored locally in /sdcard/GeocachePhotos + /** + * Add spoilers stored locally in <tt>/sdcard/GeocachePhotos</tt>. If a cache is named GC123ABC, the + * directory will be <tt>/sdcard/GeocachePhotos/C/B/GC123ABC/</tt>. + * + * @param spoilers the list to add to + */ private void addLocalSpoilersTo(final List<Image> spoilers) { if (StringUtils.length(geocode) >= 2) { final String suffix = StringUtils.right(geocode, 2); - final File baseDir = new File(Environment.getExternalStorageDirectory().toString(), "GeocachePhotos"); + final File baseDir = new File(Environment.getExternalStorageDirectory(), "GeocachePhotos"); final File lastCharDir = new File(baseDir, suffix.substring(1)); final File secondToLastCharDir = new File(lastCharDir, suffix.substring(0, 1)); - final File finalDir = new File(secondToLastCharDir, getGeocode()); + final File finalDir = new File(secondToLastCharDir, geocode); final File[] files = finalDir.listFiles(); if (files != null) { for (final File image : files) { @@ -1805,18 +1747,17 @@ public class Geocache implements ICache, IWaypoint { return getConnector().getWaypointGpxId(prefix, geocode); } + @NonNull public String getWaypointPrefix(final String name) { return getConnector().getWaypointPrefix(name); } /** * Get number of overall finds for a cache, or 0 if the number of finds is not known. - * - * @return */ public int getFindsCount() { if (getLogCounts().isEmpty()) { - setLogCounts(DataStore.loadLogCounts(getGeocode())); + setLogCounts(inDatabase() ? DataStore.loadLogCounts(getGeocode()) : Collections.<LogType, Integer>emptyMap()); } final Integer logged = getLogCounts().get(LogType.FOUND_IT); if (logged != null) { @@ -1829,12 +1770,13 @@ public class Geocache implements ICache, IWaypoint { return (getType().applyDistanceRule() || hasUserModifiedCoords()) && getConnector() == GCConnector.getInstance(); } + @NonNull public LogType getDefaultLogType() { if (isEventCache()) { final Date eventDate = getHiddenDate(); - final boolean expired = DateUtils.isPastEvent(this); + final boolean expired = CalendarUtils.isPastEvent(this); - if (hasOwnLog(LogType.WILL_ATTEND) || expired || (eventDate != null && DateUtils.daysSince(eventDate.getTime()) == 0)) { + if (hasOwnLog(LogType.WILL_ATTEND) || expired || (eventDate != null && CalendarUtils.daysSince(eventDate.getTime()) == 0)) { return hasOwnLog(LogType.ATTENDED) ? LogType.NOTE : LogType.ATTENDED; } return LogType.WILL_ATTEND; @@ -1848,4 +1790,30 @@ public class Geocache implements ICache, IWaypoint { return LogType.FOUND_IT; } + /** + * Get the geocodes of a collection of caches. + * + * @param caches a collection of caches + * @return the non-blank geocodes of the caches + */ + @NonNull + public static Set<String> getGeocodes(@NonNull final Collection<Geocache> caches) { + final Set<String> geocodes = new HashSet<>(caches.size()); + for (final Geocache cache : caches) { + final String geocode = cache.getGeocode(); + if (StringUtils.isNotBlank(geocode)) { + geocodes.add(geocode); + } + } + return geocodes; + } + + /** + * Show the hint as toast message. If no hint is available, a default "no hint available" will be shown instead. + */ + public void showHintToast(final Activity activity) { + final String hint = getHint(); + ActivityMixin.showToast(activity, StringUtils.defaultIfBlank(hint, activity.getString(R.string.cache_hint_not_available))); + } + } diff --git a/main/src/cgeo/geocaching/GpxFileListActivity.java b/main/src/cgeo/geocaching/GpxFileListActivity.java index dae52c4..352dbab 100644 --- a/main/src/cgeo/geocaching/GpxFileListActivity.java +++ b/main/src/cgeo/geocaching/GpxFileListActivity.java @@ -1,63 +1,64 @@ -package cgeo.geocaching;
-
-import cgeo.geocaching.connector.ConnectorFactory;
-import cgeo.geocaching.connector.IConnector;
-import cgeo.geocaching.files.AbstractFileListActivity;
-import cgeo.geocaching.files.GPXImporter;
-import cgeo.geocaching.list.StoredList;
-import cgeo.geocaching.settings.Settings;
-import cgeo.geocaching.ui.GPXListAdapter;
-
-import org.apache.commons.lang3.StringUtils;
-
-import android.app.Activity;
-import android.content.Intent;
-
-import java.io.File;
-import java.util.Collections;
-import java.util.List;
-
-public class GpxFileListActivity extends AbstractFileListActivity<GPXListAdapter> {
-
- public GpxFileListActivity() {
- super(new String[] { "gpx", "loc", "zip" });
- }
-
- @Override
- protected GPXListAdapter getAdapter(List<File> files) {
- return new GPXListAdapter(this, files);
- }
-
- @Override
- protected List<File> getBaseFolders() {
- return Collections.singletonList(new File(Settings.getGpxImportDir()));
- }
-
- public static void startSubActivity(Activity fromActivity, int listId) {
- final Intent intent = new Intent(fromActivity, GpxFileListActivity.class);
- intent.putExtra(Intents.EXTRA_LIST_ID, StoredList.getConcreteList(listId));
- fromActivity.startActivityForResult(intent, 0);
- }
-
- @Override
- protected boolean filenameBelongsToList(final String filename) {
- if (super.filenameBelongsToList(filename)) {
- if (StringUtils.endsWithIgnoreCase(filename, GPXImporter.ZIP_FILE_EXTENSION)) {
- for (IConnector connector : ConnectorFactory.getConnectors()) {
- if (connector.isZippedGPXFile(filename)) {
- return true;
- }
- }
- return false;
- }
- // filter out waypoint files
- return !StringUtils.containsIgnoreCase(filename, GPXImporter.WAYPOINTS_FILE_SUFFIX);
- }
- return false;
- }
-
- public int getListId() {
- return listId;
- }
-
-}
+package cgeo.geocaching; + +import cgeo.geocaching.connector.ConnectorFactory; +import cgeo.geocaching.connector.IConnector; +import cgeo.geocaching.files.AbstractFileListActivity; +import cgeo.geocaching.files.GPXImporter; +import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.ui.GPXListAdapter; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; + +import android.app.Activity; +import android.content.Intent; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +public class GpxFileListActivity extends AbstractFileListActivity<GPXListAdapter> { + + public GpxFileListActivity() { + super(new String[] { "gpx", "loc", "zip" }); + } + + @Override + protected GPXListAdapter getAdapter(final List<File> files) { + return new GPXListAdapter(this, files); + } + + @Override + protected List<File> getBaseFolders() { + return Collections.singletonList(new File(Settings.getGpxImportDir())); + } + + public static void startSubActivity(final Activity fromActivity, final int listId) { + final Intent intent = new Intent(fromActivity, GpxFileListActivity.class); + intent.putExtra(Intents.EXTRA_LIST_ID, StoredList.getConcreteList(listId)); + fromActivity.startActivityForResult(intent, 0); + } + + @Override + protected boolean filenameBelongsToList(@NonNull final String filename) { + if (super.filenameBelongsToList(filename)) { + if (StringUtils.endsWithIgnoreCase(filename, GPXImporter.ZIP_FILE_EXTENSION)) { + for (final IConnector connector : ConnectorFactory.getConnectors()) { + if (connector.isZippedGPXFile(filename)) { + return true; + } + } + return false; + } + // filter out waypoint files + return !StringUtils.containsIgnoreCase(filename, GPXImporter.WAYPOINTS_FILE_SUFFIX); + } + return false; + } + + public int getListId() { + return listId; + } + +} diff --git a/main/src/cgeo/geocaching/ICache.java b/main/src/cgeo/geocaching/ICache.java deleted file mode 100644 index b99d877..0000000 --- a/main/src/cgeo/geocaching/ICache.java +++ /dev/null @@ -1,164 +0,0 @@ -/** - * - */ -package cgeo.geocaching; - -import cgeo.geocaching.enumerations.CacheSize; -import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.enumerations.LogType; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** - * Basic interface for caches - */ -public interface ICache extends ILogable, ICoordinates { - - /** - * @return Displayed owner, might differ from the real owner - */ - public String getOwnerDisplayName(); - - /** - * @return GC username of the (actual) owner, might differ from the owner. Never empty. - */ - public String getOwnerUserId(); - - /** - * @return true if the user is the owner of the cache, false else - */ - public boolean isOwner(); - - /** - * @return true is the cache is archived, false else - */ - public boolean isArchived(); - - /** - * @return true is the cache is a Premium Member cache only, false else - */ - public boolean isPremiumMembersOnly(); - - /** - * @return Decrypted hint - */ - public String getHint(); - - /** - * @return Description - */ - public String getDescription(); - - /** - * @return Short Description - */ - public String getShortDescription(); - - - /** - * @return Id - */ - public String getCacheId(); - - /** - * @return Guid - */ - public String getGuid(); - - /** - * @return Location - */ - public String getLocation(); - - /** - * @return Personal note - */ - public String getPersonalNote(); - - - /** - * @return true if the user gave a favorite point to the cache - * - */ - public boolean isFavorite(); - - /** - * @return number of favorite points - * - */ - public int getFavoritePoints(); - - /** - * @return true if the cache is on the watchlist of the user - * - */ - public boolean isOnWatchlist(); - - /** - * @return The date the cache has been hidden - * - */ - public Date getHiddenDate(); - - /** - * null safe list of attributes - * - * @return the list of attributes for this cache - */ - public List<String> getAttributes(); - - /** - * @return the list of trackables in this cache - */ - public List<Trackable> getInventory(); - - /** - * @return the list of spoiler images - */ - public List<Image> getSpoilers(); - - /** - * @return a statistic how often the caches has been found, disabled, archived etc. - */ - public Map<LogType, Integer> getLogCounts(); - - /** - * get the name for lexicographical sorting. - * - * @return normalized, cached name which sort also correct for numerical parts in the name - */ - public String getNameForSorting(); - - /** - * @return Tradi, multi etc. - */ - CacheType getType(); - - /** - * @return Micro, small etc. - */ - CacheSize getSize(); - - /** - * @return true if the user already found the cache - * - */ - boolean isFound(); - - /** - * @return true if the cache is disabled, false else - */ - boolean isDisabled(); - - /** - * @return Difficulty assessment - */ - float getDifficulty(); - - /** - * @return Terrain assessment - */ - float getTerrain(); -} diff --git a/main/src/cgeo/geocaching/ICoordinates.java b/main/src/cgeo/geocaching/ICoordinates.java index b70e4ac..8f78e00 100644 --- a/main/src/cgeo/geocaching/ICoordinates.java +++ b/main/src/cgeo/geocaching/ICoordinates.java @@ -1,6 +1,6 @@ package cgeo.geocaching; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; public interface ICoordinates { diff --git a/main/src/cgeo/geocaching/Image.java b/main/src/cgeo/geocaching/Image.java index f592fc1..5c0e4f0 100644 --- a/main/src/cgeo/geocaching/Image.java +++ b/main/src/cgeo/geocaching/Image.java @@ -31,7 +31,7 @@ public class Image implements Parcelable { } public Image(final File file) { - this("file://" + file.getAbsolutePath(), file.getName(), null); + this(FileUtils.fileToUrl(file), file.getName(), null); } public Image(final Parcel in) { @@ -46,7 +46,7 @@ public class Image implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(final Parcel dest, final int flags) { dest.writeString(url); dest.writeString(title); dest.writeString(description); @@ -54,12 +54,12 @@ public class Image implements Parcelable { public static final Parcelable.Creator<Image> CREATOR = new Parcelable.Creator<Image>() { @Override - public Image createFromParcel(Parcel in) { + public Image createFromParcel(final Parcel in) { return new Image(in); } @Override - public Image[] newArray(int size) { + public Image[] newArray(final int size) { return new Image[size]; } }; diff --git a/main/src/cgeo/geocaching/ImageSelectActivity.java b/main/src/cgeo/geocaching/ImageSelectActivity.java index a64b4cf..9d1c1e0 100644 --- a/main/src/cgeo/geocaching/ImageSelectActivity.java +++ b/main/src/cgeo/geocaching/ImageSelectActivity.java @@ -18,6 +18,7 @@ import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -48,11 +49,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 +76,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 +93,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 +101,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 +119,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 +140,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,27 +158,36 @@ 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); - setResult(RESULT_OK, intent); - } else { - showToast(res.getString(R.string.err_select_logimage_failed)); - setResult(RESULT_CANCELED); - } + new AsyncTask<Void, Void, String>() { + @Override + protected String doInBackground(final Void... params) { + return writeScaledImage(imageUri.getPath()); + } + + @Override + protected void onPostExecute(final String filename) { + if (filename != null) { + imageUri = Uri.parse(filename); + final Intent intent = new Intent(); + syncEditTexts(); + 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)); + setResult(RESULT_CANCELED); + } + finish(); + } + }.execute(); } else { setResult(RESULT_CANCELED); + finish(); } - - finish(); } private void syncEditTexts() { @@ -193,7 +198,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 +212,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 +237,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 +248,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 +274,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); @@ -288,7 +293,6 @@ public class ImageSelectActivity extends AbstractActionBarActivity { /** * Scales and writes the scaled image. * - * @param filePath * @return the scaled image path, or <tt>null</tt> if the image cannot be decoded */ @Nullable @@ -314,8 +318,17 @@ public class ImageSelectActivity extends AbstractActionBarActivity { return; } - final Bitmap bitmap = ImageUtils.readAndScaleImageToFitDisplay(imageUri.getPath()); - imagePreview.setImageBitmap(bitmap); - imagePreview.setVisibility(View.VISIBLE); + new AsyncTask<Void, Void, Bitmap>() { + @Override + protected Bitmap doInBackground(final Void... params) { + return ImageUtils.readAndScaleImageToFitDisplay(imageUri.getPath()); + } + + @Override + protected void onPostExecute(final Bitmap bitmap) { + imagePreview.setImageBitmap(bitmap); + imagePreview.setVisibility(View.VISIBLE); + } + }.execute(); } } diff --git a/main/src/cgeo/geocaching/ImagesActivity.java b/main/src/cgeo/geocaching/ImagesActivity.java index b75e5eb..975a720 100644 --- a/main/src/cgeo/geocaching/ImagesActivity.java +++ b/main/src/cgeo/geocaching/ImagesActivity.java @@ -6,6 +6,7 @@ import cgeo.geocaching.ui.ImagesList; import cgeo.geocaching.ui.ImagesList.ImageType; import org.apache.commons.collections4.CollectionUtils; + import rx.Subscription; import android.content.Context; @@ -28,7 +29,7 @@ public class ImagesActivity extends AbstractActionBarActivity { private Subscription subscription; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // get parameters @@ -78,11 +79,12 @@ public class ImagesActivity extends AbstractActionBarActivity { super.onStop(); } - public static void startActivityLogImages(final Context fromActivity, final String geocode, List<Image> logImages) { + public static void startActivityLogImages(final Context fromActivity, final String geocode, final List<Image> logImages) { startActivity(fromActivity, geocode, logImages, ImageType.LogImages); } - private static void startActivity(final Context fromActivity, final String geocode, List<Image> logImages, ImageType imageType) { + @SuppressWarnings("deprecation") + private static void startActivity(final Context fromActivity, final String geocode, final List<Image> logImages, final ImageType imageType) { final Intent logImgIntent = new Intent(fromActivity, ImagesActivity.class); // if resuming our app within this activity, finish it and return to the cache activity logImgIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) @@ -95,18 +97,18 @@ public class ImagesActivity extends AbstractActionBarActivity { fromActivity.startActivity(logImgIntent); } - public static void startActivitySpoilerImages(final Context fromActivity, String geocode, List<Image> spoilers) { + public static void startActivitySpoilerImages(final Context fromActivity, final String geocode, final List<Image> spoilers) { startActivity(fromActivity, geocode, spoilers, ImageType.SpoilerImages); } @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); imagesList.onCreateContextMenu(menu, v); } @Override - public boolean onContextItemSelected(MenuItem item) { + public boolean onContextItemSelected(final MenuItem item) { if (imagesList.onContextItemSelected(item)) { return true; } diff --git a/main/src/cgeo/geocaching/Intents.java b/main/src/cgeo/geocaching/Intents.java index a55c22a..cbf2346 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() { @@ -10,6 +18,8 @@ public class Intents { public static final String EXTRA_ADDRESS = PREFIX + "address"; public static final String EXTRA_COORDS = PREFIX + "coords"; + public static final String EXTRA_LATITUDE = PREFIX + "latitude"; + public static final String EXTRA_LONGITUDE = PREFIX + "longitude"; public static final String EXTRA_COUNT = PREFIX + "count"; public static final String EXTRA_GEOCODE = PREFIX + "geocode"; public static final String EXTRA_GUID = PREFIX + "guid"; @@ -18,7 +28,24 @@ 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_TITLE = PREFIX + "title"; + public static final String EXTRA_MAP_MODE = PREFIX + "mapMode"; + public static final String EXTRA_LIVE_ENABLED = PREFIX + "liveEnabled"; + + public static final String EXTRA_DOWNLOAD = PREFIX + "download"; + + /** + * 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 +76,25 @@ 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; + } + try { + return CacheListType.valueOf(typeName); + } catch (final IllegalArgumentException ignored) { + return CacheListType.OFFLINE; + } + } } diff --git a/main/src/cgeo/geocaching/LogCacheActivity.java b/main/src/cgeo/geocaching/LogCacheActivity.java index bc87525..6fbd7bf 100644 --- a/main/src/cgeo/geocaching/LogCacheActivity.java +++ b/main/src/cgeo/geocaching/LogCacheActivity.java @@ -1,6 +1,7 @@ package cgeo.geocaching; import butterknife.ButterKnife; +import butterknife.InjectView; import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.connector.ILoggingManager; @@ -11,12 +12,14 @@ import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.LogTypeTrackable; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.gcvote.GCVote; +import cgeo.geocaching.gcvote.GCVoteRatingBarUtil; +import cgeo.geocaching.gcvote.GCVoteRatingBarUtil.OnRatingChangeListener; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.twitter.Twitter; import cgeo.geocaching.ui.dialog.DateDialog; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.AsyncTaskWithProgress; -import cgeo.geocaching.utils.DateUtils; +import cgeo.geocaching.utils.CalendarUtils; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.LogTemplateProvider; @@ -44,8 +47,6 @@ import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; -import android.widget.RatingBar; -import android.widget.RatingBar.OnRatingBarChangeListener; import android.widget.TextView; import java.util.ArrayList; @@ -54,8 +55,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"; @@ -72,9 +71,9 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia private String text = null; private List<LogType> possibleLogTypes = new ArrayList<>(); private List<TrackableLog> trackables = null; - private CheckBox tweetCheck = null; - private LinearLayout tweetBox = null; - private LinearLayout logPasswordBox = null; + protected @InjectView(R.id.tweet) CheckBox tweetCheck; + protected @InjectView(R.id.tweet_box) LinearLayout tweetBox; + protected @InjectView(R.id.log_password_box) LinearLayout logPasswordBox; private SparseArray<TrackableLog> actionButtons; private ILoggingManager loggingManager; @@ -88,9 +87,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia private Uri imageUri; private boolean sendButtonEnabled; - public void onLoadFinished() { - if (loggingManager.hasLoaderError()) { showErrorLoadingData(); return; @@ -111,6 +108,8 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia showToast(res.getString(R.string.info_log_type_changed)); } + initializeRatingBar(); + enablePostButton(true); initializeTrackablesAction(); @@ -141,7 +140,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia } actionButtons = new SparseArray<>(); - final LinearLayout inventoryView = (LinearLayout) findViewById(R.id.inventory); + final LinearLayout inventoryView = ButterKnife.findById(this, R.id.inventory); inventoryView.removeAllViews(); for (final TrackableLog tb : trackables) { @@ -165,7 +164,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia final String tbCode = tb.trackCode; inventoryItem.setClickable(true); - inventoryItem.findViewById(R.id.info).setOnClickListener(new View.OnClickListener() { + ButterKnife.findById(inventoryItem, R.id.info).setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View view) { @@ -179,10 +178,10 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia } if (inventoryView.getChildCount() > 0) { - findViewById(R.id.inventory_box).setVisibility(View.VISIBLE); + ButterKnife.findById(this, R.id.inventory_box).setVisibility(View.VISIBLE); } if (inventoryView.getChildCount() > 1) { - final LinearLayout inventoryChangeAllView = (LinearLayout) findViewById(R.id.inventory_changeall); + final LinearLayout inventoryChangeAllView = ButterKnife.findById(this, R.id.inventory_changeall); final Button changeButton = ButterKnife.findById(inventoryChangeAllView, R.id.changebutton); changeButton.setOnClickListener(new View.OnClickListener() { @@ -208,9 +207,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); } @@ -227,13 +226,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia setTitle(res.getString(R.string.log_new_log) + ": " + cache.getGeocode()); } - // Get ids for later use - tweetBox = (LinearLayout) findViewById(R.id.tweet_box); - tweetCheck = (CheckBox) findViewById(R.id.tweet); - logPasswordBox = (LinearLayout) findViewById(R.id.log_password_box); - - final RatingBar ratingBar = (RatingBar) findViewById(R.id.gcvoteRating); - initializeRatingBar(ratingBar); + initializeRatingBar(); // initialize with default values setDefaultValues(); @@ -261,7 +254,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia } enablePostButton(false); - final Button typeButton = (Button) findViewById(R.id.type); + final Button typeButton = ButterKnife.findById(this, R.id.type); typeButton.setText(typeSelected.getL10n()); typeButton.setOnClickListener(new View.OnClickListener() { @@ -271,11 +264,11 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia } }); - final Button dateButton = (Button) findViewById(R.id.date); + final Button dateButton = ButterKnife.findById(this, R.id.date); setDate(date); dateButton.setOnClickListener(new DateListener()); - final EditText logView = (EditText) findViewById(R.id.log); + final EditText logView = ButterKnife.findById(this, R.id.log); if (StringUtils.isBlank(currentLogText()) && StringUtils.isNotBlank(text)) { logView.setText(text); Dialogs.moveCursorToEnd(logView); @@ -288,27 +281,19 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia loggingManager = cache.getLoggingManager(this); loggingManager.init(); + requestKeyboardForLogging(); } - private void initializeRatingBar(final RatingBar ratingBar) { - final TextView label = (TextView) findViewById(R.id.gcvoteLabel); + private void initializeRatingBar() { if (GCVote.isVotingPossible(cache)) { - ratingBar.setVisibility(View.VISIBLE); - label.setVisibility(View.VISIBLE); - } - ratingBar.setOnRatingBarChangeListener(new OnRatingBarChangeListener() { + GCVoteRatingBarUtil.initializeRatingBar(cache, getWindow().getDecorView().getRootView(), new OnRatingChangeListener() { - @Override - public void onRatingChanged(final RatingBar ratingBar, final float stars, final boolean fromUser) { - // 0.5 is not a valid rating, therefore we must limit - rating = GCVote.isValidRating(stars) ? stars : 0; - if (rating < stars) { - ratingBar.setRating(rating); + @Override + public void onRatingChanged(final float stars) { + rating = stars; } - label.setText(GCVote.getDescription(rating)); - } - }); - ratingBar.setRating(cache.getMyVote()); + }); + } } private void setDefaultValues() { @@ -316,7 +301,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia rating = GCVote.NO_RATING; typeSelected = cache.getDefaultLogType(); // it this is an attended event log, use the event date by default instead of the current date - if (cache.isEventCache() && DateUtils.isPastEvent(cache) && typeSelected == LogType.ATTENDED) { + if (cache.isEventCache() && CalendarUtils.isPastEvent(cache) && typeSelected == LogType.ATTENDED) { date.setTime(cache.getHiddenDate()); } text = null; @@ -333,9 +318,9 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia setType(typeSelected); setDate(date); - final EditText logView = (EditText) findViewById(R.id.log); + final EditText logView = ButterKnife.findById(this, R.id.log); logView.setText(StringUtils.EMPTY); - final EditText logPasswordView = (EditText) findViewById(R.id.log_password); + final EditText logPasswordView = ButterKnife.findById(this, R.id.log_password); logPasswordView.setText(StringUtils.EMPTY); showToast(res.getString(R.string.info_log_cleared)); @@ -368,12 +353,12 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia public void setDate(final Calendar dateIn) { date = dateIn; - final Button dateButton = (Button) findViewById(R.id.date); + final Button dateButton = ButterKnife.findById(this, R.id.date); dateButton.setText(Formatter.formatShortDateVerbally(date.getTime().getTime())); } public void setType(final LogType type) { - final Button typeButton = (Button) findViewById(R.id.type); + final Button typeButton = ButterKnife.findById(this, R.id.type); typeSelected = type; typeButton.setText(typeSelected.getL10n()); @@ -419,13 +404,13 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia final String log = logTexts[0]; final String logPwd = logTexts.length > 1 ? logTexts[1] : null; try { - final LogResult logResult = loggingManager.postLog(cache, typeSelected, date, log, logPwd, trackables); + final LogResult logResult = loggingManager.postLog(typeSelected, date, log, logPwd, trackables); 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()); + cache.setVisitedDate(date.getTimeInMillis()); } DataStore.saveChangedCache(cache); @@ -434,7 +419,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia final LogEntry logNow = new LogEntry(date.getTimeInMillis(), typeSelected, log); logNow.friend = true; newLogs.add(0, logNow); - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), newLogs); + DataStore.saveLogs(cache.getGeocode(), newLogs); // update offline log in DB cache.clearOfflineLog(); @@ -444,8 +429,13 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia Twitter.postTweetCache(geocode, logNow); } } - if (GCVote.isValidRating(rating)) { - GCVote.setRating(cache, rating); + if (GCVote.isValidRating(rating) && GCVote.isVotingPossible(cache)) { + if (GCVote.setRating(cache, rating)) { + cache.setMyVote(rating); + DataStore.saveChangedCache(cache); + } else { + showToast(res.getString(R.string.err_gcvote_send_rating)); + } } if (StringUtils.isNotBlank(imageUri.getPath())) { @@ -453,7 +443,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia final String uploadedImageUrl = imageResult.getImageUri(); if (StringUtils.isNotEmpty(uploadedImageUrl)) { logNow.addLogImage(new Image(uploadedImageUrl, imageCaption, imageDescription)); - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), newLogs); + DataStore.saveLogs(cache.getGeocode(), newLogs); } return imageResult.getPostResult(); } @@ -461,7 +451,7 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia return logResult.getPostLogResult(); } catch (final RuntimeException e) { - Log.e("VisitCacheActivity.Poster.doInBackgroundInternal", e); + Log.e("LogCacheActivity.Poster.doInBackgroundInternal", e); } return StatusCode.LOG_POST_ERROR; @@ -491,16 +481,17 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia // again will be easy using "Clear" while retyping the text may not be. if (force || (StringUtils.isNotEmpty(log) && !StringUtils.equals(log, text))) { cache.logOffline(this, log, date, typeSelected); + Settings.setLastCacheLog(log); } text = log; } private String currentLogText() { - return ((EditText) findViewById(R.id.log)).getText().toString(); + return ButterKnife.<EditText>findById(this, R.id.log).getText().toString(); } private String currentLogPassword() { - return ((EditText) findViewById(R.id.log_password)).getText().toString(); + return ButterKnife.<EditText>findById(this, R.id.log_password).getText().toString(); } @Override @@ -578,9 +569,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); } @@ -589,9 +580,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)); @@ -603,7 +594,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(); @@ -622,18 +613,37 @@ 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 (CalendarUtils.isFuture(date)) { + Dialogs.message(this, R.string.log_date_future_not_allowed); + 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()); + Settings.setLastCacheLog(currentLogText()); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); @@ -650,4 +660,16 @@ public class LogCacheActivity extends AbstractLoggingActivity implements DateDia .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; + } + + @Override + protected String getLastLog() { + return Settings.getLastCacheLog(); + } } diff --git a/main/src/cgeo/geocaching/LogEntry.java b/main/src/cgeo/geocaching/LogEntry.java index b4b346c..41c222b 100644 --- a/main/src/cgeo/geocaching/LogEntry.java +++ b/main/src/cgeo/geocaching/LogEntry.java @@ -2,7 +2,6 @@ package cgeo.geocaching; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.settings.Settings; -import cgeo.geocaching.utils.DateUtils; import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.MatcherWrapper; @@ -89,7 +88,7 @@ public final class LogEntry { public CharSequence getImageTitles() { final List<String> titles = new ArrayList<>(5); - for (Image image : getLogImages()) { + for (final Image image : getLogImages()) { if (StringUtils.isNotBlank(image.getTitle())) { titles.add(HtmlUtils.extractText(image.getTitle())); } @@ -100,16 +99,12 @@ public final class LogEntry { return StringUtils.join(titles, ", "); } - public int daysSinceLog() { - return DateUtils.daysSince(date); - } - /** * Get the log text to be displayed. Depending on the settings, color tags might be removed. */ public String getDisplayText() { if (Settings.getPlainLogs()) { - MatcherWrapper matcher = new MatcherWrapper(PATTERN_REMOVE_COLORS, log); + final MatcherWrapper matcher = new MatcherWrapper(PATTERN_REMOVE_COLORS, log); return matcher.replaceAll(StringUtils.EMPTY); } return log; diff --git a/main/src/cgeo/geocaching/LogTrackableActivity.java b/main/src/cgeo/geocaching/LogTrackableActivity.java index 26f7c84..fa77458 100644 --- a/main/src/cgeo/geocaching/LogTrackableActivity.java +++ b/main/src/cgeo/geocaching/LogTrackableActivity.java @@ -3,6 +3,7 @@ package cgeo.geocaching; import butterknife.ButterKnife; import butterknife.InjectView; +import cgeo.geocaching.activity.Keyboard; import cgeo.geocaching.connector.gc.GCLogin; import cgeo.geocaching.connector.gc.GCParser; import cgeo.geocaching.enumerations.LogType; @@ -15,7 +16,9 @@ import cgeo.geocaching.ui.dialog.DateDialog; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.LogTemplateProvider; import cgeo.geocaching.utils.LogTemplateProvider.LogContext; +import cgeo.geocaching.utils.LogTemplateProvider.LogTemplate; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -28,6 +31,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.ContextMenu; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; @@ -61,6 +65,8 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat private int attempts = 0; private Trackable trackable; + final public static int LOG_TRACKABLE = 1; + private final Handler showProgressHandler = new Handler() { @Override public void handleMessage(final Message msg) { @@ -106,6 +112,7 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat final StatusCode error = (StatusCode) msg.obj; if (error == StatusCode.NO_ERROR) { showToast(res.getString(R.string.info_log_posted)); + setResult(RESULT_OK); finish(); } else { showToast(error.getErrorString(res)); @@ -132,20 +139,31 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat trackable = DataStore.loadTrackable(geocode); + if (trackable == null) { + Log.e("LogTrackableActivity.onCreate: cannot load trackable " + geocode); + setResult(RESULT_CANCELED); + finish(); + return; + } + if (StringUtils.isNotBlank(trackable.getName())) { setTitle(res.getString(R.string.trackable_touch) + ": " + trackable.getName()); } else { setTitle(res.getString(R.string.trackable_touch) + ": " + trackable.getGeocode()); } - if (guid == null) { - showToast(res.getString(R.string.err_tb_forgot_saw)); + init(); + requestKeyboardForLogging(); + } - finish(); - return; + @Override + protected void requestKeyboardForLogging() { + if (StringUtils.isBlank(trackingEditText.getText())) { + new Keyboard(this).show(trackingEditText); + } + else { + super.requestKeyboardForLogging(); } - - init(); } @Override @@ -241,11 +259,6 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat public LoadDataThread() { super("Load data for logging trackable"); - if (guid == null) { - showToast(res.getString(R.string.err_tb_forgot_saw)); - - finish(); - } } @Override @@ -309,6 +322,9 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat tweetCheck.isChecked() && tweetBox.getVisibility() == View.VISIBLE) { Twitter.postTweetTrackable(geocode, new LogEntry(0, typeSelected, log)); } + if (status == StatusCode.NO_ERROR) { + addLocalTrackableLog(log); + } return status; } catch (final Exception e) { @@ -318,12 +334,25 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat return StatusCode.LOG_POST_ERROR; } - public static void startActivity(final Context context, final Trackable trackable) { + /** + * Adds the new log to the list of log entries for this trackable to be able to show it in the trackable activity. + * + * + */ + private void addLocalTrackableLog(final String logText) { + final LogEntry logEntry = new LogEntry(date.getTimeInMillis(), typeSelected, logText); + final ArrayList<LogEntry> modifiedLogs = new ArrayList<>(trackable.getLogs()); + modifiedLogs.add(0, logEntry); + trackable.setLogs(modifiedLogs); + DataStore.saveTrackable(trackable); + } + + public static Intent getIntent(final Context context, final Trackable trackable) { final Intent logTouchIntent = new Intent(context, LogTrackableActivity.class); logTouchIntent.putExtra(Intents.EXTRA_GEOCODE, trackable.getGeocode()); logTouchIntent.putExtra(Intents.EXTRA_GUID, trackable.getGuid()); logTouchIntent.putExtra(Intents.EXTRA_TRACKING_CODE, trackable.getTrackingcode()); - context.startActivity(logTouchIntent); + return logTouchIntent; } @Override @@ -355,9 +384,26 @@ public class LogTrackableActivity extends AbstractLoggingActivity implements Dat final String tracking = trackingEditText.getText().toString(); final String log = logEditText.getText().toString(); new PostLogThread(postLogHandler, tracking, log).start(); + Settings.setLastTrackableLog(log); } else { showToast(res.getString(R.string.err_log_load_data_still)); } } + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + final boolean result = super.onCreateOptionsMenu(menu); + for (final LogTemplate template : LogTemplateProvider.getTemplatesWithoutSignature()) { + if (template.getTemplateString().equals("NUMBER")) { + menu.findItem(R.id.menu_templates).getSubMenu().removeItem(template.getItemId()); + } + } + return result; + } + + @Override + protected String getLastLog() { + return Settings.getLastTrackableLog(); + } + } diff --git a/main/src/cgeo/geocaching/MainActivity.java b/main/src/cgeo/geocaching/MainActivity.java index c0c6712..45ce698 100644 --- a/main/src/cgeo/geocaching/MainActivity.java +++ b/main/src/cgeo/geocaching/MainActivity.java @@ -7,17 +7,21 @@ import cgeo.geocaching.activity.AbstractActionBarActivity; import cgeo.geocaching.activity.ShowcaseViewBuilder; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ILogin; -import cgeo.geocaching.connector.gc.GCConnector; -import cgeo.geocaching.connector.gc.GCLogin; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.StatusCode; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.AndroidGeocoder; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.MapQuestGeocoder; +import cgeo.geocaching.location.Units; import cgeo.geocaching.maps.CGeoMap; +import cgeo.geocaching.network.Network; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.GpsStatusProvider; +import cgeo.geocaching.sensors.GpsStatusProvider.Status; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.SettingsActivity; import cgeo.geocaching.ui.dialog.Dialogs; @@ -35,21 +39,23 @@ import com.google.zxing.integration.android.IntentResult; import org.apache.commons.lang3.StringUtils; import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action0; import rx.functions.Action1; -import rx.subscriptions.Subscriptions; +import rx.functions.Func1; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.SearchManager; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.location.Address; -import android.location.Geocoder; +import android.net.ConnectivityManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -64,12 +70,12 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Locale; public class MainActivity extends AbstractActionBarActivity { @InjectView(R.id.nav_satellites) protected TextView navSatellites; @@ -88,11 +94,9 @@ public class MainActivity extends AbstractActionBarActivity { public static final int SEARCH_REQUEST_CODE = 2; - private int version = 0; - private boolean cleanupRunning = false; - private int countBubbleCnt = 0; private Geopoint addCoords = null; private boolean initialized = false; + private ConnectivityChangeReceiver connectivityChangeReceiver; private final UpdateLocation locationUpdater = new UpdateLocation(); @@ -110,7 +114,7 @@ public class MainActivity extends AbstractActionBarActivity { for (final ILogin conn : loginConns) { - final TextView connectorInfo = (TextView) inflater.inflate(R.layout.main_activity_connectorstatus, null); + final TextView connectorInfo = (TextView) inflater.inflate(R.layout.main_activity_connectorstatus, infoArea, false); infoArea.addView(connectorInfo); final StringBuilder userInfo = new StringBuilder(conn.getName()).append(Formatter.SEPARATOR); @@ -128,6 +132,19 @@ public class MainActivity extends AbstractActionBarActivity { } }; + private final class ConnectivityChangeReceiver extends BroadcastReceiver { + private boolean isConnected = Network.isNetworkConnected(); + + @Override + public void onReceive(final Context context, final Intent intent) { + final boolean wasConnected = isConnected; + isConnected = Network.isNetworkConnected(); + if (isConnected && !wasConnected) { + startBackgroundLogin(); + } + } + } + private static String formatAddress(final Address address) { final ArrayList<String> addressParts = new ArrayList<>(); @@ -147,37 +164,16 @@ public class MainActivity extends AbstractActionBarActivity { return StringUtils.join(addressParts, ", "); } - private class SatellitesHandler extends GeoDirHandler { - - private boolean gpsEnabled = false; - private int satellitesFixed = 0; - private int satellitesVisible = 0; - + private final Action1<GpsStatusProvider.Status> satellitesHandler = new Action1<Status>() { @Override - public void updateGeoData(final IGeoData data) { - if (data.getGpsEnabled() == gpsEnabled && - data.getSatellitesFixed() == satellitesFixed && - data.getSatellitesVisible() == satellitesVisible) { - return; - } - gpsEnabled = data.getGpsEnabled(); - satellitesFixed = data.getSatellitesFixed(); - satellitesVisible = data.getSatellitesVisible(); - - if (gpsEnabled) { - if (satellitesFixed > 0) { - navSatellites.setText(res.getString(R.string.loc_sat) + ": " + satellitesFixed + '/' + satellitesVisible); - } else if (satellitesVisible >= 0) { - navSatellites.setText(res.getString(R.string.loc_sat) + ": 0/" + satellitesVisible); - } + public void call(final Status gpsStatus) { + if (gpsStatus.gpsEnabled) { + navSatellites.setText(res.getString(R.string.loc_sat) + ": " + gpsStatus.satellitesFixed + '/' + gpsStatus.satellitesVisible); } else { navSatellites.setText(res.getString(R.string.loc_gps_disabled)); } } - - } - - private final SatellitesHandler satellitesHandler = new SatellitesHandler(); + }; private final Handler firstLoginHandler = new Handler() { @@ -214,8 +210,7 @@ public class MainActivity extends AbstractActionBarActivity { setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); // type to search - version = Version.getVersionCode(this); - Log.i("Starting " + getPackageName() + ' ' + version + " a.k.a " + Version.getVersionName(this)); + Log.i("Starting " + getPackageName() + ' ' + Version.getVersionCode(this) + " a.k.a " + Version.getVersionName(this)); init(); @@ -231,30 +226,34 @@ public class MainActivity extends AbstractActionBarActivity { @Override public void onResume() { - super.onResume(Subscriptions.from(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA), satellitesHandler.start(GeoDirHandler.UPDATE_GEODATA))); + super.onResume(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA | GeoDirHandler.LOW_POWER), + Sensors.getInstance().gpsStatusObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(satellitesHandler)); updateUserInfoHandler.sendEmptyMessage(-1); startBackgroundLogin(); init(); + + connectivityChangeReceiver = new ConnectivityChangeReceiver(); + registerReceiver(connectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } private void startBackgroundLogin() { - assert(app != null); + assert app != null; final boolean mustLogin = app.mustRelog(); for (final ILogin conn : ConnectorFactory.getActiveLiveConnectors()) { if (mustLogin || !conn.isLoggedIn()) { - new Thread() { + RxUtils.networkScheduler.createWorker().schedule(new Action0() { @Override - public void run() { - if (mustLogin && conn == GCConnector.getInstance()) { + public void call() { + if (mustLogin) { // Properly log out from geocaching.com - GCLogin.getInstance().logout(); + conn.logout(); } conn.login(firstLoginHandler, MainActivity.this); updateUserInfoHandler.sendEmptyMessage(-1); } - }.start(); + }); } } } @@ -276,6 +275,7 @@ public class MainActivity extends AbstractActionBarActivity { @Override public void onPause() { initialized = false; + unregisterReceiver(connectivityChangeReceiver); super.onPause(); } @@ -315,7 +315,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(); @@ -447,7 +447,7 @@ public class MainActivity extends AbstractActionBarActivity { setFilterTitle(); checkRestore(); - (new CleanDatabaseThread()).start(); + DataStore.cleanIfNeeded(this); } protected void selectGlobalTypeFilter() { @@ -464,11 +464,11 @@ public class MainActivity extends AbstractActionBarActivity { sorted.addAll(Arrays.asList(CacheType.values())); sorted.removeAll(cacheTypes); + final Collator collator = TextUtils.getCollator(); Collections.sort(sorted, new Comparator<CacheType>() { - @Override public int compare(final CacheType left, final CacheType right) { - return left.getL10n().compareToIgnoreCase(right.getL10n()); + return collator.compare(left.getL10n(), right.getL10n()); } }); @@ -501,7 +501,23 @@ public class MainActivity extends AbstractActionBarActivity { } public void updateCacheCounter() { - (new CountBubbleUpdateThread()).start(); + AppObservable.bindActivity(this, DataStore.getAllCachesCountObservable()).subscribe(new Action1<Integer>() { + @Override + public void call(final Integer countBubbleCnt1) { + if (countBubbleCnt1 == 0) { + countBubble.setVisibility(View.GONE); + } else { + countBubble.setText(Integer.toString(countBubbleCnt1)); + countBubble.bringToFront(); + countBubble.setVisibility(View.VISIBLE); + } + } + }, new Action1<Throwable>() { + @Override + public void call(final Throwable throwable) { + Log.e("Unable to add bubble count", throwable); + } + }); } private void checkRestore() { @@ -534,7 +550,7 @@ public class MainActivity extends AbstractActionBarActivity { private class UpdateLocation extends GeoDirHandler { @Override - public void updateGeoData(final IGeoData geo) { + public void updateGeoData(final GeoData geo) { if (!nearestView.isClickable()) { nearestView.setFocusable(true); nearestView.setClickable(true); @@ -556,29 +572,21 @@ public class MainActivity extends AbstractActionBarActivity { navAccuracy.setText(null); } + final Geopoint currentCoords = geo.getCoords(); if (Settings.isShowAddress()) { if (addCoords == null) { navLocation.setText(R.string.loc_no_addr); } - if (addCoords == null || (geo.getCoords().distanceTo(addCoords) > 0.5)) { - final Observable<String> address = Observable.create(new OnSubscribe<String>() { + if (addCoords == null || (currentCoords.distanceTo(addCoords) > 0.5)) { + addCoords = currentCoords; + final Observable<String> address = (new AndroidGeocoder(MainActivity.this).getFromLocation(currentCoords) + .onErrorResumeNext(MapQuestGeocoder.getFromLocation(currentCoords))).map(new Func1<Address, String>() { @Override - public void call(final Subscriber<? super String> subscriber) { - try { - addCoords = geo.getCoords(); - final Geocoder geocoder = new Geocoder(MainActivity.this, Locale.getDefault()); - final Geopoint coords = app.currentGeo().getCoords(); - final List<Address> addresses = geocoder.getFromLocation(coords.getLatitude(), coords.getLongitude(), 1); - if (!addresses.isEmpty()) { - subscriber.onNext(formatAddress(addresses.get(0))); - } - subscriber.onCompleted(); - } catch (final Exception e) { - subscriber.onError(e); - } + public String call(final Address address) { + return formatAddress(address); } - }); - AndroidObservable.bindActivity(MainActivity.this, address.onErrorResumeNext(Observable.from(geo.getCoords().toString()))) + }).onErrorResumeNext(Observable.just(currentCoords.toString())); + AppObservable.bindActivity(MainActivity.this, address) .subscribeOn(RxUtils.networkScheduler) .subscribe(new Action1<String>() { @Override @@ -588,7 +596,7 @@ public class MainActivity extends AbstractActionBarActivity { }); } } else { - navLocation.setText(geo.getCoords().toString()); + navLocation.setText(currentCoords.toString()); } } } @@ -599,7 +607,7 @@ public class MainActivity extends AbstractActionBarActivity { */ public void cgeoFindOnMap(final View v) { findOnMap.setPressed(true); - CGeoMap.startActivityLiveMap(this); + startActivity(CGeoMap.getLiveMapIntent(this)); } /** @@ -607,12 +615,8 @@ public class MainActivity extends AbstractActionBarActivity { * unused here but needed since this method is referenced from XML layout */ public void cgeoFindNearest(final View v) { - if (app.currentGeo().getCoords() == null) { - return; - } - nearestView.setPressed(true); - CacheListActivity.startActivityNearest(this, app.currentGeo().getCoords()); + startActivity(CacheListActivity.getNearestIntent(this)); } /** @@ -659,86 +663,19 @@ public class MainActivity extends AbstractActionBarActivity { startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); } - private class CountBubbleUpdateThread extends Thread { - private final Handler countBubbleHandler = new Handler() { - - @Override - public void handleMessage(final Message msg) { - try { - if (countBubbleCnt == 0) { - countBubble.setVisibility(View.GONE); - } else { - countBubble.setText(Integer.toString(countBubbleCnt)); - countBubble.bringToFront(); - countBubble.setVisibility(View.VISIBLE); - } - } catch (final Exception e) { - Log.w("MainActivity.countBubbleHander", e); - } - } - }; - - @Override - public void run() { - if (app == null) { - return; - } - - int checks = 0; - while (!DataStore.isInitialized()) { - try { - sleep(500); - checks++; - } catch (final Exception e) { - Log.e("MainActivity.CountBubbleUpdateThread.run", e); - } - - if (checks > 10) { - return; - } - } - - countBubbleCnt = DataStore.getAllCachesCount(); - - countBubbleHandler.sendEmptyMessage(0); - } - } - - private class CleanDatabaseThread extends Thread { - - @Override - public void run() { - if (app == null) { - return; - } - if (cleanupRunning) { - return; - } - - boolean more = false; - if (version != Settings.getVersion()) { - Log.i("Initializing hard cleanup - version changed from " + Settings.getVersion() + " to " + version + "."); - - more = true; - } - - cleanupRunning = true; - DataStore.clean(more); - cleanupRunning = false; - - if (version > 0) { - Settings.setVersion(version); - } - } - } - private void checkShowChangelog() { - final long lastChecksum = Settings.getLastChangelogChecksum(); - final long checksum = TextUtils.checksum(getString(R.string.changelog_master) + getString(R.string.changelog_release)); - Settings.setLastChangelogChecksum(checksum); - // don't show change log after new install... - if (lastChecksum > 0 && lastChecksum != checksum) { - AboutActivity.showChangeLog(this); + // temporary workaround for #4143 + //TODO: understand and avoid if possible + try { + final long lastChecksum = Settings.getLastChangelogChecksum(); + final long checksum = TextUtils.checksum(getString(R.string.changelog_master) + getString(R.string.changelog_release)); + Settings.setLastChangelogChecksum(checksum); + // don't show change log after new install... + if (lastChecksum > 0 && lastChecksum != checksum) { + AboutActivity.showChangeLog(this); + } + } catch (final Exception ex) { + Log.e("Error checking/showing changelog!", ex); } } diff --git a/main/src/cgeo/geocaching/NavigateAnyPointActivity.java b/main/src/cgeo/geocaching/NavigateAnyPointActivity.java index 2591987..b0d413d 100644 --- a/main/src/cgeo/geocaching/NavigateAnyPointActivity.java +++ b/main/src/cgeo/geocaching/NavigateAnyPointActivity.java @@ -7,11 +7,12 @@ import butterknife.Optional; import cgeo.geocaching.activity.AbstractActionBarActivity; import cgeo.geocaching.activity.INavigationSource; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; -import cgeo.geocaching.geopoint.DistanceParser; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; +import cgeo.geocaching.location.DistanceParser; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.AbstractViewHolder; import cgeo.geocaching.ui.NavigationActionProvider; @@ -19,8 +20,13 @@ import cgeo.geocaching.ui.dialog.CoordinatesInputDialog; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.Nullable; + +import rx.functions.Action0; +import rx.schedulers.Schedulers; import android.app.Activity; import android.content.Context; @@ -94,7 +100,7 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen public View getView(final int position, final View convertView, final ViewGroup parent) { View rowView = convertView; - ViewHolder viewHolder; + final ViewHolder viewHolder; if (rowView == null) { rowView = getInflater().inflate(R.layout.simple_way_point, parent, false); viewHolder = new ViewHolder(rowView); @@ -261,7 +267,7 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen private void initializeDistanceUnitSelector() { if (StringUtils.isBlank(distanceUnit)) { - if (Settings.isUseImperialUnits()) { + if (Settings.useImperialUnits()) { distanceUnitSelector.setSelection(2); // ft distanceUnit = res.getStringArray(R.array.distance_units)[2]; } else { @@ -281,7 +287,7 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen if (latButton.getText().length() > 0 && lonButton.getText().length() > 0) { gp = new Geopoint(latButton.getText().toString() + " " + lonButton.getText().toString()); } - final CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(null, gp, app.currentGeo()); + final CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(null, gp, Sensors.getInstance().currentGeo()); coordsDialog.setCancelable(true); coordsDialog.show(getSupportFragmentManager(),"wpedit_dialog"); } @@ -336,7 +342,7 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen menu.findItem(R.id.menu_caches_around).setVisible(visible); menu.findItem(R.id.menu_clear_history).setVisible(!getHistoryOfSearchedLocations().isEmpty()); - } catch (final RuntimeException e) { + } catch (final RuntimeException ignored) { // nothing } @@ -346,20 +352,14 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen @Override public boolean onOptionsItemSelected(final MenuItem item) { final int menuItem = item.getItemId(); - - final Geopoint coords = getDestination(); - - if (coords != null) { - addToHistory(coords); - } - + final Geopoint coords = getDestinationAndAddToHistory(); switch (menuItem) { case R.id.menu_default_navigation: - navigateTo(); + navigateTo(coords); return true; case R.id.menu_caches_around: - cachesAround(); + cachesAround(coords); return true; case R.id.menu_clear_history: @@ -373,22 +373,33 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen return super.onOptionsItemSelected(item); } - private void addToHistory(final Geopoint coords) { + private Geopoint getDestinationAndAddToHistory() { + final Geopoint coords = getDestination(); + addToHistory(coords); + return coords; + } + + private void addToHistory(@Nullable final Geopoint coords) { + if (coords == null) { + return; + } + // Add locations to history final Destination loc = new Destination(coords); if (!getHistoryOfSearchedLocations().contains(loc)) { getHistoryOfSearchedLocations().add(0, loc); - - // Save location - DataStore.saveSearchedDestination(loc); - - // Ensure to remove the footer - historyListView.removeFooterView(getEmptyHistoryFooter()); - - runOnUiThread(new Runnable() { + RxUtils.andThenOnUi(Schedulers.io(), new Action0() { @Override - public void run() { + public void call() { + // Save location + DataStore.saveSearchedDestination(loc); + } + }, new Action0() { + @Override + public void call() { + // Ensure to remove the footer + historyListView.removeFooterView(getEmptyHistoryFooter()); destinationHistoryAdapter.notifyDataSetChanged(); } }); @@ -431,35 +442,34 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen } } - private void navigateTo() { - navigateTo(getDestination()); - } + private void navigateTo(final Geopoint coords) { + if (coords == null) { + showToast(res.getString(R.string.err_location_unknown)); + return; + } - private void navigateTo(final Geopoint geopoint) { - NavigationAppFactory.startDefaultNavigationApplication(1, this, geopoint); + NavigationAppFactory.startDefaultNavigationApplication(1, this, coords); } - private void cachesAround() { - final Geopoint coords = getDestination(); - + private void cachesAround(final Geopoint coords) { if (coords == null) { showToast(res.getString(R.string.err_location_unknown)); return; } - CacheListActivity.startActivityCoordinates(this, coords); + CacheListActivity.startActivityCoordinates(this, coords, null); finish(); } private final GeoDirHandler geoDirHandler = new GeoDirHandler() { @Override - public void updateGeoData(final IGeoData geo) { + public void updateGeoData(final GeoData geo) { try { latButton.setHint(geo.getCoords().format(GeopointFormatter.Format.LAT_DECMINUTE_RAW)); lonButton.setHint(geo.getCoords().format(GeopointFormatter.Format.LON_DECMINUTE_RAW)); } catch (final RuntimeException e) { - Log.w("Failed to update location."); + Log.w("Failed to update location", e); } } }; @@ -468,15 +478,9 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen @Override public void onClick(final View arg0) { - final Geopoint coords = app.currentGeo().getCoords(); - if (coords == null) { - showToast(res.getString(R.string.err_point_unknown_position)); - return; - } - + final Geopoint coords = Sensors.getInstance().currentGeo().getCoords(); latButton.setText(coords.format(GeopointFormatter.Format.LAT_DECMINUTE)); lonButton.setText(coords.format(GeopointFormatter.Format.LON_DECMINUTE)); - changed = false; } } @@ -504,30 +508,25 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen return null; } } else { - if (app.currentGeo().getCoords() == null) { - showToast(res.getString(R.string.err_point_curr_position_unavailable)); - return null; - } - - coords = app.currentGeo().getCoords(); + coords = Sensors.getInstance().currentGeo().getCoords(); } // apply projection - if (coords != null && StringUtils.isNotBlank(bearingText) && StringUtils.isNotBlank(distanceText)) { + if (StringUtils.isNotBlank(bearingText) && StringUtils.isNotBlank(distanceText)) { // bearing & distance - double bearing; + final double bearing; try { bearing = Double.parseDouble(bearingText); - } catch (final NumberFormatException e) { + } catch (final NumberFormatException ignored) { Dialogs.message(this, R.string.err_point_bear_and_dist_title, R.string.err_point_bear_and_dist); return null; } - double distance; + final double distance; try { distance = DistanceParser.parseDistance(distanceText, - !Settings.isUseImperialUnits()); - } catch (final NumberFormatException e) { + !Settings.useImperialUnits()); + } catch (final NumberFormatException ignored) { showToast(res.getString(R.string.err_parse_dist)); return null; } @@ -535,9 +534,7 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen coords = coords.project(bearing, distance); } - if (coords != null) { - saveCoords(coords); - } + saveCoords(coords); return coords; } @@ -551,11 +548,11 @@ public class NavigateAnyPointActivity extends AbstractActionBarActivity implemen @Override public void startDefaultNavigation() { - navigateTo(); + navigateTo(getDestinationAndAddToHistory()); } @Override public void startDefaultNavigation2() { - NavigationAppFactory.startDefaultNavigationApplication(2, this, getDestination()); + NavigationAppFactory.startDefaultNavigationApplication(2, this, getDestinationAndAddToHistory()); } } diff --git a/main/src/cgeo/geocaching/PocketQueryList.java b/main/src/cgeo/geocaching/PocketQueryList.java index 21f306e..27edffd 100644 --- a/main/src/cgeo/geocaching/PocketQueryList.java +++ b/main/src/cgeo/geocaching/PocketQueryList.java @@ -2,14 +2,10 @@ package cgeo.geocaching; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.connector.gc.GCParser; -import cgeo.geocaching.utils.RxUtils; import org.apache.commons.collections4.CollectionUtils; -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action1; import android.app.Activity; @@ -47,13 +43,7 @@ public final class PocketQueryList { public static void promptForListSelection(final Activity activity, final Action1<PocketQueryList> runAfterwards) { final Dialog waitDialog = ProgressDialog.show(activity, activity.getString(R.string.search_pocket_title), activity.getString(R.string.search_pocket_loading), true, true); - AndroidObservable.bindActivity(activity, Observable.create(new OnSubscribe<List<PocketQueryList>>() { - @Override - public void call(final Subscriber<? super List<PocketQueryList>> subscriber) { - subscriber.onNext(GCParser.searchPocketQueryList()); - subscriber.onCompleted(); - } - })).subscribeOn(RxUtils.networkScheduler).subscribe(new Action1<List<PocketQueryList>>() { + AppObservable.bindActivity(activity, GCParser.searchPocketQueryListObservable).subscribe(new Action1<List<PocketQueryList>>() { @Override public void call(final List<PocketQueryList> pocketQueryLists) { waitDialog.dismiss(); @@ -61,6 +51,7 @@ public final class PocketQueryList { } }); } + private static void selectFromPocketQueries(final Activity activity, final List<PocketQueryList> pocketQueryList, final Action1<PocketQueryList> runAfterwards) { if (CollectionUtils.isEmpty(pocketQueryList)) { ActivityMixin.showToast(activity, activity.getString(R.string.warn_no_pocket_query_found)); diff --git a/main/src/cgeo/geocaching/SearchActivity.java b/main/src/cgeo/geocaching/SearchActivity.java index edd611a..ea91857 100644 --- a/main/src/cgeo/geocaching/SearchActivity.java +++ b/main/src/cgeo/geocaching/SearchActivity.java @@ -9,10 +9,11 @@ import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.capability.ISearchByGeocode; import cgeo.geocaching.connector.trackable.TrackableConnector; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; import cgeo.geocaching.search.AutoCompleteAdapter; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.GeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.dialog.CoordinatesInputDialog; import cgeo.geocaching.ui.dialog.Dialogs; @@ -62,6 +63,8 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin @InjectView(R.id.trackable) protected AutoCompleteTextView trackableEditText; @InjectView(R.id.display_trackable) protected Button buttonSearchTrackable; + private static final String GOOGLE_NOW_SEARCH_ACTION = "com.google.android.gms.actions.SEARCH_ACTION"; + @Override public final void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -81,8 +84,8 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin return; } - // search query - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + // search query, from search toolbar or from google now + if (Intent.ACTION_SEARCH.equals(intent.getAction()) || GOOGLE_NOW_SEARCH_ACTION.equals(intent.getAction())) { hideKeyboard(); final String query = intent.getStringExtra(SearchManager.QUERY); final boolean keywordSearch = intent.getBooleanExtra(Intents.EXTRA_KEYWORD_SEARCH, true); @@ -124,7 +127,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin /** * Performs a search for query either as geocode, trackable code or keyword * - * @param query + * @param nonTrimmedQuery * String to search for * @param keywordSearch * Set to true if keyword search should be performed if query isn't GC or TB @@ -142,7 +145,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin } final IConnector connector = ConnectorFactory.getConnector(geocode); - if (connector instanceof ISearchByGeocode) { + if (connector instanceof ISearchByGeocode && geocode != null) { CacheDetailActivity.startActivity(this, geocode.toUpperCase(Locale.US)); return true; } @@ -159,7 +162,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin } } - if (trackableConnector != ConnectorFactory.UNKNOWN_TRACKABLE_CONNECTOR) { + if (trackableConnector != ConnectorFactory.UNKNOWN_TRACKABLE_CONNECTOR && geocode != null) { final Intent trackablesIntent = new Intent(this, TrackableActivity.class); trackablesIntent.putExtra(Intents.EXTRA_GEOCODE, geocode.toUpperCase(Locale.US)); startActivity(trackablesIntent); @@ -291,7 +294,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin } private void updateCoordinates() { - final CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(null, null, app.currentGeo()); + final CoordinatesInputDialog coordsDialog = CoordinatesInputDialog.getInstance(null, null, Sensors.getInstance().currentGeo()); coordsDialog.setCancelable(true); coordsDialog.show(getSupportFragmentManager(), "wpedit_dialog"); } @@ -307,14 +310,12 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin final String lonText = StringUtils.trim(buttonLongitude.getText().toString()); if (StringUtils.isEmpty(latText) || StringUtils.isEmpty(lonText)) { - final IGeoData geo = app.currentGeo(); - if (geo.getCoords() != null) { - buttonLatitude.setText(geo.getCoords().format(GeopointFormatter.Format.LAT_DECMINUTE)); - buttonLongitude.setText(geo.getCoords().format(GeopointFormatter.Format.LON_DECMINUTE)); - } + final GeoData geo = Sensors.getInstance().currentGeo(); + buttonLatitude.setText(geo.getCoords().format(GeopointFormatter.Format.LAT_DECMINUTE)); + buttonLongitude.setText(geo.getCoords().format(GeopointFormatter.Format.LON_DECMINUTE)); } else { try { - CacheListActivity.startActivityCoordinates(this, new Geopoint(latText, lonText)); + CacheListActivity.startActivityCoordinates(this, new Geopoint(latText, lonText), null); } catch (final Geopoint.ParseException e) { showToast(res.getString(e.resource)); } @@ -373,7 +374,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin } private void findByGeocodeFn() { - final String geocodeText = StringUtils.trim(geocodeEditText.getText().toString()); + final String geocodeText = StringUtils.trimToEmpty(geocodeEditText.getText().toString()); if (StringUtils.isBlank(geocodeText) || geocodeText.equalsIgnoreCase("GC")) { Dialogs.message(this, R.string.warn_search_help_title, R.string.warn_search_help_gccode); @@ -384,7 +385,7 @@ public class SearchActivity extends AbstractActionBarActivity implements Coordin } private void findTrackableFn() { - final String trackableText = StringUtils.trim(trackableEditText.getText().toString()); + final String trackableText = StringUtils.trimToEmpty(trackableEditText.getText().toString()); if (StringUtils.isBlank(trackableText) || trackableText.equalsIgnoreCase("TB")) { Dialogs.message(this, R.string.warn_search_help_title, R.string.warn_search_help_tb); diff --git a/main/src/cgeo/geocaching/SearchResult.java b/main/src/cgeo/geocaching/SearchResult.java index 74cc59d..5de1298 100644 --- a/main/src/cgeo/geocaching/SearchResult.java +++ b/main/src/cgeo/geocaching/SearchResult.java @@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import rx.Observable; +import rx.functions.Func0; import rx.functions.Func1; import rx.functions.Func2; @@ -215,10 +216,6 @@ public class SearchResult implements Parcelable { } /** - * @param excludeDisabled - * @param excludeMine - * @param cacheType - * @return */ public SearchResult filterSearchResults(final boolean excludeDisabled, final boolean excludeMine, final CacheType cacheType) { @@ -314,17 +311,17 @@ public class SearchResult implements Parcelable { public static <C extends IConnector> SearchResult parallelCombineActive(final Collection<C> connectors, final Func1<C, SearchResult> func) { - return Observable.from(connectors).parallel(new Func1<Observable<C>, Observable<SearchResult>>() { + return Observable.from(connectors).flatMap(new Func1<C, Observable<SearchResult>>() { @Override - public Observable<SearchResult> call(final Observable<C> cObservable) { - return cObservable.flatMap(new Func1<C, Observable<? extends SearchResult>>() { + public Observable<SearchResult> call(final C connector) { + return connector.isActive() ? Observable.defer(new Func0<Observable<SearchResult>>() { @Override - public Observable<? extends SearchResult> call(final C c) { - return c.isActive() ? Observable.from(func.call(c)) : Observable.<SearchResult>empty(); + public Observable<SearchResult> call() { + return Observable.just(func.call(connector)); } - }); + }).subscribeOn(RxUtils.networkScheduler) : Observable.<SearchResult>empty(); } - }, RxUtils.networkScheduler).reduce(new SearchResult(), new Func2<SearchResult, SearchResult, SearchResult>() { + }).reduce(new SearchResult(), new Func2<SearchResult, SearchResult, SearchResult>() { @Override public SearchResult call(final SearchResult searchResult, final SearchResult searchResult2) { searchResult.addSearchResult(searchResult2); diff --git a/main/src/cgeo/geocaching/SelectMapfileActivity.java b/main/src/cgeo/geocaching/SelectMapfileActivity.java index dc898d7..a506f16 100644 --- a/main/src/cgeo/geocaching/SelectMapfileActivity.java +++ b/main/src/cgeo/geocaching/SelectMapfileActivity.java @@ -33,7 +33,7 @@ public class SelectMapfileActivity extends AbstractFileListActivity<FileSelectio private String mapFile; - private static int REQUEST_DIRECTORY = 1; + private final static int REQUEST_DIRECTORY = 1; @Override public void onCreate(Bundle savedInstanceState) { @@ -53,7 +53,7 @@ public class SelectMapfileActivity extends AbstractFileListActivity<FileSelectio dirChooser.putExtra(FileManagerIntents.EXTRA_BUTTON_TEXT, getString(android.R.string.ok)); startActivityForResult(dirChooser, REQUEST_DIRECTORY); - } catch (android.content.ActivityNotFoundException ex) { + } catch (android.content.ActivityNotFoundException ignored) { // OI file manager not available final Intent dirChooser = new Intent(SelectMapfileActivity.this, SimpleDirChooser.class); dirChooser.putExtra(Intents.EXTRA_START_DIR, LocalStorage.getStorage().getAbsolutePath()); @@ -87,7 +87,7 @@ public class SelectMapfileActivity extends AbstractFileListActivity<FileSelectio for (File dir : LocalStorage.getStorages()) { folders.add(new File(dir, "mfmaps")); folders.add(new File(new File(dir, "Locus"), "mapsVector")); - folders.add(new File(dir, LocalStorage.cache)); + folders.add(new File(dir, LocalStorage.CACHE_DIRNAME)); } return folders; } @@ -98,8 +98,8 @@ public class SelectMapfileActivity extends AbstractFileListActivity<FileSelectio } @Override - public void setCurrentFile(String newFile) { - mapFile = newFile; + public void setCurrentFile(String name) { + mapFile = name; } @Override diff --git a/main/src/cgeo/geocaching/StaticMapsActivity.java b/main/src/cgeo/geocaching/StaticMapsActivity.java index ceceab9..be363f0 100644 --- a/main/src/cgeo/geocaching/StaticMapsActivity.java +++ b/main/src/cgeo/geocaching/StaticMapsActivity.java @@ -27,14 +27,11 @@ import java.util.List; @OptionsMenu(R.menu.static_maps_activity_options) public class StaticMapsActivity extends AbstractActionBarActivity { - private static final String EXTRAS_WAYPOINT = "waypoint"; - private static final String EXTRAS_DOWNLOAD = "download"; - private static final String EXTRAS_GEOCODE = "geocode"; - - @Extra(EXTRAS_DOWNLOAD) boolean download = false; - @Extra(EXTRAS_WAYPOINT) Integer waypointId = null; - @Extra(EXTRAS_GEOCODE) String geocode = null; + @Extra(Intents.EXTRA_DOWNLOAD) boolean download = false; + @Extra(Intents.EXTRA_WAYPOINT_ID) Integer waypointId = null; + @Extra(Intents.EXTRA_GEOCODE) String geocode = null; + private Geocache cache; private final List<Bitmap> maps = new ArrayList<>(); private LayoutInflater inflater = null; private ProgressDialog waitDialog = null; @@ -94,12 +91,16 @@ public class StaticMapsActivity extends AbstractActionBarActivity { public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.staticmaps_activity); - if (geocode == null) { - showToast("Sorry, c:geo forgot for what cache you want to load static maps."); + cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); + + if (cache == null) { + Log.e("StaticMapsActivity.onCreate: cannot find the cache " + geocode); finish(); return; } + setCacheTitleBar(cache); + waitDialog = ProgressDialog.show(this, null, res.getString(R.string.map_static_loading), true); waitDialog.setCancelable(true); @@ -116,7 +117,6 @@ public class StaticMapsActivity extends AbstractActionBarActivity { for (int level = 1; level <= StaticMapsProvider.MAPS_LEVEL_MAX; level++) { try { if (waypointId != null) { - final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); final Bitmap image = StaticMapsProvider.getWaypointMap(geocode, cache.getWaypointById(waypointId), level); if (image != null) { maps.add(image); @@ -150,7 +150,6 @@ public class StaticMapsActivity extends AbstractActionBarActivity { } private boolean downloadStaticMaps() { - final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); if (waypointId == null) { showToast(res.getString(R.string.info_storing_static_maps)); RxUtils.waitForCompletion(StaticMapsProvider.storeCacheStaticMap(cache)); diff --git a/main/src/cgeo/geocaching/StaticMapsProvider.java b/main/src/cgeo/geocaching/StaticMapsProvider.java index 030c379..8c399af 100644 --- a/main/src/cgeo/geocaching/StaticMapsProvider.java +++ b/main/src/cgeo/geocaching/StaticMapsProvider.java @@ -2,7 +2,7 @@ package cgeo.geocaching; import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.files.LocalStorage; -import cgeo.geocaching.geopoint.GeopointFormatter.Format; +import cgeo.geocaching.location.GeopointFormatter.Format; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; @@ -16,8 +16,7 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import rx.Observable; -import rx.functions.Action0; -import rx.util.async.Async; +import rx.functions.Func0; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -38,6 +37,8 @@ public final class StaticMapsProvider { private static final String MAP_FILENAME_PREFIX = "map_"; private static final String MARKERS_URL = "http://status.cgeo.org/assets/markers/"; + private static volatile long last403 = 0; + /** We assume there is no real usable image with less than 1k. */ private static final int MIN_MAP_IMAGE_BYTES = 1000; @@ -54,32 +55,43 @@ public final class StaticMapsProvider { return LocalStorage.getStorageFile(geocode, MAP_FILENAME_PREFIX + prefix, false, createDirs); } + private static <T> Observable<T> checkDownloadPermission(final Observable<T> ifPermitted) { + return Observable.defer(new Func0<Observable<T>>() { + @Override + public Observable<T> call() { + if (System.currentTimeMillis() - last403 >= 30000) { + return ifPermitted; + } + Log.d("StaticMaps.downloadMap: request ignored because of recent \"permission denied\" answer"); + return Observable.empty(); + } + }); + } + private static Observable<String> downloadDifferentZooms(final String geocode, final String markerUrl, final String prefix, final String latlonMap, final int width, final int height, final Parameters waypoints) { - return Observable.merge(downloadMap(geocode, 20, SATELLITE, markerUrl, prefix + '1', "", latlonMap, width, height, waypoints), + return checkDownloadPermission(Observable.merge(downloadMap(geocode, 20, SATELLITE, markerUrl, prefix + '1', "", latlonMap, width, height, waypoints), downloadMap(geocode, 18, SATELLITE, markerUrl, prefix + '2', "", latlonMap, width, height, waypoints), downloadMap(geocode, 16, ROADMAP, markerUrl, prefix + '3', "", latlonMap, width, height, waypoints), downloadMap(geocode, 14, ROADMAP, markerUrl, prefix + '4', "", latlonMap, width, height, waypoints), - downloadMap(geocode, 11, ROADMAP, markerUrl, prefix + '5', "", latlonMap, width, height, waypoints)); + downloadMap(geocode, 11, ROADMAP, markerUrl, prefix + '5', "", latlonMap, width, height, waypoints))); } private static Observable<String> downloadMap(final String geocode, final int zoom, final String mapType, final String markerUrl, final String prefix, final String shadow, final String latlonMap, final int width, final int height, final Parameters waypoints) { - int scale = 1; - if (width > GOOGLE_MAPS_MAX_SIZE) { - scale = 2; - } + // If it has been less than 30 seconds since we got a 403 (permission denied) from Google servers, + // do not try again. + final int scale = width <= GOOGLE_MAPS_MAX_SIZE ? 1 : 2; final float aspectRatio = width / (float) height; final int requestWidth = Math.min(width / scale, GOOGLE_MAPS_MAX_SIZE); final int requestHeight = (aspectRatio > 1) ? Math.round(requestWidth / aspectRatio) : requestWidth; - final int requestScale = scale; final int requestZoom = Math.min((scale == 2) ? zoom + 1 : zoom, GOOGLE_MAX_ZOOM); - return Async.fromAction(new Action0() { + return checkDownloadPermission(Observable.defer(new Func0<Observable<String>>() { @Override - public void call() { + public Observable<String> call() { final Parameters params = new Parameters( "center", latlonMap, "zoom", String.valueOf(requestZoom), "size", String.valueOf(requestWidth) + 'x' + String.valueOf(requestHeight), - "scale", String.valueOf(requestScale), + "scale", String.valueOf(scale), "maptype", mapType, "markers", "icon:" + markerUrl + '|' + shadow + latlonMap, "sensor", "false"); @@ -90,11 +102,15 @@ public final class StaticMapsProvider { if (httpResponse == null) { Log.e("StaticMapsProvider.downloadMap: httpResponse is null"); - return; + return Observable.just(prefix); } - if (httpResponse.getStatusLine().getStatusCode() != 200) { - Log.d("StaticMapsProvider.downloadMap: httpResponseCode = " + httpResponse.getStatusLine().getStatusCode()); - return; + final int statusCode = httpResponse.getStatusLine().getStatusCode(); + if (statusCode != 200) { + Log.d("StaticMapsProvider.downloadMap: httpResponseCode = " + statusCode); + if (statusCode == 403) { + last403 = System.currentTimeMillis(); + } + return Observable.just(prefix); } final File file = getMapFile(geocode, prefix, true); if (LocalStorage.saveEntityToFile(httpResponse, file)) { @@ -104,8 +120,9 @@ public final class StaticMapsProvider { FileUtils.deleteIgnoringFailure(file); } } + return Observable.just(prefix); } - }, prefix, RxUtils.networkScheduler); + }).subscribeOn(RxUtils.networkScheduler)); } public static Observable<String> downloadMaps(final Geocache cache) { @@ -132,16 +149,11 @@ public final class StaticMapsProvider { } - return Observable.merge(downloaders); + return checkDownloadPermission(Observable.merge(downloaders)); } /** * Deletes and download all Waypoints static maps. - * - * @param cache - * The cache instance - * @param edge - * The boundings */ private static Observable<String> refreshAllWpStaticMaps(final Geocache cache, final int width, final int height) { LocalStorage.deleteFilesWithPrefix(cache.getGeocode(), MAP_FILENAME_PREFIX + WAYPOINT_PREFIX); @@ -149,7 +161,7 @@ public final class StaticMapsProvider { for (final Waypoint waypoint : cache.getWaypoints()) { downloaders.add(storeWaypointStaticMap(cache.getGeocode(), width, height, waypoint)); } - return Observable.merge(downloaders); + return checkDownloadPermission(Observable.merge(downloaders)); } public static Observable<String> storeWaypointStaticMap(final Geocache cache, final Waypoint waypoint) { @@ -203,9 +215,8 @@ public final class StaticMapsProvider { public static Observable<String> storeCachePreviewMap(final Geocache cache) { final String latlonMap = cache.getCoords().format(Format.LAT_LON_DECDEGREE_COMMA); final Point displaySize = Compatibility.getDisplaySize(); - final int minSize = Math.min(displaySize.x, displaySize.y); final String markerUrl = MARKERS_URL + "my_location_mdpi.png"; - return downloadMap(cache.getGeocode(), 15, ROADMAP, markerUrl, PREFIX_PREVIEW, "shadow:false|", latlonMap, minSize, minSize, null); + return downloadMap(cache.getGeocode(), 15, ROADMAP, markerUrl, PREFIX_PREVIEW, "shadow:false|", latlonMap, displaySize.x, displaySize.y, null); } private static Observable<String> downloadMaps(final String geocode, final String markerUrl, final String prefix, @@ -248,7 +259,6 @@ public final class StaticMapsProvider { /** * Check if at least one map file exists for the given cache. * - * @param cache * @return <code>true</code> if at least one map file exists; <code>false</code> otherwise */ public static boolean hasStaticMap(@NonNull final Geocache cache) { @@ -268,8 +278,6 @@ public final class StaticMapsProvider { /** * Checks if at least one map file exists for the given geocode and waypoint ID. * - * @param geocode - * @param waypoint * @return <code>true</code> if at least one map file exists; <code>false</code> otherwise */ public static boolean hasStaticMapForWaypoint(final String geocode, final Waypoint waypoint) { @@ -287,8 +295,6 @@ public final class StaticMapsProvider { /** * Checks if all map files exist for the given geocode and waypoint ID. * - * @param geocode - * @param waypoint * @return <code>true</code> if all map files exist; <code>false</code> otherwise */ public static boolean hasAllStaticMapsForWaypoint(final String geocode, final Waypoint waypoint) { diff --git a/main/src/cgeo/geocaching/StatusFragment.java b/main/src/cgeo/geocaching/StatusFragment.java index a228363..acc7011 100644 --- a/main/src/cgeo/geocaching/StatusFragment.java +++ b/main/src/cgeo/geocaching/StatusFragment.java @@ -1,15 +1,15 @@ package cgeo.geocaching; import butterknife.ButterKnife; +import butterknife.InjectView; import cgeo.geocaching.network.StatusUpdater; import cgeo.geocaching.network.StatusUpdater.Status; import cgeo.geocaching.utils.Log; import rx.Subscription; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action1; -import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; import android.content.Intent; @@ -26,15 +26,17 @@ import android.widget.TextView; public class StatusFragment extends Fragment { + protected @InjectView(R.id.status_icon) ImageView statusIcon; + protected @InjectView(R.id.status_message) TextView statusMessage; + private Subscription statusSubscription = Subscriptions.empty(); @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); 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()) + ButterKnife.inject(this, statusGroup); + statusSubscription = AppObservable.bindFragment(this, StatusUpdater.latestStatus) .subscribe(new Action1<Status>() { @Override public void call(final Status status) { @@ -89,6 +91,7 @@ public class StatusFragment extends Fragment { public void onDestroyView() { statusSubscription.unsubscribe(); super.onDestroyView(); + ButterKnife.reset(this); } } diff --git a/main/src/cgeo/geocaching/Trackable.java b/main/src/cgeo/geocaching/Trackable.java index 9c2b044..6ec8c0d 100644 --- a/main/src/cgeo/geocaching/Trackable.java +++ b/main/src/cgeo/geocaching/Trackable.java @@ -3,17 +3,21 @@ 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 org.eclipse.jdt.annotation.NonNull; 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 { - static final public int SPOTTED_UNSET = 0; + static final private int SPOTTED_UNSET = 0; static final public int SPOTTED_CACHE = 1; static final public int SPOTTED_USER = 2; static final public int SPOTTED_UNKNOWN = 3; @@ -38,10 +42,19 @@ public class Trackable implements ILogable { private List<LogEntry> logs = new ArrayList<>(); private String trackingcode = null; + /** + * Check whether this trackable has a corresponding URL. + */ + public boolean hasUrl() { + return getConnector().hasTrackableUrls(); + } + + @NonNull public String getUrl() { return getConnector().getUrl(this); } + @NonNull private TrackableConnector getConnector() { return ConnectorFactory.getConnector(this); } @@ -50,7 +63,7 @@ public class Trackable implements ILogable { return guid; } - public void setGuid(String guid) { + public void setGuid(final String guid) { this.guid = guid; } @@ -59,7 +72,7 @@ public class Trackable implements ILogable { return geocode; } - public void setGeocode(String geocode) { + public void setGeocode(final String geocode) { this.geocode = StringUtils.upperCase(geocode); } @@ -67,7 +80,7 @@ public class Trackable implements ILogable { return iconUrl; } - public void setIconUrl(String iconUrl) { + public void setIconUrl(final String iconUrl) { this.iconUrl = iconUrl; } @@ -76,7 +89,7 @@ public class Trackable implements ILogable { return name; } - public void setName(String name) { + public void setName(final String name) { this.name = name; } @@ -84,7 +97,7 @@ public class Trackable implements ILogable { return type; } - public void setType(String type) { + public void setType(final String type) { this.type = type; } @@ -92,7 +105,7 @@ public class Trackable implements ILogable { return released; } - public void setReleased(Date released) { + public void setReleased(final Date released) { if (released == null) { this.released = null; } @@ -105,7 +118,7 @@ public class Trackable implements ILogable { return distance; } - public void setDistance(float distance) { + public void setDistance(final float distance) { this.distance = distance; } @@ -113,7 +126,7 @@ public class Trackable implements ILogable { return origin; } - public void setOrigin(String origin) { + public void setOrigin(final String origin) { this.origin = origin; } @@ -121,7 +134,7 @@ public class Trackable implements ILogable { return owner; } - public void setOwner(String owner) { + public void setOwner(final String owner) { this.owner = owner; } @@ -129,7 +142,7 @@ public class Trackable implements ILogable { return ownerGuid; } - public void setOwnerGuid(String ownerGuid) { + public void setOwnerGuid(final String ownerGuid) { this.ownerGuid = ownerGuid; } @@ -137,7 +150,7 @@ public class Trackable implements ILogable { return spottedName; } - public void setSpottedName(String spottedName) { + public void setSpottedName(final String spottedName) { this.spottedName = spottedName; } @@ -145,7 +158,7 @@ public class Trackable implements ILogable { return spottedType; } - public void setSpottedType(int spottedType) { + public void setSpottedType(final int spottedType) { this.spottedType = spottedType; } @@ -153,7 +166,7 @@ public class Trackable implements ILogable { return spottedGuid; } - public void setSpottedGuid(String spottedGuid) { + public void setSpottedGuid(final String spottedGuid) { this.spottedGuid = spottedGuid; } @@ -161,7 +174,7 @@ public class Trackable implements ILogable { return goal; } - public void setGoal(String goal) { + public void setGoal(final String goal) { this.goal = goal; } @@ -169,7 +182,7 @@ public class Trackable implements ILogable { return details; } - public void setDetails(String details) { + public void setDetails(final String details) { this.details = details; } @@ -177,15 +190,16 @@ public class Trackable implements ILogable { return image; } - public void setImage(String image) { + public void setImage(final String image) { this.image = image; } + @NonNull public List<LogEntry> getLogs() { return logs; } - public void setLogs(List<LogEntry> logs) { + public void setLogs(final List<LogEntry> logs) { this.logs = logs != null ? logs : new ArrayList<LogEntry>(); } @@ -210,11 +224,25 @@ public class Trackable implements ILogable { return trackingcode; } - public void setTrackingcode(String trackingcode) { + public void setTrackingcode(final String trackingcode) { this.trackingcode = trackingcode; } - static public List<LogType> getPossibleLogTypes() { + @NonNull + 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, geocode, getDetails()); + for (final LogEntry log : getLogs()) { + images.addAll(log.getLogImages()); + } + return images; + } + + @NonNull + static List<LogType> getPossibleLogTypes() { final List<LogType> logTypes = new ArrayList<>(); logTypes.add(LogType.RETRIEVED_IT); logTypes.add(LogType.GRABBED_IT); diff --git a/main/src/cgeo/geocaching/TrackableActivity.java b/main/src/cgeo/geocaching/TrackableActivity.java index 0e784bd..7d10d34 100644 --- a/main/src/cgeo/geocaching/TrackableActivity.java +++ b/main/src/cgeo/geocaching/TrackableActivity.java @@ -4,40 +4,46 @@ 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; import cgeo.geocaching.connector.trackable.TravelBugConnector; import cgeo.geocaching.enumerations.LogType; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.network.AndroidBeam; 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 org.eclipse.jdt.annotation.Nullable; -import rx.android.observables.AndroidObservable; -import rx.android.observables.ViewObservable; +import rx.Observable; +import rx.android.app.AppObservable; +import rx.android.view.OnClickEvent; +import rx.android.view.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; @@ -57,11 +63,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivity.Page> implements ActivitySharingInterface { +public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivity.Page> implements AndroidBeam.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; @@ -77,50 +86,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(); - } - - } - }; - private CharSequence clickedItemText = null; + private ImagesList imagesList = null; + /** * Action mode of the current contextual action bar (e.g. for copy and share actions). */ @@ -129,16 +97,17 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.viewpager_activity); + createSubscriptions = new CompositeSubscription(); // set title in code, as the activity needs a hard coded title due to the intent filters setTitle(res.getString(R.string.trackable)); // get parameters final Bundle extras = getIntent().getExtras(); - final Uri uri = getIntent().getData(); - // try to get data from extras + final Uri uri = AndroidBeam.getUri(getIntent()); if (extras != null) { + // try to get data from extras geocode = extras.getString(Intents.EXTRA_GEOCODE); name = extras.getString(Intents.EXTRA_NAME); guid = extras.getString(Intents.EXTRA_GUID); @@ -150,7 +119,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi geocode = ConnectorFactory.getTrackableFromURL(uri.toString()); final String uriHost = uri.getHost().toLowerCase(Locale.US); - if (uriHost.contains("geocaching.com")) { + if (uriHost.endsWith("geocaching.com")) { geocode = uri.getQueryParameter("tracker"); guid = uri.getQueryParameter("guid"); id = uri.getQueryParameter("id"); @@ -172,17 +141,6 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi finish(); return; } - } else if (uriHost.contains("coord.info")) { - final String uriPath = uri.getPath().toLowerCase(Locale.US); - if (StringUtils.startsWith(uriPath, "/tb")) { - geocode = uriPath.substring(1).toUpperCase(Locale.US); - guid = null; - id = null; - } else { - showToast(res.getString(R.string.err_tb_details_open)); - finish(); - return; - } } } @@ -193,7 +151,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi return; } - String message; + final String message; if (StringUtils.isNotBlank(name)) { message = Html.fromHtml(name).toString(); } else if (StringUtils.isNotBlank(geocode)) { @@ -201,17 +159,34 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi } else { message = res.getString(R.string.trackable); } - waitDialog = ProgressDialog.show(this, message, res.getString(R.string.trackable_details_loading), true, true); - // If we have a newer Android device setup Android Beam for easy cache sharing - initializeAndroidBeam(this); + AndroidBeam.enable(this, this); - createViewPager(0, null); - final LoadTrackableThread thread = new LoadTrackableThread(loadTrackableHandler, geocode, guid, id); - thread.start(); + createViewPager(0, new OnPageSelectedListener() { + @Override + public void onPageSelected(final int position) { + // Lazy loading of trackable images + if (getPage(position) == Page.IMAGES) { + loadTrackableImages(); + } + } + }); + refreshTrackable(message); } + private void refreshTrackable(final String message) { + waitDialog = ProgressDialog.show(this, message, res.getString(R.string.trackable_details_loading), true, true); + createSubscriptions.add(AppObservable.bindActivity(this, loadTrackable(geocode, guid, id)).singleOrDefault(null).subscribe(new Action1<Trackable>() { + @Override + public void call(final Trackable trackable) { + TrackableActivity.this.trackable = trackable; + displayTrackable(); + } + })); + } + + @Nullable @Override public String getAndroidBeamUri() { return trackable != null ? trackable.getUrl() : null; @@ -227,7 +202,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_log_touch: - LogTrackableActivity.startActivity(this, trackable); + startActivityForResult(LogTrackableActivity.getIntent(this, trackable), LogTrackableActivity.LOG_TRACKABLE); return true; case R.id.menu_browser_trackable: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(trackable.getUrl()))); @@ -240,92 +215,90 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi public boolean onPrepareOptionsMenu(final Menu menu) { if (trackable != null) { menu.findItem(R.id.menu_log_touch).setVisible(StringUtils.isNotBlank(geocode) && trackable.isLoggable()); - menu.findItem(R.id.menu_browser_trackable).setVisible(StringUtils.isNotBlank(trackable.getUrl())); + menu.findItem(R.id.menu_browser_trackable).setVisible(trackable.hasUrl()); } 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(); + } - public TrackableIconThread(final String urlIn, final Handler handlerIn) { - url = urlIn; - handler = handlerIn; + 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; } - @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); + AppObservable.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,11 +316,38 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi case DETAILS: return new DetailsViewCreator(); case LOGS: - return new TrackableLogsViewCreator(this, trackable); + 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); @@ -357,9 +357,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); } @@ -382,16 +385,14 @@ 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 - addContextMenu(details.add(R.string.trackable_name, StringUtils.isNotBlank(trackable.getName()) ? Html.fromHtml(trackable.getName()).toString() : res.getString(R.string.trackable_unknown))); + addContextMenu(details.add(R.string.trackable_name, StringUtils.isNotBlank(trackable.getName()) ? Html.fromHtml(trackable.getName()).toString() : res.getString(R.string.trackable_unknown)).right); // trackable type - String tbType; + final String tbType; if (StringUtils.isNotBlank(trackable.getType())) { tbType = Html.fromHtml(trackable.getType()).toString(); } else { @@ -400,10 +401,10 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi details.add(R.string.trackable_type, tbType); // trackable geocode - addContextMenu(details.add(R.string.trackable_code, trackable.getGeocode())); + addContextMenu(details.add(R.string.trackable_code, trackable.getGeocode()).right); // trackable owner - final TextView owner = details.add(R.string.trackable_owner, res.getString(R.string.trackable_unknown)); + final TextView owner = details.add(R.string.trackable_owner, res.getString(R.string.trackable_unknown)).right; if (StringUtils.isNotBlank(trackable.getOwner())) { owner.setText(Html.fromHtml(trackable.getOwner()), TextView.BufferType.SPANNABLE); owner.setOnClickListener(new UserActionsClickListener(trackable)); @@ -413,34 +414,39 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi if (StringUtils.isNotBlank(trackable.getSpottedName()) || trackable.getSpottedType() == Trackable.SPOTTED_UNKNOWN || trackable.getSpottedType() == Trackable.SPOTTED_OWNER) { + + final StringBuilder text; boolean showTimeSpan = true; - StringBuilder text; - - if (trackable.getSpottedType() == Trackable.SPOTTED_CACHE) { - text = new StringBuilder(res.getString(R.string.trackable_spotted_in_cache) + ' ' + Html.fromHtml(trackable.getSpottedName()).toString()); - } else if (trackable.getSpottedType() == Trackable.SPOTTED_USER) { - text = new StringBuilder(res.getString(R.string.trackable_spotted_at_user) + ' ' + Html.fromHtml(trackable.getSpottedName()).toString()); - } else if (trackable.getSpottedType() == Trackable.SPOTTED_UNKNOWN) { - text = new StringBuilder(res.getString(R.string.trackable_spotted_unknown_location)); - } else if (trackable.getSpottedType() == Trackable.SPOTTED_OWNER) { - text = new StringBuilder(res.getString(R.string.trackable_spotted_owner)); - } else { - text = new StringBuilder("N/A"); - showTimeSpan = false; + switch (trackable.getSpottedType()) { + case Trackable.SPOTTED_CACHE: + text = new StringBuilder(res.getString(R.string.trackable_spotted_in_cache) + ' ' + Html.fromHtml(trackable.getSpottedName()).toString()); + break; + case Trackable.SPOTTED_USER: + text = new StringBuilder(res.getString(R.string.trackable_spotted_at_user) + ' ' + Html.fromHtml(trackable.getSpottedName()).toString()); + break; + case Trackable.SPOTTED_UNKNOWN: + text = new StringBuilder(res.getString(R.string.trackable_spotted_unknown_location)); + break; + case Trackable.SPOTTED_OWNER: + text = new StringBuilder(res.getString(R.string.trackable_spotted_owner)); + break; + default: + text = new StringBuilder("N/A"); + showTimeSpan = false; + break; } // days since last spotting - if (showTimeSpan && trackable.getLogs() != null) { + if (showTimeSpan) { 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; } } } - final TextView spotted = details.add(R.string.trackable_spotted, text.toString()); + final TextView spotted = details.add(R.string.trackable_spotted, text.toString()).right; spotted.setClickable(true); if (Trackable.SPOTTED_CACHE == trackable.getSpottedType()) { spotted.setOnClickListener(new View.OnClickListener() { @@ -467,19 +473,19 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi // trackable origin if (StringUtils.isNotBlank(trackable.getOrigin())) { - final TextView origin = details.add(R.string.trackable_origin, ""); + final TextView origin = details.add(R.string.trackable_origin, "").right; origin.setText(Html.fromHtml(trackable.getOrigin()), TextView.BufferType.SPANNABLE); addContextMenu(origin); } // trackable released if (trackable.getReleased() != null) { - addContextMenu(details.add(R.string.trackable_released, Formatter.formatDate(trackable.getReleased().getTime()))); + addContextMenu(details.add(R.string.trackable_released, Formatter.formatDate(trackable.getReleased().getTime())).right); } // trackable distance if (trackable.getDistance() >= 0) { - addContextMenu(details.add(R.string.trackable_distance, Units.getDistanceFromKilometers(trackable.getDistance()))); + addContextMenu(details.add(R.string.trackable_distance, Units.getDistanceFromKilometers(trackable.getDistance())).right); } // trackable goal @@ -507,14 +513,14 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi trackableImage.setImageResource(R.drawable.image_not_loaded); trackableImage.setClickable(true); - ViewObservable.clicks(trackableImage, false).subscribe(new Action1<View>() { + ViewObservable.clicks(trackableImage, false).subscribe(new Action1<OnClickEvent>() { @Override - public void call(final View view) { + public void call(final OnClickEvent onClickEvent) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(trackable.getImage()))); } }); - AndroidObservable.bindActivity(TrackableActivity.this, new HtmlImage(geocode, true, 0, false).fetchDrawable(trackable.getImage())).subscribe(new Action1<BitmapDrawable>() { + AppObservable.bindActivity(TrackableActivity.this, new HtmlImage(geocode, true, 0, false).fetchDrawable(trackable.getImage())).subscribe(new Action1<BitmapDrawable>() { @Override public void call(final BitmapDrawable bitmapDrawable) { trackableImage.setImageDrawable(bitmapDrawable); @@ -555,6 +561,10 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi @Override public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) { + return prepareClipboardActionMode(view, actionMode, menu); + } + + private boolean prepareClipboardActionMode(final View view, final ActionMode actionMode, final Menu menu) { final int viewId = view.getId(); assert view instanceof TextView; clickedItemText = ((TextView) view).getText(); @@ -584,6 +594,7 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi @Override public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) { actionMode.getMenuInflater().inflate(R.menu.details_context, menu); + prepareClipboardActionMode(view, actionMode, menu); return true; } @@ -595,4 +606,21 @@ public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivi return false; } + @Override + protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { + // Refresh the logs view after coming back from logging a trackable + if (requestCode == LogTrackableActivity.LOG_TRACKABLE && resultCode == RESULT_OK) { + refreshTrackable(StringUtils.defaultIfBlank(trackable.getName(), trackable.getGeocode())); + } + } + + @Override + protected void onDestroy() { + createSubscriptions.unsubscribe(); + super.onDestroy(); + } + + public Trackable getTrackable() { + return trackable; + } } diff --git a/main/src/cgeo/geocaching/UsefulAppsActivity.java b/main/src/cgeo/geocaching/UsefulAppsActivity.java index a2cdaf7..d4517dc 100644 --- a/main/src/cgeo/geocaching/UsefulAppsActivity.java +++ b/main/src/cgeo/geocaching/UsefulAppsActivity.java @@ -5,8 +5,10 @@ import butterknife.InjectView; import cgeo.geocaching.activity.AbstractActionBarActivity; import cgeo.geocaching.ui.AbstractViewHolder; +import cgeo.geocaching.utils.ProcessUtils; + +import org.eclipse.jdt.annotation.NonNull; -import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -36,27 +38,16 @@ public class UsefulAppsActivity extends AbstractActionBarActivity { private final int titleId; private final int descriptionId; private final int iconId; + @NonNull private final String packageName; - public HelperApp(final int title, final int description, final int icon, final String packageName) { + public HelperApp(final int title, final int description, final int icon, @NonNull final String packageName) { this.titleId = title; this.descriptionId = description; this.iconId = icon; this.packageName = packageName; } - private void installFromMarket(final Activity activity) { - try { - // allow also opening pure http URLs in addition to market packages - final String url = (packageName.startsWith("http:")) ? packageName : "market://details?id=" + packageName; - final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - marketIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - activity.startActivity(marketIntent); - - } catch (final RuntimeException e) { - // market not available in standard emulator - } - } } private static final HelperApp[] HELPER_APPS = { @@ -106,7 +97,12 @@ public class UsefulAppsActivity extends AbstractActionBarActivity { @Override public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) { final HelperApp helperApp = HELPER_APPS[position]; - helperApp.installFromMarket(UsefulAppsActivity.this); + if (helperApp.packageName.startsWith("http")) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(helperApp.packageName))); + } + else { + ProcessUtils.openMarket(UsefulAppsActivity.this, helperApp.packageName); + } } }); } diff --git a/main/src/cgeo/geocaching/Waypoint.java b/main/src/cgeo/geocaching/Waypoint.java index 7381aab..cc00b1c 100644 --- a/main/src/cgeo/geocaching/Waypoint.java +++ b/main/src/cgeo/geocaching/Waypoint.java @@ -2,7 +2,7 @@ package cgeo.geocaching; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.WaypointType; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.utils.MatcherWrapper; import org.apache.commons.lang3.StringUtils; @@ -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; @@ -37,8 +37,6 @@ public class Waypoint implements IWaypoint { /** * require name and type for every waypoint * - * @param name - * @param type */ public Waypoint(final String name, final WaypointType type, final boolean own) { this.name = name; @@ -49,7 +47,6 @@ public class Waypoint implements IWaypoint { /** * copy constructor * - * @param other */ public Waypoint(final Waypoint other) { merge(other); @@ -67,9 +64,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; } @@ -100,18 +94,11 @@ public class Waypoint implements IWaypoint { if (newPrefixes.containsKey(prefix)) { newPrefixes.get(prefix).merge(oldWaypoint); } else if (oldWaypoint.isUserDefined() || forceMerge) { - // personal note waypoints should always be taken from the new list only - if (!isPersonalNoteWaypoint(oldWaypoint)) { - newPoints.add(oldWaypoint); - } + newPoints.add(oldWaypoint); } } } - private static boolean isPersonalNoteWaypoint(final @NonNull Waypoint waypoint) { - return StringUtils.startsWith(waypoint.getName(), CgeoApplication.getInstance().getString(R.string.cache_personal_note) + " "); - } - public boolean isUserDefined() { return own || WaypointType.OWN == waypointType; } @@ -160,6 +147,7 @@ public class Waypoint implements IWaypoint { cachedOrder = ORDER_UNDEFINED; } + @NonNull public String getUrl() { return "http://www.geocaching.com/seek/cache_details.aspx?wp=" + geocode; } @@ -204,14 +192,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; @@ -279,7 +259,6 @@ public class Waypoint implements IWaypoint { /** * Delegates the creation of the waypoint-id for gpx-export to the waypoint * - * @return */ public String getGpxId() { @@ -303,10 +282,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 { @@ -316,17 +294,16 @@ public class Waypoint implements IWaypoint { ((point.getLatitudeE6() % 1000) != 0 || (point.getLongitudeE6() % 1000) != 0)) { final String name = CgeoApplication.getInstance().getString(R.string.cache_personal_note) + " " + count; final String potentialWaypointType = note.substring(Math.max(0, matcher.start() - 15)); - final Waypoint waypoint = new Waypoint(name, parseWaypointType(potentialWaypointType), false); + final Waypoint waypoint = new Waypoint(name, parseWaypointType(potentialWaypointType), true); waypoint.setCoords(point); waypoints.add(waypoint); count++; } - } catch (final Geopoint.ParseException e) { - // ignore + } catch (final Geopoint.ParseException ignored) { } 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/WaypointPopupFragment.java b/main/src/cgeo/geocaching/WaypointPopupFragment.java index 03d95e5..227e30d 100644 --- a/main/src/cgeo/geocaching/WaypointPopupFragment.java +++ b/main/src/cgeo/geocaching/WaypointPopupFragment.java @@ -4,9 +4,9 @@ import butterknife.ButterKnife; import butterknife.InjectView; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.ui.CacheDetailsCreator; import cgeo.geocaching.utils.Log; @@ -48,8 +48,8 @@ public class WaypointPopupFragment extends AbstractDialogFragment { } @Override - protected void onUpdateGeoData(IGeoData geo) { - if (geo.getCoords() != null && waypoint != null && waypoint.getCoords() != null) { + protected void onUpdateGeoData(GeoData geo) { + if (waypoint != null && waypoint.getCoords() != null) { waypointDistance.setText(Units.getDistanceFromKilometers(geo.getCoords().distanceTo(waypoint.getCoords()))); waypointDistance.bringToFront(); } @@ -60,6 +60,13 @@ public class WaypointPopupFragment extends AbstractDialogFragment { super.init(); waypoint = DataStore.loadWaypoint(waypointId); + + if (waypoint == null) { + Log.e("WaypointPopupFragment.init: unable to get waypoint " + waypointId); + getActivity().finish(); + return; + } + try { if (StringUtils.isNotBlank(waypoint.getName())) { setTitle(waypoint.getName()); diff --git a/main/src/cgeo/geocaching/activity/AbstractActivity.java b/main/src/cgeo/geocaching/activity/AbstractActivity.java index cc9931a..6ceead0 100644 --- a/main/src/cgeo/geocaching/activity/AbstractActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractActivity.java @@ -3,7 +3,10 @@ package cgeo.geocaching.activity; import butterknife.ButterKnife; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import cgeo.geocaching.enumerations.CacheType; +import cgeo.geocaching.network.AndroidBeam; import cgeo.geocaching.network.Cookies; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.ClipboardUtils; @@ -13,18 +16,13 @@ import cgeo.geocaching.utils.TranslationUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import rx.Subscription; import rx.subscriptions.Subscriptions; -import android.annotation.TargetApi; import android.content.Intent; import android.content.res.Resources; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.NfcEvent; -import android.os.Build; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.support.v7.view.ActionMode; @@ -75,6 +73,7 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); initializeCommonFields(); + AndroidBeam.disable(this); } @Override @@ -96,9 +95,9 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs return super.onOptionsItemSelected(item); } - public void onResume(final Subscription resumeSubscription) { + public void onResume(final Subscription... resumeSubscriptions) { super.onResume(); - this.resumeSubscription = resumeSubscription; + this.resumeSubscription = Subscriptions.from(resumeSubscriptions); } @Override @@ -203,34 +202,21 @@ public abstract class AbstractActivity extends ActionBarActivity implements IAbs } } - // Do not support older devices than Android 4.0 - // Although there even are 2.3 devices (Nexus S) - // 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 or null for no sharing. */ - public String getAndroidBeamUri(); - } - - protected void initializeAndroidBeam(final ActivitySharingInterface sharingInterface) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - initializeICSAndroidBeam(sharingInterface); + protected void setCacheTitleBar(@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)); } - } - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - protected void initializeICSAndroidBeam(final ActivitySharingInterface sharingInterface) { - final NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (nfcAdapter == null) { - return; + if (type != null) { + getSupportActionBar().setIcon(getResources().getDrawable(type.markerId)); + } else { + getSupportActionBar().setIcon(android.R.color.transparent); } - nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { - @Override - public NdefMessage createNdefMessage(final NfcEvent event) { - final String uri = sharingInterface.getAndroidBeamUri(); - return uri != null ? new NdefMessage(new NdefRecord[]{NdefRecord.createUri(uri)}) : null; - } - }, this); + } + protected void setCacheTitleBar(final @NonNull Geocache cache) { + setCacheTitleBar(cache.getGeocode(), cache.getName(), cache.getType()); } + } diff --git a/main/src/cgeo/geocaching/activity/AbstractListActivity.java b/main/src/cgeo/geocaching/activity/AbstractListActivity.java index d7482c3..86ca98f 100644 --- a/main/src/cgeo/geocaching/activity/AbstractListActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractListActivity.java @@ -1,6 +1,7 @@ package cgeo.geocaching.activity; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.network.AndroidBeam; import android.content.res.Resources; import android.os.Bundle; @@ -48,6 +49,7 @@ public abstract class AbstractListActivity extends ActionBarListActivity impleme initializeCommonFields(); initUpAction(); + AndroidBeam.disable(this); } protected void initUpAction() { diff --git a/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java b/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java index 64186a0..11a5436 100644 --- a/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java +++ b/main/src/cgeo/geocaching/activity/AbstractViewPagerActivity.java @@ -68,14 +68,12 @@ public abstract class AbstractViewPagerActivity<Page extends Enum<Page>> extends /** * Returns a validated view. * - * @return */ public View getDispatchedView(final ViewGroup parentView); /** * Returns a (maybe cached) view. * - * @return */ public View getView(final ViewGroup parentView); diff --git a/main/src/cgeo/geocaching/activity/ActionBarListActivity.java b/main/src/cgeo/geocaching/activity/ActionBarListActivity.java index 07c7ec3..a2d3ddf 100644 --- a/main/src/cgeo/geocaching/activity/ActionBarListActivity.java +++ b/main/src/cgeo/geocaching/activity/ActionBarListActivity.java @@ -18,12 +18,12 @@ public class ActionBarListActivity extends ActionBarActivity { return mListView; } - protected void setListAdapter(ListAdapter adapter) { + protected void setListAdapter(final ListAdapter adapter) { getListView().setAdapter(adapter); } protected ListAdapter getListAdapter() { - ListAdapter adapter = getListView().getAdapter(); + final ListAdapter adapter = getListView().getAdapter(); if (adapter instanceof HeaderViewListAdapter) { return ((HeaderViewListAdapter)adapter).getWrappedAdapter(); } diff --git a/main/src/cgeo/geocaching/activity/ActivityMixin.java b/main/src/cgeo/geocaching/activity/ActivityMixin.java index 28042b0..01fc62e 100644 --- a/main/src/cgeo/geocaching/activity/ActivityMixin.java +++ b/main/src/cgeo/geocaching/activity/ActivityMixin.java @@ -1,11 +1,14 @@ package cgeo.geocaching.activity; +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.MainActivity; import cgeo.geocaching.R; import cgeo.geocaching.settings.Settings; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Intent; import android.os.Build.VERSION; @@ -111,10 +114,15 @@ public final class ActivityMixin { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } if (Settings.useHardwareAcceleration()) { - window.setFlags(LayoutParams.FLAG_HARDWARE_ACCELERATED, LayoutParams.FLAG_HARDWARE_ACCELERATED); + enableHardwareAcceleration(window); } } + @TargetApi(VERSION_CODES.HONEYCOMB) + private static void enableHardwareAcceleration(final Window window) { + window.addFlags(LayoutParams.FLAG_HARDWARE_ACCELERATED); + } + public static void invalidateOptionsMenu(final Activity activity) { if (activity instanceof ActionBarActivity) { ((ActionBarActivity) activity).supportInvalidateOptionsMenu(); @@ -127,8 +135,6 @@ public final class ActivityMixin { /** * insert text into the EditText at the current cursor position * - * @param editText - * @param insertText * @param moveCursor * place the cursor after the inserted text */ @@ -139,7 +145,7 @@ public final class ActivityMixin { final int end = Math.max(selectionStart, selectionEnd); final String content = editText.getText().toString(); - String completeText; + final String completeText; if (start > 0 && !Character.isWhitespace(content.charAt(start - 1))) { completeText = " " + insertText; } else { @@ -152,13 +158,18 @@ public final class ActivityMixin { } public static boolean navigateUp(@NonNull final Activity activity) { - // see http://developer.android.com/training/implementing-navigation/ancestral.html - final Intent upIntent = NavUtils.getParentActivityIntent(activity); - if (upIntent == null) { + // first check if there is a parent declared in the manifest + Intent upIntent = NavUtils.getParentActivityIntent(activity); + // if there is no parent, and if this was not a new task, then just go back to simulate going to a parent + if (upIntent == null && !activity.isTaskRoot()) { activity.finish(); return true; } - if (NavUtils.shouldUpRecreateTask(activity, upIntent)) { + // use the main activity, if there was no back stack and no manifest based parent + if (upIntent == null) { + upIntent = new Intent(CgeoApplication.getInstance(), MainActivity.class); + } + if (NavUtils.shouldUpRecreateTask(activity, upIntent) || activity.isTaskRoot()) { // This activity is NOT part of this app's task, so create a new task // when navigating up, with a synthesized back stack. TaskStackBuilder.create(activity) @@ -175,7 +186,7 @@ public final class ActivityMixin { } public static void presentShowcase(final IAbstractActivity activity) { - if (VERSION.SDK_INT < 11) { + if (VERSION.SDK_INT < 14) { return; } final ShowcaseViewBuilder builder = activity.getShowcase(); diff --git a/main/src/cgeo/geocaching/network/OAuthAuthorizationActivity.java b/main/src/cgeo/geocaching/activity/OAuthAuthorizationActivity.java index eb56f0b..2dfac5b 100644 --- a/main/src/cgeo/geocaching/network/OAuthAuthorizationActivity.java +++ b/main/src/cgeo/geocaching/activity/OAuthAuthorizationActivity.java @@ -1,13 +1,17 @@ -package cgeo.geocaching.network; +package cgeo.geocaching.activity; import butterknife.InjectView; import cgeo.geocaching.Intents; import cgeo.geocaching.R; -import cgeo.geocaching.activity.AbstractActivity; +import cgeo.geocaching.network.Network; +import cgeo.geocaching.network.OAuth; +import cgeo.geocaching.network.OAuthTokens; +import cgeo.geocaching.network.Parameters; import cgeo.geocaching.utils.BundleUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; +import cgeo.geocaching.utils.RxUtils; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.ParseException; @@ -19,6 +23,8 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import rx.functions.Action0; + import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; @@ -40,6 +46,8 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { private static final int STATUS_ERROR = 0; private static final int STATUS_SUCCESS = 1; private static final int STATUS_ERROR_EXT_MSG = 2; + private static final Pattern PARAMS_PATTERN_1 = Pattern.compile("oauth_token=([\\w_.-]+)"); + private static final Pattern PARAMS_PATTERN_2 = Pattern.compile("oauth_token_secret=([\\w_.-]+)"); @NonNull private String host = StringUtils.EMPTY; @NonNull private String pathRequest = StringUtils.EMPTY; @@ -51,18 +59,16 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { @NonNull private String callback = StringUtils.EMPTY; private String OAtoken = null; private String OAtokenSecret = null; - private final Pattern paramsPattern1 = Pattern.compile("oauth_token=([a-zA-Z0-9\\-\\_.]+)"); - private final Pattern paramsPattern2 = Pattern.compile("oauth_token_secret=([a-zA-Z0-9\\-\\_.]+)"); @InjectView(R.id.start) protected Button startButton; @InjectView(R.id.auth_1) protected TextView auth_1; @InjectView(R.id.auth_2) protected TextView auth_2; private ProgressDialog requestTokenDialog = null; private ProgressDialog changeTokensDialog = null; - private Handler requestTokenHandler = new Handler() { + private final Handler requestTokenHandler = new Handler() { @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { if (requestTokenDialog != null && requestTokenDialog.isShowing()) { requestTokenDialog.dismiss(); } @@ -85,10 +91,10 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { }; - private Handler changeTokensHandler = new Handler() { + private final Handler changeTokensHandler = new Handler() { @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { if (changeTokensDialog != null && changeTokensDialog.isShowing()) { changeTokensDialog.dismiss(); } @@ -105,10 +111,10 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { }; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.authorization_activity); - Bundle extras = getIntent().getExtras(); + final Bundle extras = getIntent().getExtras(); if (extras != null) { host = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_HOST, host); pathRequest = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_PATH_REQUEST, pathRequest); @@ -125,7 +131,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { auth_1.setText(getAuthExplainShort()); auth_2.setText(getAuthExplainLong()); - ImmutablePair<String, String> tempToken = getTempTokens(); + final ImmutablePair<String, String> tempToken = getTempTokens(); OAtoken = tempToken.left; OAtokenSecret = tempToken.right; @@ -167,7 +173,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { final Parameters params = new Parameters(); params.put("oauth_callback", callback); final String method = "GET"; - OAuth.signOAuth(host, pathRequest, method, https, params, null, null, consumerKey, consumerSecret); + OAuth.signOAuth(host, pathRequest, method, https, params, new OAuthTokens(null, null), consumerKey, consumerSecret); final HttpResponse response = Network.getRequest(getUrlPrefix() + host + pathRequest, params); if (Network.isSuccess(response)) { @@ -176,11 +182,11 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { int status = STATUS_ERROR; if (StringUtils.isNotBlank(line)) { assert line != null; - final MatcherWrapper paramsMatcher1 = new MatcherWrapper(paramsPattern1, line); + final MatcherWrapper paramsMatcher1 = new MatcherWrapper(PARAMS_PATTERN_1, line); if (paramsMatcher1.find()) { OAtoken = paramsMatcher1.group(1); } - final MatcherWrapper paramsMatcher2 = new MatcherWrapper(paramsPattern2, line); + final MatcherWrapper paramsMatcher2 = new MatcherWrapper(PARAMS_PATTERN_2, line); if (paramsMatcher2.find()) { OAtokenSecret = paramsMatcher2.group(1); } @@ -193,9 +199,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { final String encodedParams = EntityUtils.toString(new UrlEncodedFormEntity(paramsBrowser)); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getUrlPrefix() + host + pathAuthorize + "?" + encodedParams))); status = STATUS_SUCCESS; - } catch (ParseException e) { - Log.e("OAuthAuthorizationActivity.requestToken", e); - } catch (IOException e) { + } catch (ParseException | IOException e) { Log.e("OAuthAuthorizationActivity.requestToken", e); } } @@ -221,17 +225,17 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { final Parameters params = new Parameters("oauth_verifier", verifier); final String method = "POST"; - OAuth.signOAuth(host, pathAccess, method, https, params, OAtoken, OAtokenSecret, consumerKey, consumerSecret); + OAuth.signOAuth(host, pathAccess, method, https, params, new OAuthTokens(OAtoken, OAtokenSecret), consumerKey, consumerSecret); final String line = StringUtils.defaultString(Network.getResponseData(Network.postRequest(getUrlPrefix() + host + pathAccess, params))); OAtoken = ""; OAtokenSecret = ""; - final MatcherWrapper paramsMatcher1 = new MatcherWrapper(paramsPattern1, line); + final MatcherWrapper paramsMatcher1 = new MatcherWrapper(PARAMS_PATTERN_1, line); if (paramsMatcher1.find()) { OAtoken = paramsMatcher1.group(1); } - final MatcherWrapper paramsMatcher2 = new MatcherWrapper(paramsPattern2, line); + final MatcherWrapper paramsMatcher2 = new MatcherWrapper(PARAMS_PATTERN_2, line); if (paramsMatcher2.find() && paramsMatcher2.groupCount() > 0) { OAtokenSecret = paramsMatcher2.group(1); } @@ -244,7 +248,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { setTokens(OAtoken, OAtokenSecret, true); status = AUTHENTICATED; } - } catch (Exception e) { + } catch (final Exception e) { Log.e("OAuthAuthorizationActivity.changeToken", e); } @@ -258,7 +262,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { private class StartListener implements View.OnClickListener { @Override - public void onClick(View arg0) { + public void onClick(final View arg0) { if (requestTokenDialog == null) { requestTokenDialog = new ProgressDialog(OAuthAuthorizationActivity.this); requestTokenDialog.setCancelable(false); @@ -270,13 +274,12 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { startButton.setOnClickListener(null); setTempTokens(null, null); - (new Thread() { - + RxUtils.networkScheduler.createWorker().schedule(new Action0() { @Override - public void run() { + public void call() { requestToken(); } - }).start(); + }); } } @@ -288,13 +291,12 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { } changeTokensDialog.show(); - (new Thread() { - + RxUtils.networkScheduler.createWorker().schedule(new Action0() { @Override - public void run() { + public void call() { changeToken(verifier); } - }).start(); + }); } protected abstract ImmutablePair<String, String> getTempTokens(); @@ -333,7 +335,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { * @return String with a more detailed error message (user-facing, localized), can be empty */ @SuppressWarnings("static-method") - protected String getExtendedErrorMsg(HttpResponse response) { + protected String getExtendedErrorMsg(final HttpResponse response) { return StringUtils.EMPTY; } @@ -363,14 +365,14 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { @NonNull public final String consumerSecret; @NonNull public final String callback; - public OAuthParameters(@NonNull String host, - @NonNull String pathRequest, - @NonNull String pathAuthorize, - @NonNull String pathAccess, - boolean https, - @NonNull String consumerKey, - @NonNull String consumerSecret, - @NonNull String callback) { + public OAuthParameters(@NonNull final String host, + @NonNull final String pathRequest, + @NonNull final String pathAuthorize, + @NonNull final String pathAccess, + final boolean https, + @NonNull final String consumerKey, + @NonNull final String consumerSecret, + @NonNull final String callback) { this.host = host; this.pathRequest = pathRequest; this.pathAuthorize = pathAuthorize; @@ -381,7 +383,7 @@ public abstract class OAuthAuthorizationActivity extends AbstractActivity { this.callback = callback; } - public void setOAuthExtras(Intent intent) { + public void setOAuthExtras(final Intent intent) { if (intent != null) { intent.putExtra(Intents.EXTRA_OAUTH_HOST, host); intent.putExtra(Intents.EXTRA_OAUTH_PATH_REQUEST, pathRequest); diff --git a/main/src/cgeo/geocaching/activity/Progress.java b/main/src/cgeo/geocaching/activity/Progress.java index 8ee88a7..e87eaa6 100644 --- a/main/src/cgeo/geocaching/activity/Progress.java +++ b/main/src/cgeo/geocaching/activity/Progress.java @@ -19,7 +19,7 @@ public class Progress { private int progressDivider = 1; final private boolean hideAbsolute; - public Progress(boolean hideAbsolute) { + public Progress(final boolean hideAbsolute) { this.hideAbsolute = hideAbsolute; } diff --git a/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java b/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java index 04f096d..86f5302 100644 --- a/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java +++ b/main/src/cgeo/geocaching/activity/ShowcaseViewBuilder.java @@ -29,7 +29,6 @@ public class ShowcaseViewBuilder extends Builder { /** * Use the hash of the title for the single shot remembering * - * @param resId */ private void setSingleshot(final CharSequence title) { super.singleShot(title.hashCode()); diff --git a/main/src/cgeo/geocaching/apps/AbstractApp.java b/main/src/cgeo/geocaching/apps/AbstractApp.java index 494e245..3bc7f71 100644 --- a/main/src/cgeo/geocaching/apps/AbstractApp.java +++ b/main/src/cgeo/geocaching/apps/AbstractApp.java @@ -5,28 +5,31 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.utils.ProcessUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import android.content.Intent; public abstract class AbstractApp implements App { - private final String packageName; - private final String intent; + @Nullable private final String packageName; + @Nullable private final String intent; + @NonNull private final String name; /** * a unique id, defined in res/values/ids.xml */ private final int id; - protected AbstractApp(final String name, final int id, final String intent, - final String packageName) { + protected AbstractApp(@NonNull final String name, final int id, @Nullable final String intent, + @Nullable final String packageName) { this.name = name; this.id = id; this.intent = intent; this.packageName = packageName; } - protected AbstractApp(final String name, final int id, final String intent) { + protected AbstractApp(@NonNull final String name, final int id, @Nullable final String intent) { this(name, id, intent, null); } @@ -35,9 +38,14 @@ public abstract class AbstractApp implements App { if (StringUtils.isNotEmpty(packageName) && ProcessUtils.isLaunchable(packageName)) { return true; } + if (intent == null) { + return false; + } + assert intent != null; // eclipse issue return ProcessUtils.isIntentAvailable(intent); } + @Nullable protected Intent getLaunchIntent() { return ProcessUtils.getLaunchIntent(packageName); } @@ -48,6 +56,7 @@ public abstract class AbstractApp implements App { } @Override + @NonNull public String getName() { return name; } @@ -57,12 +66,12 @@ public abstract class AbstractApp implements App { return id; } - protected static String getString(int ressourceId) { + protected static String getString(final int ressourceId) { return CgeoApplication.getInstance().getString(ressourceId); } @Override - public boolean isEnabled(Geocache cache) { + public boolean isEnabled(final Geocache cache) { return cache != null; } } diff --git a/main/src/cgeo/geocaching/apps/AbstractAppFactory.java b/main/src/cgeo/geocaching/apps/AbstractAppFactory.java deleted file mode 100644 index 945f7d6..0000000 --- a/main/src/cgeo/geocaching/apps/AbstractAppFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package cgeo.geocaching.apps; - -import android.view.MenuItem; - -public abstract class AbstractAppFactory { - - protected static App getAppFromMenuItem(MenuItem item, final App[] availableApps) { - final int id = item.getItemId(); - for (App app : availableApps) { - if (app.getId() == id) { - return app; - } - } - return null; - } -} diff --git a/main/src/cgeo/geocaching/apps/AbstractLocusApp.java b/main/src/cgeo/geocaching/apps/AbstractLocusApp.java index baf36a4..dfd148d 100644 --- a/main/src/cgeo/geocaching/apps/AbstractLocusApp.java +++ b/main/src/cgeo/geocaching/apps/AbstractLocusApp.java @@ -8,6 +8,7 @@ import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.utils.SynchronizedDateFormat; +import menion.android.locus.LocusDataStorageProvider; import menion.android.locus.addon.publiclib.DisplayData; import menion.android.locus.addon.publiclib.LocusUtils; import menion.android.locus.addon.publiclib.geoData.Point; @@ -15,6 +16,9 @@ import menion.android.locus.addon.publiclib.geoData.PointGeocachingData; import menion.android.locus.addon.publiclib.geoData.PointGeocachingDataWaypoint; import menion.android.locus.addon.publiclib.geoData.PointsData; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import android.app.Activity; import android.location.Location; @@ -31,7 +35,7 @@ import java.util.Locale; public abstract class AbstractLocusApp extends AbstractApp { private static final SynchronizedDateFormat ISO8601DATE = new SynchronizedDateFormat("yyyy-MM-dd'T'", Locale.US); - protected AbstractLocusApp(final String text, int id, final String intent) { + protected AbstractLocusApp(@NonNull final String text, final int id, @NonNull final String intent) { super(text, id, intent); } @@ -47,7 +51,6 @@ public abstract class AbstractLocusApp extends AbstractApp { * which caches/waypoints to show * @param withCacheWaypoints * Whether to give waypoints of caches to Locus or not - * @param activity */ protected static boolean showInLocus(final List<?> objectsToShow, final boolean withCacheWaypoints, final boolean export, final Activity activity) { @@ -57,7 +60,7 @@ public abstract class AbstractLocusApp extends AbstractApp { final boolean withCacheDetails = objectsToShow.size() < 200; final PointsData pd = new PointsData("c:geo"); - for (Object o : objectsToShow) { + for (final Object o : objectsToShow) { Point p = null; // get icon and Point if (o instanceof Geocache) { @@ -90,7 +93,6 @@ public abstract class AbstractLocusApp extends AbstractApp { /** * This method constructs a <code>Point</code> for displaying in Locus * - * @param cache * @param withWaypoints * whether to give waypoints to Locus or not * @param withCacheDetails @@ -98,7 +100,8 @@ public abstract class AbstractLocusApp extends AbstractApp { * should be false for all if more then 200 Caches are transferred * @return null, when the <code>Point</code> could not be constructed */ - private static Point getCachePoint(Geocache cache, boolean withWaypoints, boolean withCacheDetails) { + @Nullable + private static Point getCachePoint(final Geocache cache, final boolean withWaypoints, final boolean withCacheDetails) { if (cache == null || cache.getCoords() == null) { return null; } @@ -141,19 +144,24 @@ public abstract class AbstractLocusApp extends AbstractApp { if (withWaypoints && cache.hasWaypoints()) { pg.waypoints = new ArrayList<>(); - for (Waypoint waypoint : cache.getWaypoints()) { - if (waypoint == null || waypoint.getCoords() == null) { + for (final Waypoint waypoint : cache.getWaypoints()) { + if (waypoint == null) { continue; } - PointGeocachingDataWaypoint wp = new PointGeocachingDataWaypoint(); - wp.code = waypoint.getGeocode(); + + final PointGeocachingDataWaypoint wp = new PointGeocachingDataWaypoint(); + wp.code = waypoint.getLookup(); wp.name = waypoint.getName(); - String locusWpId = toLocusWaypoint(waypoint.getWaypointType()); + wp.description = waypoint.getNote(); + final String locusWpId = toLocusWaypoint(waypoint.getWaypointType()); if (locusWpId != null) { wp.type = locusWpId; } - wp.lat = waypoint.getCoords().getLatitude(); - wp.lon = waypoint.getCoords().getLongitude(); + + if (waypoint.getCoords() != null) { + wp.lat = waypoint.getCoords().getLatitude(); + wp.lon = waypoint.getCoords().getLongitude(); + } pg.waypoints.add(wp); } } @@ -174,10 +182,10 @@ public abstract class AbstractLocusApp extends AbstractApp { /** * This method constructs a <code>Point</code> for displaying in Locus * - * @param waypoint * @return null, when the <code>Point</code> could not be constructed */ - private static Point getWaypointPoint(Waypoint waypoint) { + @Nullable + private static Point getWaypointPoint(final Waypoint waypoint) { if (waypoint == null || waypoint.getCoords() == null) { return null; } @@ -248,6 +256,7 @@ public abstract class AbstractLocusApp extends AbstractApp { } } + @Nullable private static String toLocusWaypoint(final WaypointType wt) { switch (wt) { case FINAL: @@ -268,5 +277,4 @@ public abstract class AbstractLocusApp extends AbstractApp { return null; } } - } diff --git a/main/src/cgeo/geocaching/apps/App.java b/main/src/cgeo/geocaching/apps/App.java index 7e70581..1383809 100644 --- a/main/src/cgeo/geocaching/apps/App.java +++ b/main/src/cgeo/geocaching/apps/App.java @@ -2,6 +2,8 @@ package cgeo.geocaching.apps; import cgeo.geocaching.Geocache; +import org.eclipse.jdt.annotation.NonNull; + public interface App { public boolean isInstalled(); @@ -10,6 +12,7 @@ public interface App { */ public boolean isUsableAsDefaultNavigationApp(); + @NonNull public String getName(); /** @@ -20,8 +23,6 @@ public interface App { /** * Whether or not the app can be used with the given cache (may depend on properties of the cache). * - * @param cache - * @return */ boolean isEnabled(final Geocache cache); } diff --git a/main/src/cgeo/geocaching/apps/LocusDataStorageProvider.java b/main/src/cgeo/geocaching/apps/LocusDataStorageProvider.java deleted file mode 100644 index 03954f5..0000000 --- a/main/src/cgeo/geocaching/apps/LocusDataStorageProvider.java +++ /dev/null @@ -1,63 +0,0 @@ -package cgeo.geocaching.apps; - -import menion.android.locus.addon.publiclib.geoData.PointsData; -import menion.android.locus.addon.publiclib.utils.DataCursor; -import menion.android.locus.addon.publiclib.utils.DataStorage; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.Parcel; - -/** - * code provided by menion - developer of Locus - */ -public class LocusDataStorageProvider extends ContentProvider { - - @Override - public Cursor query(Uri aUri, String[] aProjection, String aSelection, - String[] aSelectionArgs, String aSortOrder) { - - final DataCursor cursor = new DataCursor(new String[] { "data" }); - - for (final PointsData item : DataStorage.getData()) { - final Parcel par = Parcel.obtain(); - item.writeToParcel(par, 0); - // add byte array to row - cursor.addRow(new Object[] { par.marshall() }); - par.recycle(); - } - // data filled to cursor, clear reference to prevent some memory issue - DataStorage.clearData(); - // now finally return filled cursor - return cursor; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - return 0; - } - - @Override - public String getType(Uri uri) { - return null; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - - @Override - public boolean onCreate() { - return false; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - return 0; - } - -} diff --git a/main/src/cgeo/geocaching/apps/cache/AbstractGeneralApp.java b/main/src/cgeo/geocaching/apps/cache/AbstractGeneralApp.java index c4f2723..4e542b8 100644 --- a/main/src/cgeo/geocaching/apps/cache/AbstractGeneralApp.java +++ b/main/src/cgeo/geocaching/apps/cache/AbstractGeneralApp.java @@ -4,17 +4,19 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.apps.AbstractApp; import cgeo.geocaching.apps.cache.navi.CacheNavigationApp; +import org.eclipse.jdt.annotation.NonNull; + import android.app.Activity; import android.content.Intent; abstract class AbstractGeneralApp extends AbstractApp implements CacheNavigationApp { - protected AbstractGeneralApp(final String name, final int id, final String packageName) { + protected AbstractGeneralApp(@NonNull final String name, final int id, @NonNull final String packageName) { super(name, id, null, packageName); } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { final Intent intent = getLaunchIntent(); if (intent != null) { intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); diff --git a/main/src/cgeo/geocaching/apps/cache/WhereYouGoApp.java b/main/src/cgeo/geocaching/apps/cache/WhereYouGoApp.java index b2a2cad..cdfbceb 100644 --- a/main/src/cgeo/geocaching/apps/cache/WhereYouGoApp.java +++ b/main/src/cgeo/geocaching/apps/cache/WhereYouGoApp.java @@ -21,16 +21,16 @@ public class WhereYouGoApp extends AbstractGeneralApp { } @Override - public boolean isEnabled(Geocache cache) { + public boolean isEnabled(final Geocache cache) { return cache.getType() == CacheType.WHERIGO && StringUtils.isNotEmpty(getWhereIGoUrl(cache)); } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getWhereIGoUrl(cache)))); } - protected static String getWhereIGoUrl(Geocache cache) { + protected static String getWhereIGoUrl(final Geocache cache) { return TextUtils.getMatch(cache.getDescription(), PATTERN_CARTRIDGE, null); } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java index ec9705c..0bf2c1c 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/AbstractPointNavigationApp.java @@ -5,7 +5,9 @@ import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.apps.AbstractApp; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; + +import org.eclipse.jdt.annotation.NonNull; import android.app.Activity; import android.content.Intent; @@ -15,20 +17,20 @@ import android.content.Intent; */ abstract class AbstractPointNavigationApp extends AbstractApp implements CacheNavigationApp, WaypointNavigationApp, GeopointNavigationApp { - protected AbstractPointNavigationApp(final String name, final int id, final String intent) { + protected AbstractPointNavigationApp(@NonNull final String name, final int id, final String intent) { super(name, id, intent); } - protected AbstractPointNavigationApp(final String name, final int id, final String intent, final String packageName) { + protected AbstractPointNavigationApp(@NonNull final String name, final int id, final String intent, final String packageName) { super(name, id, intent, packageName); } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { navigateWithNullCheck(activity, cache.getCoords()); } - private void navigateWithNullCheck(Activity activity, final Geopoint coords) { + private void navigateWithNullCheck(final Activity activity, final Geopoint coords) { if (coords != null) { navigate(activity, coords); } else { @@ -37,17 +39,17 @@ abstract class AbstractPointNavigationApp extends AbstractApp implements CacheNa } @Override - public void navigate(Activity activity, Waypoint waypoint) { + public void navigate(final Activity activity, final Waypoint waypoint) { navigateWithNullCheck(activity, waypoint.getCoords()); } @Override - public boolean isEnabled(Geocache cache) { + public boolean isEnabled(final Geocache cache) { return cache.getCoords() != null; } @Override - public boolean isEnabled(Waypoint waypoint) { + public boolean isEnabled(final Waypoint waypoint) { return waypoint.getCoords() != null; } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java index 6c6ffda..00b0954 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/AbstractRadarApp.java @@ -2,12 +2,15 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.Geocache; import cgeo.geocaching.Waypoint; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import android.app.Activity; import android.content.Intent; -public abstract class AbstractRadarApp extends AbstractPointNavigationApp { +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; diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AbstractStaticMapsApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AbstractStaticMapsApp.java index a2a5803..700c8aa 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/AbstractStaticMapsApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/AbstractStaticMapsApp.java @@ -11,11 +11,12 @@ import cgeo.geocaching.activity.ActivityMixin; import cgeo.geocaching.apps.AbstractApp; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import android.app.Activity; abstract class AbstractStaticMapsApp extends AbstractApp implements CacheNavigationApp, WaypointNavigationApp { - protected AbstractStaticMapsApp(final String name, final int id) { + protected AbstractStaticMapsApp(@NonNull final String name, final int id) { super(name, id, null); } @@ -29,11 +30,11 @@ abstract class AbstractStaticMapsApp extends AbstractApp implements CacheNavigat return false; } - protected static boolean hasStaticMap(Waypoint waypoint) { + protected static boolean hasStaticMap(final Waypoint waypoint) { if (waypoint==null) { return false; } - String geocode = waypoint.getGeocode(); + final String geocode = waypoint.getGeocode(); if (StringUtils.isNotEmpty(geocode) && DataStore.isOffline(geocode, null)) { return StaticMapsProvider.hasStaticMapForWaypoint(geocode, waypoint); } @@ -49,7 +50,7 @@ abstract class AbstractStaticMapsApp extends AbstractApp implements CacheNavigat } final String geocode = StringUtils.upperCase(logable.getGeocode()); - StaticMapsActivity_.IntentBuilder_ builder = StaticMapsActivity_.intent(activity).geocode(geocode).download(download); + final StaticMapsActivity_.IntentBuilder_ builder = StaticMapsActivity_.intent(activity).geocode(geocode).download(download); if (waypoint != null) { builder.waypointId(waypoint.getId()); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/AndroidWearApp.java b/main/src/cgeo/geocaching/apps/cache/navi/AndroidWearApp.java new file mode 100644 index 0000000..5abe2fc --- /dev/null +++ b/main/src/cgeo/geocaching/apps/cache/navi/AndroidWearApp.java @@ -0,0 +1,52 @@ +package cgeo.geocaching.apps.cache.navi; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.Intents; +import cgeo.geocaching.R; +import cgeo.geocaching.Waypoint; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.utils.ProcessUtils; + +import android.app.Activity; +import android.content.Intent; + +/** + * For use with any Android Wear geocaching apps which can handle the intent action below. + */ +class AndroidWearApp extends AbstractPointNavigationApp { + private static final String INTENT_ACTION = "cgeo.geocaching.wear.NAVIGATE_TO"; + + public AndroidWearApp() { + super(getString(R.string.cache_menu_android_wear), R.id.cache_app_android_wear, INTENT_ACTION, null); + } + + @Override + public boolean isInstalled() { + return ProcessUtils.isIntentAvailable(INTENT_ACTION); + } + + @Override + public void navigate(final Activity activity, final Geopoint coords) { + navigate(activity, null, null, coords); + } + + @Override + public void navigate(final Activity activity, final Geocache cache) { + navigate(activity, cache.getName(), cache.getGeocode(), cache.getCoords()); + } + + @Override + public void navigate(final Activity activity, final Waypoint waypoint) { + navigate(activity, waypoint.getName(), waypoint.getGeocode(), waypoint.getCoords()); + } + + private static void navigate(final Activity activity, final String destName, + final String destCode, final Geopoint coords) { + final Intent launchIntent = new Intent(INTENT_ACTION); + launchIntent.putExtra(Intents.EXTRA_NAME, destName) + .putExtra(Intents.EXTRA_GEOCODE, destCode) + .putExtra(Intents.EXTRA_LATITUDE, coords.getLatitude()) + .putExtra(Intents.EXTRA_LONGITUDE, coords.getLongitude()); + activity.startService(launchIntent); + } +} diff --git a/main/src/cgeo/geocaching/apps/cache/navi/CacheNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/CacheNavigationApp.java index d47bdc0..7387e94 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/CacheNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/CacheNavigationApp.java @@ -11,7 +11,4 @@ import android.app.Activity; */ public interface CacheNavigationApp extends App { void navigate(final Activity activity, final Geocache cache); - - @Override - boolean isEnabled(final Geocache cache); }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java b/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java index 03d2220..8e1e4cf 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/CompassApp.java @@ -4,7 +4,7 @@ import cgeo.geocaching.CompassActivity; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import android.app.Activity; @@ -20,19 +20,18 @@ 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.startActivityPoint(activity, coords, getString(R.string.navigation_direct_navigation)); } @Override - public void navigate(Activity activity, Waypoint waypoint) { - CompassActivity.startActivity(activity, waypoint.getPrefix() + "/" + waypoint.getLookup(), waypoint.getName(), waypoint.getCoords(), null, - waypoint.getWaypointType().getL10n()); + public void navigate(final Activity activity, final Waypoint waypoint) { + CompassActivity.startActivityWaypoint(activity, waypoint); } @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/GeopointNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/GeopointNavigationApp.java index fe4fd5d..5031dd8 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/GeopointNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/GeopointNavigationApp.java @@ -1,6 +1,6 @@ package cgeo.geocaching.apps.cache.navi; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import android.app.Activity; @@ -8,6 +8,6 @@ import android.app.Activity; * interface for navigation to a coordinate. This one cannot be enabled/disabled. * */ -public interface GeopointNavigationApp { +interface GeopointNavigationApp { void navigate(final Activity activity, final Geopoint coords); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java index 6c184ce..c12b4ca 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsApp.java @@ -4,9 +4,9 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; -import cgeo.geocaching.geopoint.GeopointFormatter.Format; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; +import cgeo.geocaching.location.GeopointFormatter.Format; import cgeo.geocaching.utils.Log; import android.app.Activity; @@ -25,20 +25,20 @@ class GoogleMapsApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint point) { + public void navigate(final Activity activity, final Geopoint point) { navigate(activity, point, activity.getString(R.string.waypoint)); } - private static void navigate(Activity activity, Geopoint point, String label) { + private static void navigate(final Activity activity, final Geopoint point, final String label) { try { - String latitude = GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, point); - String longitude = GeopointFormatter.format(Format.LON_DECDEGREE_RAW, point); + final String latitude = GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, point); + final String longitude = GeopointFormatter.format(Format.LON_DECDEGREE_RAW, point); final String geoLocation = "geo:" + latitude + "," + longitude; final String query = latitude + "," + longitude + "(" + label + ")"; final String uriString = geoLocation + "?q=" + Uri.encode(query); activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(uriString))); return; - } catch (RuntimeException e) { + } catch (final RuntimeException ignored) { // nothing } Log.i("GoogleMapsApp.navigate: No maps application available."); @@ -47,12 +47,12 @@ class GoogleMapsApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { navigate(activity, cache.getCoords(), cache.getName()); } @Override - public void navigate(Activity activity, Waypoint waypoint) { + public void navigate(final Activity activity, final Waypoint waypoint) { navigate(activity, waypoint.getCoords(), waypoint.getName()); } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java index 4924786..d14fca4 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/GoogleMapsDirectionApp.java @@ -1,17 +1,17 @@ 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.location.Geopoint; import cgeo.geocaching.maps.MapProviderFactory; +import cgeo.geocaching.sensors.GeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.utils.Log; import android.app.Activity; import android.content.Intent; import android.net.Uri; -public class GoogleMapsDirectionApp extends AbstractPointNavigationApp { +class GoogleMapsDirectionApp extends AbstractPointNavigationApp { protected GoogleMapsDirectionApp() { super(getString(R.string.cache_menu_maps_directions), R.id.cache_app_google_maps_direction, null); @@ -23,23 +23,15 @@ 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) { - activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri - .parse("http://maps.google.com/maps?f=d&saddr=" - + coordsNow.getLatitude() + "," + coordsNow.getLongitude() + "&daddr=" - + coords.getLatitude() + "," + coords.getLongitude()))); - } else { - activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri - .parse("http://maps.google.com/maps?f=d&daddr=" - + coords.getLatitude() + "," + coords.getLongitude()))); - } - - } catch (Exception e) { + final GeoData geo = Sensors.getInstance().currentGeo(); + activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri + .parse("http://maps.google.com/maps?f=d&saddr=" + + geo.getCoords().getLatitude() + "," + geo.getCoords().getLongitude() + "&daddr=" + + coords.getLatitude() + "," + coords.getLongitude()))); + + } catch (final Exception e) { Log.i("GoogleMapsDirectionApp: application not available.", e); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/GoogleNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/GoogleNavigationApp.java index 902eebf..0e6e97a 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/GoogleNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/GoogleNavigationApp.java @@ -1,7 +1,7 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.utils.Log; import android.app.Activity; @@ -12,7 +12,7 @@ abstract class GoogleNavigationApp extends AbstractPointNavigationApp { private final String mode; - protected GoogleNavigationApp(final int nameResourceId, final int id, final String mode) { + private GoogleNavigationApp(final int nameResourceId, final int id, final String mode) { super(getString(nameResourceId), id, null); this.mode = mode; } @@ -23,7 +23,7 @@ abstract class GoogleNavigationApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint coords) { + public void navigate(final Activity activity, final Geopoint coords) { try { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri .parse("google.navigation:ll=" + coords.getLatitude() + "," diff --git a/main/src/cgeo/geocaching/apps/cache/navi/InternalMap.java b/main/src/cgeo/geocaching/apps/cache/navi/InternalMap.java index 540b025..03c6dc1 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/InternalMap.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/InternalMap.java @@ -4,7 +4,7 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; import cgeo.geocaching.enumerations.WaypointType; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.CGeoMap; import android.app.Activity; @@ -21,17 +21,17 @@ class InternalMap extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint coords) { + public void navigate(final Activity activity, final Geopoint coords) { CGeoMap.startActivityCoords(activity, coords, WaypointType.WAYPOINT, null); } @Override - public void navigate(Activity activity, Waypoint waypoint) { + public void navigate(final Activity activity, final Waypoint waypoint) { CGeoMap.startActivityCoords(activity, waypoint.getCoords(), waypoint.getWaypointType(), waypoint.getName()); } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { CGeoMap.startActivityGeoCode(activity, cache.getGeocode()); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/LocusApp.java b/main/src/cgeo/geocaching/apps/cache/navi/LocusApp.java index b60d78a..61445c3 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/LocusApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/LocusApp.java @@ -20,12 +20,12 @@ class LocusApp extends AbstractLocusApp implements CacheNavigationApp, WaypointN } @Override - public boolean isEnabled(Waypoint waypoint) { + public boolean isEnabled(final Waypoint waypoint) { return waypoint.getCoords() != null; } @Override - public boolean isEnabled(Geocache cache) { + public boolean isEnabled(final Geocache cache) { return cache.getCoords() != null; } @@ -35,12 +35,12 @@ class LocusApp extends AbstractLocusApp implements CacheNavigationApp, WaypointN * */ @Override - public void navigate(Activity activity, Waypoint waypoint) { + public void navigate(final Activity activity, final Waypoint waypoint) { showInLocus(Collections.singletonList(waypoint), true, false, activity); } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { showInLocus(Collections.singletonList(cache), true, false, activity); } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/MapsWithMeApp.java b/main/src/cgeo/geocaching/apps/cache/navi/MapsWithMeApp.java index ea5aebb..9403dcd 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/MapsWithMeApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/MapsWithMeApp.java @@ -3,34 +3,34 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import com.mapswithme.maps.api.MapsWithMeApi; import android.app.Activity; -public class MapsWithMeApp extends AbstractPointNavigationApp { +class MapsWithMeApp extends AbstractPointNavigationApp { protected MapsWithMeApp() { super(getString(R.string.cache_menu_mapswithme), R.id.cache_app_mapswithme, null); } @Override - public void navigate(Activity activity, Geopoint coords) { + public void navigate(final Activity activity, final Geopoint coords) { navigate(activity, coords, getString(R.string.unknown)); } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { navigate(activity, cache.getCoords(), cache.getName()); } - private static void navigate(Activity activity, Geopoint coords, String label) { + private static void navigate(final Activity activity, final Geopoint coords, final String label) { MapsWithMeApi.showPointOnMap(activity, coords.getLatitude(), coords.getLongitude(), label); } @Override - public void navigate(Activity activity, Waypoint waypoint) { + public void navigate(final Activity activity, final Waypoint waypoint) { navigate(activity, waypoint.getCoords(), waypoint.getName()); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java b/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java index c00723d..32eba7e 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/NavigationAppFactory.java @@ -5,13 +5,12 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.apps.AbstractAppFactory; import cgeo.geocaching.apps.App; import cgeo.geocaching.apps.cache.WhereYouGoApp; import cgeo.geocaching.apps.cache.navi.GoogleNavigationApp.GoogleNavigationBikeApp; import cgeo.geocaching.apps.cache.navi.GoogleNavigationApp.GoogleNavigationDrivingApp; import cgeo.geocaching.apps.cache.navi.GoogleNavigationApp.GoogleNavigationWalkingApp; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.settings.Settings; import org.eclipse.jdt.annotation.Nullable; @@ -25,7 +24,11 @@ import android.widget.ArrayAdapter; import java.util.ArrayList; import java.util.List; -public final class NavigationAppFactory extends AbstractAppFactory { +public final class NavigationAppFactory { + + private NavigationAppFactory() { + // utility class + } public enum NavigationAppsEnum { /** The internal compass activity */ @@ -69,12 +72,16 @@ public final class NavigationAppFactory extends AbstractAppFactory { WHERE_YOU_GO(new WhereYouGoApp(), 16, R.string.pref_navigation_menu_where_you_go), PEBBLE(new PebbleApp(), 17, R.string.pref_navigation_menu_pebble), + ANDROID_WEAR(new AndroidWearApp(), 18, R.string.pref_navigation_menu_android_wear), MAPSWITHME(new MapsWithMeApp(), 22, R.string.pref_navigation_menu_mapswithme); NavigationAppsEnum(final App app, final int id, final int preferenceKey) { this.app = app; this.id = id; this.preferenceKey = preferenceKey; + if (preferenceKey == 0 || preferenceKey == -1) { + throw new IllegalStateException("Every navigation app must have a boolean preference in the settings to be enabled/disabled."); + } } /** @@ -109,10 +116,6 @@ public final class NavigationAppFactory extends AbstractAppFactory { * Delegates to {@link #showNavigationMenu(Activity, cgeo.geocaching.Geocache, cgeo.geocaching.Waypoint, Geopoint, boolean, boolean)} with * <code>showInternalMap = true</code> and <code>showDefaultNavigation = false</code> * - * @param activity - * @param cache - * @param waypoint - * @param destination */ public static void showNavigationMenu(final Activity activity, final Geocache cache, final Waypoint waypoint, final Geopoint destination) { @@ -123,7 +126,6 @@ public final class NavigationAppFactory extends AbstractAppFactory { * Specialized way to handle selection of navigation tool.<br /> * A dialog is created for tool selection and the selected tool is started afterwards. * - * @param activity * @param cache * may be <code>null</code> * @param waypoint @@ -188,9 +190,8 @@ public final class NavigationAppFactory extends AbstractAppFactory { /** * Returns all installed navigation apps. * - * @return */ - public static List<NavigationAppsEnum> getInstalledNavigationApps() { + static List<NavigationAppsEnum> getInstalledNavigationApps() { final List<NavigationAppsEnum> installedNavigationApps = new ArrayList<>(); for (final NavigationAppsEnum appEnum : NavigationAppsEnum.values()) { if (appEnum.app.isInstalled()) { @@ -203,7 +204,7 @@ public final class NavigationAppFactory extends AbstractAppFactory { /** * @return all navigation apps, which are installed and activated in the settings */ - public static List<NavigationAppsEnum> getActiveNavigationApps() { + static List<NavigationAppsEnum> getActiveNavigationApps() { final List<NavigationAppsEnum> activeApps = new ArrayList<>(); for (final NavigationAppsEnum appEnum : getInstalledNavigationApps()) { if (Settings.isUseNavigationApp(appEnum)) { @@ -216,7 +217,6 @@ public final class NavigationAppFactory extends AbstractAppFactory { /** * Returns all installed navigation apps for default navigation. * - * @return */ public static List<NavigationAppsEnum> getInstalledDefaultNavigationApps() { final List<NavigationAppsEnum> installedNavigationApps = new ArrayList<>(); @@ -232,10 +232,6 @@ public final class NavigationAppFactory extends AbstractAppFactory { * Handles menu selections for menu entries created with * {@link #showNavigationMenu(Activity, Geocache, Waypoint, Geopoint)}. * - * @param item - * @param activity - * @param cache - * @return */ public static boolean onMenuItemSelected(final MenuItem item, final Activity activity, final Geocache cache) { final App menuItem = getAppFromMenuItem(item); @@ -278,10 +274,7 @@ public final class NavigationAppFactory extends AbstractAppFactory { /** * Starts the default navigation tool if correctly set and installed or the compass app as default fallback. * - * @param defaultNavigation * - * @param activity - * @param cache */ public static void startDefaultNavigationApplication(final int defaultNavigation, final Activity activity, final Geocache cache) { if (cache == null || cache.getCoords() == null) { @@ -302,8 +295,6 @@ public final class NavigationAppFactory extends AbstractAppFactory { /** * Starts the default navigation tool if correctly set and installed or the compass app as default fallback. * - * @param activity - * @param waypoint */ public static void startDefaultNavigationApplication(final int defaultNavigation, final Activity activity, final Waypoint waypoint) { if (waypoint == null || waypoint.getCoords() == null) { @@ -316,8 +307,6 @@ public final class NavigationAppFactory extends AbstractAppFactory { /** * Starts the default navigation tool if correctly set and installed or the compass app as default fallback. * - * @param activity - * @param destination */ public static void startDefaultNavigationApplication(final int defaultNavigation, final Activity activity, final Geopoint destination) { if (destination == null) { diff --git a/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java b/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java index 82883a2..43eaee3 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/NavigationSelectionActionProvider.java @@ -3,6 +3,7 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.Geocache; import cgeo.geocaching.apps.App; import cgeo.geocaching.apps.cache.navi.NavigationAppFactory.NavigationAppsEnum; +import cgeo.geocaching.ui.AbstractMenuActionProvider; import android.app.Activity; import android.content.Context; @@ -12,11 +13,13 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.SubMenu; -import android.view.View; import java.util.List; -public class NavigationSelectionActionProvider extends ActionProvider { +/** + * Action provider listing all available navigation actions as sub menu. + */ +public class NavigationSelectionActionProvider extends AbstractMenuActionProvider { private Geocache geocache; private final Activity activity; @@ -26,17 +29,6 @@ public class NavigationSelectionActionProvider extends ActionProvider { activity = (Activity) context; } - @Override - public boolean hasSubMenu() { - return true; - } - - @Override - public View onCreateActionView() { - // must return null, otherwise the menu will not work - return null; - } - public void setTarget(final Geocache cache) { geocache = cache; } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/NavigonApp.java b/main/src/cgeo/geocaching/apps/cache/navi/NavigonApp.java index 024bf37..9954382 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/NavigonApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/NavigonApp.java @@ -1,7 +1,7 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import android.app.Activity; import android.content.Intent; @@ -17,7 +17,7 @@ class NavigonApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint point) { + public void navigate(final Activity activity, final Geopoint point) { final Intent intent = new Intent(INTENT); /* diff --git a/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java b/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java index 5d645f7..b3e21a3 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/OruxMapsApp.java @@ -1,13 +1,15 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import android.app.Activity; 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() { @@ -15,10 +17,10 @@ class OruxMapsApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint point) { + public void navigate(final Activity activity, final 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 ac83085..f384192 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/PebbleApp.java @@ -1,26 +1,26 @@ -package cgeo.geocaching.apps.cache.navi;
-
-import cgeo.geocaching.R;
-import cgeo.geocaching.geopoint.Geopoint;
-
-import android.content.Intent;
-
-/**
- * Application for communication with the Pebble watch.
- *
- */
-class PebbleApp extends AbstractRadarApp {
-
- private static final String INTENT = "com.webmajstr.pebble_gc.NAVIGATE_TO";
- private static final String PACKAGE_NAME = "com.webmajstr.pebble_gc";
-
- PebbleApp() {
- super(getString(R.string.cache_menu_pebble), R.id.cache_app_pebble, INTENT, PACKAGE_NAME);
- }
-
- @Override
- protected void addCoordinates(final Intent intent, final Geopoint coords) {
- intent.putExtra("latitude", coords.getLatitude());
- intent.putExtra("longitude", coords.getLongitude());
- }
+package cgeo.geocaching.apps.cache.navi; + +import cgeo.geocaching.R; +import cgeo.geocaching.location.Geopoint; + +import android.content.Intent; + +/** + * Application for communication with the Pebble watch. + * + */ +class PebbleApp extends AbstractRadarApp { + + private static final String INTENT = "com.webmajstr.pebble_gc.NAVIGATE_TO"; + private static final String PACKAGE_NAME = "com.webmajstr.pebble_gc"; + + PebbleApp() { + super(getString(R.string.cache_menu_pebble), R.id.cache_app_pebble, INTENT, PACKAGE_NAME); + } + + @Override + protected void addCoordinates(final Intent intent, final Geopoint coords) { + 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/RMapsApp.java b/main/src/cgeo/geocaching/apps/cache/navi/RMapsApp.java index 4dbb46c..1eefdec 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/RMapsApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/RMapsApp.java @@ -3,8 +3,8 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter.Format; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter.Format; import android.app.Activity; import android.content.Intent; @@ -20,11 +20,11 @@ class RMapsApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Waypoint waypoint) { + public void navigate(final Activity activity, final Waypoint waypoint) { navigate(activity, waypoint.getCoords(), waypoint.getLookup(), waypoint.getName()); } - private static void navigate(Activity activity, Geopoint coords, String code, String name) { + private static void navigate(final Activity activity, final Geopoint coords, final String code, final String name) { final ArrayList<String> locations = new ArrayList<>(); locations.add(coords.format(Format.LAT_LON_DECDEGREE_COMMA) + ";" + code + ";" + name); final Intent intent = new Intent(INTENT); @@ -33,12 +33,12 @@ class RMapsApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geocache cache) { + public void navigate(final Activity activity, final Geocache cache) { navigate(activity, cache.getCoords(), cache.getGeocode(), cache.getName()); } @Override - public void navigate(Activity activity, Geopoint coords) { + public void navigate(final Activity activity, final Geopoint coords) { navigate(activity, coords, "", ""); } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java b/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java index 41cf2d8..a26edc4 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/RadarApp.java @@ -1,7 +1,7 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import android.content.Intent; @@ -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/apps/cache/navi/StreetviewApp.java b/main/src/cgeo/geocaching/apps/cache/navi/StreetviewApp.java index 7294a40..ca3aefe 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/StreetviewApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/StreetviewApp.java @@ -3,7 +3,7 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.utils.ProcessUtils; import android.app.Activity; @@ -26,11 +26,11 @@ class StreetviewApp extends AbstractPointNavigationApp { } @Override - public void navigate(Activity activity, Geopoint point) { + public void navigate(final Activity activity, final Geopoint point) { try { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("google.streetview:cbll=" + point.getLatitude() + "," + point.getLongitude()))); - } catch (final ActivityNotFoundException e) { + } catch (final ActivityNotFoundException ignored) { ActivityMixin.showToast(activity, CgeoApplication.getInstance().getString(R.string.err_application_no)); } } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/SygicNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/SygicNavigationApp.java index 76b7f0e..391fa90 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/SygicNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/SygicNavigationApp.java @@ -1,7 +1,8 @@ package cgeo.geocaching.apps.cache.navi; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.utils.ProcessUtils; import android.app.Activity; import android.content.Intent; @@ -12,17 +13,26 @@ import android.net.Uri; * handler * */ -public class SygicNavigationApp extends AbstractPointNavigationApp { +class SygicNavigationApp extends AbstractPointNavigationApp { - private static final String PACKAGE = "com.sygic.aura"; + private static final String PACKAGE_NORMAL = "com.sygic.aura"; + /** + * there is a secondary edition of this app + */ + private static final String PACKAGE_VOUCHER = "com.sygic.aura_voucher"; SygicNavigationApp() { - super(getString(R.string.cache_menu_sygic), R.id.cache_app_sygic, null, PACKAGE); + super(getString(R.string.cache_menu_sygic), R.id.cache_app_sygic, null, PACKAGE_NORMAL); } @Override - public void navigate(Activity activity, Geopoint coords) { - String str = "http://com.sygic.aura/coordinate|" + coords.getLongitude() + "|" + coords.getLatitude() + "|show"; + public boolean isInstalled() { + return ProcessUtils.isLaunchable(PACKAGE_NORMAL) || ProcessUtils.isLaunchable(PACKAGE_VOUCHER); + } + + @Override + public void navigate(final Activity activity, final Geopoint coords) { + final String str = "http://com.sygic.aura/coordinate|" + coords.getLongitude() + "|" + coords.getLatitude() + "|show"; activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(str))); } diff --git a/main/src/cgeo/geocaching/apps/cache/navi/WaypointNavigationApp.java b/main/src/cgeo/geocaching/apps/cache/navi/WaypointNavigationApp.java index c26ec3e..ba85a1d 100644 --- a/main/src/cgeo/geocaching/apps/cache/navi/WaypointNavigationApp.java +++ b/main/src/cgeo/geocaching/apps/cache/navi/WaypointNavigationApp.java @@ -8,7 +8,7 @@ import android.app.Activity; * interface for navigation to a waypoint * */ -public interface WaypointNavigationApp { +interface WaypointNavigationApp { void navigate(final Activity activity, final Waypoint waypoint); boolean isEnabled(final Waypoint waypoint); diff --git a/main/src/cgeo/geocaching/apps/cachelist/AbstractLocusCacheListApp.java b/main/src/cgeo/geocaching/apps/cachelist/AbstractLocusCacheListApp.java index 6411758..d047d1a 100644 --- a/main/src/cgeo/geocaching/apps/cachelist/AbstractLocusCacheListApp.java +++ b/main/src/cgeo/geocaching/apps/cachelist/AbstractLocusCacheListApp.java @@ -14,9 +14,9 @@ import java.util.List; abstract class AbstractLocusCacheListApp extends AbstractLocusApp implements CacheListApp { - private boolean export; + private final boolean export; - public AbstractLocusCacheListApp(final int id, boolean export) { + public AbstractLocusCacheListApp(final int id, final boolean export) { super(getString(export ? R.string.caches_map_locus_export : R.string.caches_map_locus), id, Intent.ACTION_VIEW); this.export = export; } @@ -27,7 +27,7 @@ abstract class AbstractLocusCacheListApp extends AbstractLocusApp implements Cac * @see AbstractLocusApp#showInLocus */ @Override - public boolean invoke(List<Geocache> cacheList, Activity activity, final SearchResult search) { + public boolean invoke(final List<Geocache> cacheList, final Activity activity, final SearchResult search) { if (CollectionUtils.isEmpty(cacheList)) { return false; } diff --git a/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java b/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java index 40c4d92..8beb5e4 100644 --- a/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java +++ b/main/src/cgeo/geocaching/apps/cachelist/CacheListApp.java @@ -8,7 +8,7 @@ import android.app.Activity; import java.util.List; -interface CacheListApp extends App { +public interface CacheListApp extends App { boolean invoke(final List<Geocache> caches, final Activity activity, final SearchResult search); diff --git a/main/src/cgeo/geocaching/apps/cachelist/CacheListAppFactory.java b/main/src/cgeo/geocaching/apps/cachelist/CacheListAppFactory.java deleted file mode 100644 index 5886168..0000000 --- a/main/src/cgeo/geocaching/apps/cachelist/CacheListAppFactory.java +++ /dev/null @@ -1,84 +0,0 @@ -package cgeo.geocaching.apps.cachelist; - -import cgeo.geocaching.Geocache; -import cgeo.geocaching.R; -import cgeo.geocaching.SearchResult; -import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.apps.AbstractAppFactory; -import cgeo.geocaching.utils.Log; - -import android.app.Activity; -import android.content.res.Resources; -import android.view.Menu; -import android.view.MenuItem; -import android.view.SubMenu; - -import java.util.ArrayList; -import java.util.List; - -public final class CacheListAppFactory extends AbstractAppFactory { - private static class LazyHolder { - public static final CacheListApp[] apps = { - new InternalCacheListMap(), - new LocusShowCacheListApp(), - new LocusExportCacheListApp(), - new MapsWithMeCacheListApp() - }; - } - - /** - * @param menu - * @param activity - * @param res - */ - public static void addMenuItems(final Menu menu, final Activity activity, final Resources res) { - final List<CacheListApp> activeApps = getActiveApps(); - if (activeApps.isEmpty()) { - return; - } - if (activeApps.size() == 1) { - final MenuItem subItem = menu.findItem(R.id.menu_cache_list_app); - subItem.setVisible(true); - subItem.setTitle(activeApps.get(0).getName()); - } else { - final MenuItem subItem = menu.findItem(R.id.submenu_cache_list_app); - subItem.setVisible(true); - final SubMenu subMenu = subItem.getSubMenu(); - for (final CacheListApp app : activeApps) { - subMenu.add(0, app.getId(), 0, app.getName()); - } - } - } - - private static List<CacheListApp> getActiveApps() { - final List<CacheListApp> activeApps = new ArrayList<>(LazyHolder.apps.length); - for (final CacheListApp app : LazyHolder.apps) { - if (app.isInstalled()) { - activeApps.add(app); - } - } - return activeApps; - } - - public static boolean onMenuItemSelected(final MenuItem item, final List<Geocache> caches, final Activity activity, - final SearchResult search) { - CacheListApp app; - if (item.getItemId() == R.id.menu_cache_list_app) { - app = getActiveApps().get(0); - } - else { - app = (CacheListApp) getAppFromMenuItem(item, LazyHolder.apps); - } - if (app != null) { - try { - boolean result = app.invoke(caches, activity, search); - ActivityMixin.invalidateOptionsMenu(activity); - return result; - } catch (Exception e) { - Log.e("CacheListAppFactory.onMenuItemSelected", e); - } - } - return false; - } - -} diff --git a/main/src/cgeo/geocaching/apps/cachelist/CacheListApps.java b/main/src/cgeo/geocaching/apps/cachelist/CacheListApps.java new file mode 100644 index 0000000..e8e81a8 --- /dev/null +++ b/main/src/cgeo/geocaching/apps/cachelist/CacheListApps.java @@ -0,0 +1,29 @@ +package cgeo.geocaching.apps.cachelist; + +import java.util.ArrayList; +import java.util.List; + +public enum CacheListApps { + INTERNAL(new InternalCacheListMap()), + LOCUS_SHOW(new LocusShowCacheListApp()), + LOCUS_EXPORT(new LocusExportCacheListApp()), + MAPS_ME(new MapsWithMeCacheListApp()); + + private final CacheListApp app; + + private CacheListApps(final CacheListApp app) { + this.app = app; + } + + public static List<CacheListApp> getActiveApps() { + final List<CacheListApp> activeApps = new ArrayList<>(); + for (final CacheListApps appEnum : values()) { + if (appEnum.app.isInstalled()) { + activeApps.add(appEnum.app); + } + } + return activeApps; + } + +} + diff --git a/main/src/cgeo/geocaching/apps/cachelist/InternalCacheListMap.java b/main/src/cgeo/geocaching/apps/cachelist/InternalCacheListMap.java index 9216bc0..d364cda 100644 --- a/main/src/cgeo/geocaching/apps/cachelist/InternalCacheListMap.java +++ b/main/src/cgeo/geocaching/apps/cachelist/InternalCacheListMap.java @@ -22,7 +22,7 @@ class InternalCacheListMap extends AbstractApp implements CacheListApp { } @Override - public boolean invoke(List<Geocache> caches, Activity activity, final SearchResult search) { + public boolean invoke(final List<Geocache> caches, final Activity activity, final SearchResult search) { CGeoMap.startActivitySearch(activity, search, null); return true; } diff --git a/main/src/cgeo/geocaching/apps/cachelist/ListNavigationSelectionActionProvider.java b/main/src/cgeo/geocaching/apps/cachelist/ListNavigationSelectionActionProvider.java new file mode 100644 index 0000000..6a49995 --- /dev/null +++ b/main/src/cgeo/geocaching/apps/cachelist/ListNavigationSelectionActionProvider.java @@ -0,0 +1,60 @@ +package cgeo.geocaching.apps.cachelist; + +import cgeo.geocaching.ui.AbstractMenuActionProvider; + +import android.content.Context; +import android.support.v4.view.ActionProvider; +import android.support.v4.view.MenuItemCompat; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.SubMenu; + +import java.util.List; + +public class ListNavigationSelectionActionProvider extends AbstractMenuActionProvider { + + public static interface Callback { + void onListNavigationSelected(final CacheListApp app); + } + + private Callback callback; + + public ListNavigationSelectionActionProvider(final Context context) { + super(context); + } + + public void setCallback(final Callback callback) { + this.callback = callback; + } + + @Override + public void onPrepareSubMenu(final SubMenu subMenu) { + subMenu.clear(); + if (callback == null) { + return; + } + final List<CacheListApp> activeApps = CacheListApps.getActiveApps(); + for (int i = 0; i < activeApps.size(); i++) { + final CacheListApp app = activeApps.get(i); + subMenu.add(Menu.NONE, i, Menu.NONE, app.getName()).setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(final MenuItem item) { + final CacheListApp app = activeApps.get(item.getItemId()); + callback.onListNavigationSelected(app); + return true; + } + }); + } + } + + public static void initialize(final MenuItem menuItem, final Callback callback) { + final ActionProvider actionProvider = MenuItemCompat.getActionProvider(menuItem); + if (actionProvider instanceof ListNavigationSelectionActionProvider) { + final ListNavigationSelectionActionProvider navigateAction = (ListNavigationSelectionActionProvider) actionProvider; + navigateAction.setCallback(callback); + } + } + +} diff --git a/main/src/cgeo/geocaching/apps/cachelist/MapsWithMeCacheListApp.java b/main/src/cgeo/geocaching/apps/cachelist/MapsWithMeCacheListApp.java index ba177f0..ed64d2d 100644 --- a/main/src/cgeo/geocaching/apps/cachelist/MapsWithMeCacheListApp.java +++ b/main/src/cgeo/geocaching/apps/cachelist/MapsWithMeCacheListApp.java @@ -26,10 +26,10 @@ public class MapsWithMeCacheListApp extends AbstractApp implements CacheListApp } @Override - public boolean invoke(List<Geocache> caches, Activity activity, SearchResult search) { + public boolean invoke(final List<Geocache> caches, final Activity activity, final SearchResult search) { final MWMPoint[] points = new MWMPoint[caches.size()]; for (int i = 0; i < points.length; i++) { - Geocache geocache = caches.get(i); + final Geocache geocache = caches.get(i); points[i] = new MWMPoint(geocache.getCoords().getLatitude(), geocache.getCoords().getLongitude(), geocache.getName(), geocache.getGeocode()); } MapsWithMeApi.showPointsOnMap(activity, null, getPendingIntent(activity), points); @@ -44,22 +44,19 @@ public class MapsWithMeCacheListApp extends AbstractApp implements CacheListApp /** * get cache code from a PendingIntent after an invocation of MapsWithMe - * - * @return + * */ @Nullable public static String getCacheFromMapsWithMe(final Context context, final Intent intent) { final MWMResponse mwmResponse = MWMResponse.extractFromIntent(context, intent); - if (mwmResponse != null) { - final MWMPoint point = mwmResponse.getPoint(); - if (point != null) { - return point.getId(); - } + final MWMPoint point = mwmResponse.getPoint(); + if (point != null) { + return point.getId(); } return null; } - private static PendingIntent getPendingIntent(Context context) { + private static PendingIntent getPendingIntent(final Context context) { final Intent intent = new Intent(context, CacheDetailActivity.class); return PendingIntent.getActivity(context, 0, intent, 0); } diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel11.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel11.java new file mode 100644 index 0000000..8398eb3 --- /dev/null +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel11.java @@ -0,0 +1,15 @@ +package cgeo.geocaching.compatibility; + +import android.annotation.TargetApi; +import android.os.Build; +import android.widget.TextView; + +@TargetApi(Build.VERSION_CODES.HONEYCOMB) +public class AndroidLevel11 implements AndroidLevel11Interface { + + @Override + public void setTextIsSelectable(final TextView textView, final boolean selectable) { + textView.setTextIsSelectable(selectable); + } + +} diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel11Emulation.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel11Emulation.java new file mode 100644 index 0000000..b4111ab --- /dev/null +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel11Emulation.java @@ -0,0 +1,12 @@ +package cgeo.geocaching.compatibility; + +import android.widget.TextView; + +public class AndroidLevel11Emulation implements AndroidLevel11Interface { + + @Override + public void setTextIsSelectable(final TextView textView, final boolean selectable) { + // do nothing + } + +} diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel11Interface.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel11Interface.java new file mode 100644 index 0000000..45c06a4 --- /dev/null +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel11Interface.java @@ -0,0 +1,9 @@ +package cgeo.geocaching.compatibility; + +import android.widget.TextView; + +public interface AndroidLevel11Interface { + + void setTextIsSelectable(TextView textView, boolean selectable); + +} diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel13.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel13.java index eb2be4b..f599611 100644 --- a/main/src/cgeo/geocaching/compatibility/AndroidLevel13.java +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel13.java @@ -8,16 +8,11 @@ import android.graphics.Point; import android.view.WindowManager; @TargetApi(value = 13) -public class AndroidLevel13 implements AndroidLevel13Interface { - - @Override - public int getDisplayWidth() { - return getDisplaySize().x; - } +class AndroidLevel13 implements AndroidLevel13Interface { @Override public Point getDisplaySize() { - Point dimensions = new Point(); + final Point dimensions = new Point(); ((WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay().getSize(dimensions); return dimensions; diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel13Emulation.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel13Emulation.java index 56c784f..f9fd3bc 100644 --- a/main/src/cgeo/geocaching/compatibility/AndroidLevel13Emulation.java +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel13Emulation.java @@ -8,12 +8,7 @@ import android.view.Display; import android.view.WindowManager; @SuppressWarnings("deprecation") -public class AndroidLevel13Emulation implements AndroidLevel13Interface { - - @Override - public int getDisplayWidth() { - return getDisplay().getWidth(); - } +class AndroidLevel13Emulation implements AndroidLevel13Interface { @Override public Point getDisplaySize() { diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel13Interface.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel13Interface.java index 8483e38..b78875f 100644 --- a/main/src/cgeo/geocaching/compatibility/AndroidLevel13Interface.java +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel13Interface.java @@ -2,8 +2,7 @@ package cgeo.geocaching.compatibility; import android.graphics.Point; -public interface AndroidLevel13Interface { - int getDisplayWidth(); +interface AndroidLevel13Interface { Point getDisplaySize(); } diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel19.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel19.java index ed4849f..6b15a00 100644 --- a/main/src/cgeo/geocaching/compatibility/AndroidLevel19.java +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel19.java @@ -5,12 +5,12 @@ import android.app.Activity; import android.content.Intent; @TargetApi(19) -public class AndroidLevel19 implements AndroidLevel19Interface { +class AndroidLevel19 implements AndroidLevel19Interface { @Override public void importGpxFromStorageAccessFramework(final Activity activity, final int requestCode) { // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file browser. - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a file (as opposed to a list // of contacts or timezones) diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel19Emulation.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel19Emulation.java index 99f140f..7404c82 100644 --- a/main/src/cgeo/geocaching/compatibility/AndroidLevel19Emulation.java +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel19Emulation.java @@ -2,10 +2,10 @@ package cgeo.geocaching.compatibility; import android.app.Activity; -public class AndroidLevel19Emulation implements AndroidLevel19Interface { +class AndroidLevel19Emulation implements AndroidLevel19Interface { @Override - public void importGpxFromStorageAccessFramework(Activity activity, int requestCode) { + public void importGpxFromStorageAccessFramework(final Activity activity, final int requestCode) { // do nothing } diff --git a/main/src/cgeo/geocaching/compatibility/AndroidLevel19Interface.java b/main/src/cgeo/geocaching/compatibility/AndroidLevel19Interface.java index 9a27cd8..de397a6 100644 --- a/main/src/cgeo/geocaching/compatibility/AndroidLevel19Interface.java +++ b/main/src/cgeo/geocaching/compatibility/AndroidLevel19Interface.java @@ -2,6 +2,6 @@ package cgeo.geocaching.compatibility; import android.app.Activity; -public interface AndroidLevel19Interface { +interface AndroidLevel19Interface { void importGpxFromStorageAccessFramework(Activity activity, final int requestCode); } diff --git a/main/src/cgeo/geocaching/compatibility/Compatibility.java b/main/src/cgeo/geocaching/compatibility/Compatibility.java index 54e2966..56e18bf 100644 --- a/main/src/cgeo/geocaching/compatibility/Compatibility.java +++ b/main/src/cgeo/geocaching/compatibility/Compatibility.java @@ -5,15 +5,18 @@ import org.eclipse.jdt.annotation.NonNull; import android.app.Activity; import android.graphics.Point; import android.os.Build; +import android.widget.TextView; public final class Compatibility { private static final int SDK_VERSION = Build.VERSION.SDK_INT; + private static final AndroidLevel11Interface LEVEL_11; private static final AndroidLevel13Interface LEVEL_13; private static final AndroidLevel19Interface LEVEL_19; static { + LEVEL_11 = SDK_VERSION >= 11 ? new AndroidLevel11() : new AndroidLevel11Emulation(); LEVEL_13 = SDK_VERSION >= 13 ? new AndroidLevel13() : new AndroidLevel13Emulation(); LEVEL_19 = SDK_VERSION >= 19 ? new AndroidLevel19() : new AndroidLevel19Emulation(); } @@ -22,19 +25,19 @@ public final class Compatibility { // utility class } - public static int getDisplayWidth() { - return LEVEL_13.getDisplayWidth(); - } - public static Point getDisplaySize() { return LEVEL_13.getDisplaySize(); } - public static void importGpxFromStorageAccessFramework(final @NonNull Activity activity, int requestCodeImportGpx) { + public static void importGpxFromStorageAccessFramework(final @NonNull Activity activity, final int requestCodeImportGpx) { LEVEL_19.importGpxFromStorageAccessFramework(activity, requestCodeImportGpx); } public static boolean isStorageAccessFrameworkAvailable() { return SDK_VERSION >= 19; } + + public static void setTextIsSelectable(final TextView textView, final boolean selectable) { + LEVEL_11.setTextIsSelectable(textView, selectable); + } } diff --git a/main/src/cgeo/geocaching/concurrent/BlockingThreadPool.java b/main/src/cgeo/geocaching/concurrent/BlockingThreadPool.java deleted file mode 100644 index a6d7e9b..0000000 --- a/main/src/cgeo/geocaching/concurrent/BlockingThreadPool.java +++ /dev/null @@ -1,57 +0,0 @@ -package cgeo.geocaching.concurrent; - - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * BlockingThreadPool restricts the amount of parallel threads executing Runnables. - */ -public class BlockingThreadPool { - /** The queue holding the Runnable. **/ - private BlockingQueue<Runnable> queue = null; - /** The Executor. **/ - private ThreadPoolExecutor executor; - - /** - * Creates a ThreadPool with a given maximum of parallel threads running. - * Idle threads will be stopped until new threads are added. - * - * @param poolSize - * Maximum amout of parallel Threads - * @param priority - * The Thread priority e.g. Thread.MIN_PRIORITY - */ - public BlockingThreadPool(int poolSize, int priority) { - ThreadFactory threadFactory = new PriorityThreadFactory(priority); - this.queue = new ArrayBlockingQueue<>(poolSize, true); - this.executor = new ThreadPoolExecutor(0, poolSize, 5, TimeUnit.SECONDS, this.queue); - this.executor.setThreadFactory(threadFactory); - } - - /** - * Add a runnable to the pool. This will start the core threads in the underlying - * executor and try to add the Runnable to the pool. This method waits until timeout - * if no free thread is available. - * - * @param task - * The Runnable to add to the pool - * @param timeout - * The timeout to wait for a free thread - * @param unit - * The timeout unit - * @return true/false successful added - * @throws InterruptedException - * Operation was interrupted - */ - public boolean add(Runnable task, int timeout, TimeUnit unit) throws InterruptedException { - this.executor.setCorePoolSize(this.executor.getMaximumPoolSize()); - this.executor.prestartAllCoreThreads(); - boolean successfull = this.queue.offer(task, timeout, unit); - this.executor.setCorePoolSize(0); - return successfull; - } -} diff --git a/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java b/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java deleted file mode 100644 index 0da198b..0000000 --- a/main/src/cgeo/geocaching/concurrent/PriorityThreadFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -package cgeo.geocaching.concurrent; - -import org.eclipse.jdt.annotation.NonNull; - -import java.util.concurrent.ThreadFactory; - -/** - * Helper class for setting Thread priority in ThreadPool. - */ -public class PriorityThreadFactory implements ThreadFactory { - private int priority; - - public PriorityThreadFactory(int priority) { - this.priority = priority; - } - - @NonNull - @Override - public Thread newThread(Runnable r) { - Thread result = new Thread(r); - result.setPriority(this.priority); - return result; - } - -} diff --git a/main/src/cgeo/geocaching/connector/AbstractConnector.java b/main/src/cgeo/geocaching/connector/AbstractConnector.java index 9729e06..0583aa1 100644 --- a/main/src/cgeo/geocaching/connector/AbstractConnector.java +++ b/main/src/cgeo/geocaching/connector/AbstractConnector.java @@ -15,10 +15,12 @@ import cgeo.geocaching.connector.capability.ISearchByOwner; import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LogType; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import rx.functions.Action1; import java.util.ArrayList; @@ -38,12 +40,12 @@ public abstract class AbstractConnector implements IConnector { } @Override - public boolean addToWatchlist(Geocache cache) { + public boolean addToWatchlist(@NonNull final Geocache cache) { return false; } @Override - public boolean removeFromWatchlist(Geocache cache) { + public boolean removeFromWatchlist(@NonNull final Geocache cache) { return false; } @@ -53,7 +55,7 @@ public abstract class AbstractConnector implements IConnector { } @Override - public boolean uploadPersonalNote(Geocache cache) { + public boolean uploadPersonalNote(@NonNull final Geocache cache) { throw new UnsupportedOperationException(); } @@ -63,7 +65,7 @@ public abstract class AbstractConnector implements IConnector { } @Override - public boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt) { + public boolean uploadModifiedCoordinates(@NonNull final Geocache cache, @NonNull final Geopoint wpt) { throw new UnsupportedOperationException(); } @@ -71,12 +73,12 @@ public abstract class AbstractConnector implements IConnector { * {@link IConnector} */ @Override - public boolean deleteModifiedCoordinates(Geocache cache) { + public boolean deleteModifiedCoordinates(@NonNull final Geocache cache) { throw new UnsupportedOperationException(); } @Override - public boolean supportsFavoritePoints(final Geocache cache) { + public boolean supportsFavoritePoints(@NonNull final Geocache cache) { return false; } @@ -91,46 +93,48 @@ public abstract class AbstractConnector implements IConnector { } @Override - public boolean canLog(Geocache cache) { + public boolean canLog(@NonNull final Geocache cache) { return false; } @Override - public ILoggingManager getLoggingManager(final LogCacheActivity activity, final Geocache cache) { + @NonNull + public ILoggingManager getLoggingManager(@NonNull final LogCacheActivity activity, @NonNull final Geocache cache) { return new NoLoggingManager(); } @Override + @NonNull public String getLicenseText(final @NonNull Geocache cache) { - return null; + return StringUtils.EMPTY; } - protected static boolean isNumericId(final String string) { + protected static boolean isNumericId(final String str) { try { - return Integer.parseInt(string) > 0; - } catch (NumberFormatException e) { + return Integer.parseInt(str) > 0; + } catch (final NumberFormatException ignored) { } return false; } @Override - public boolean isZippedGPXFile(String fileName) { + public boolean isZippedGPXFile(@NonNull final String fileName) { // don't accept any file by default return false; } @Override - public boolean isReliableLatLon(boolean cacheHasReliableLatLon) { + public boolean isReliableLatLon(final boolean cacheHasReliableLatLon) { // let every cache have reliable coordinates by default return true; } @Override - public String getGeocodeFromUrl(final String url) { + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { final String urlPrefix = getCacheUrlPrefix(); - if (StringUtils.startsWith(url, urlPrefix)) { - @NonNull - String geocode = url.substring(urlPrefix.length()); + if (StringUtils.isEmpty(urlPrefix) || StringUtils.startsWith(url, urlPrefix)) { + @NonNull final String geocode = url.substring(urlPrefix.length()); if (canHandle(geocode)) { return geocode; } @@ -138,9 +142,11 @@ public abstract class AbstractConnector implements IConnector { return null; } + @NonNull abstract protected String getCacheUrlPrefix(); @Override + @Nullable public String getLongCacheUrl(final @NonNull Geocache cache) { return getCacheUrl(cache); } @@ -151,7 +157,7 @@ public abstract class AbstractConnector implements IConnector { } @Override - public int getCacheMapMarkerId(boolean disabled) { + public int getCacheMapMarkerId(final boolean disabled) { if (disabled) { return R.drawable.marker_disabled_other; } @@ -159,7 +165,8 @@ public abstract class AbstractConnector implements IConnector { } @Override - public List<LogType> getPossibleLogTypes(Geocache geocache) { + @NonNull + public List<LogType> getPossibleLogTypes(@NonNull final Geocache geocache) { final List<LogType> logTypes = new ArrayList<>(); if (geocache.isEventCache()) { logTypes.add(LogType.WILL_ATTEND); @@ -196,13 +203,14 @@ public abstract class AbstractConnector implements IConnector { } @Override - public String getWaypointGpxId(String prefix, String geocode) { + public String getWaypointGpxId(final String prefix, @NonNull final String geocode) { // Default: just return the prefix return prefix; } @Override - public String getWaypointPrefix(String name) { + @NonNull + public String getWaypointPrefix(final String name) { // Default: just return the name return name; } @@ -213,8 +221,9 @@ public abstract class AbstractConnector implements IConnector { } @Override + @NonNull public final Collection<String> getCapabilities() { - ArrayList<String> list = new ArrayList<>(); + final ArrayList<String> list = new ArrayList<>(); addCapability(list, ISearchByViewPort.class, R.string.feature_search_live_map); addCapability(list, ISearchByKeyword.class, R.string.feature_search_keyword); addCapability(list, ISearchByCenter.class, R.string.feature_search_center); @@ -245,20 +254,20 @@ public abstract class AbstractConnector implements IConnector { } } - private static String feature(int featureResourceId) { + private static String feature(final int featureResourceId) { return CgeoApplication.getInstance().getString(featureResourceId); } @Override public @NonNull List<UserAction> getUserActions() { - List<UserAction> actions = getDefaultUserActions(); + final List<UserAction> actions = getDefaultUserActions(); if (this instanceof ISearchByOwner) { actions.add(new UserAction(R.string.user_menu_view_hidden, new Action1<Context>() { @Override - public void call(Context context) { + public void call(final Context context) { CacheListActivity.startActivityOwner(context.activity, context.userName); } })); @@ -268,7 +277,7 @@ public abstract class AbstractConnector implements IConnector { actions.add(new UserAction(R.string.user_menu_view_found, new Action1<UserAction.Context>() { @Override - public void call(Context context) { + public void call(final Context context) { CacheListActivity.startActivityFinder(context.activity, context.userName); } })); @@ -286,7 +295,7 @@ public abstract class AbstractConnector implements IConnector { actions.add(new UserAction(R.string.user_menu_open_contact, new Action1<UserAction.Context>() { @Override - public void call(Context context) { + public void call(final Context context) { ContactsAddon.openContactCard(context.activity, context.userName); } })); @@ -295,4 +304,6 @@ public abstract class AbstractConnector implements IConnector { return actions; } + public void logout() { + } } diff --git a/main/src/cgeo/geocaching/connector/AbstractLoggingManager.java b/main/src/cgeo/geocaching/connector/AbstractLoggingManager.java index 9e702c4..e53fcf1 100644 --- a/main/src/cgeo/geocaching/connector/AbstractLoggingManager.java +++ b/main/src/cgeo/geocaching/connector/AbstractLoggingManager.java @@ -2,6 +2,8 @@ package cgeo.geocaching.connector; import cgeo.geocaching.TrackableLog; +import org.eclipse.jdt.annotation.NonNull; + import java.util.Collections; import java.util.List; @@ -13,6 +15,7 @@ public abstract class AbstractLoggingManager implements ILoggingManager { } @Override + @NonNull public List<TrackableLog> getTrackables() { return Collections.emptyList(); } diff --git a/main/src/cgeo/geocaching/connector/AbstractLogin.java b/main/src/cgeo/geocaching/connector/AbstractLogin.java index 6527685..252daeb 100644 --- a/main/src/cgeo/geocaching/connector/AbstractLogin.java +++ b/main/src/cgeo/geocaching/connector/AbstractLogin.java @@ -4,9 +4,11 @@ import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.network.Cookies; +import cgeo.geocaching.network.Network; import cgeo.geocaching.settings.Settings; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; public abstract class AbstractLogin { @@ -38,7 +40,7 @@ public abstract class AbstractLogin { return actualLoginStatus; } - protected void setActualLoginStatus(boolean loginStatus) { + protected void setActualLoginStatus(final boolean loginStatus) { actualLoginStatus = loginStatus; } @@ -46,7 +48,7 @@ public abstract class AbstractLogin { return actualUserName; } - protected void setActualUserName(String userName) { + protected void setActualUserName(final String userName) { actualUserName = userName; } @@ -68,10 +70,15 @@ public abstract class AbstractLogin { setActualStatus(CgeoApplication.getInstance().getString(R.string.err_login)); } + @NonNull public StatusCode login() { + if (!Network.isNetworkConnected()) { + return StatusCode.COMMUNICATION_ERROR; + } return login(true); } + @NonNull protected abstract StatusCode login(boolean retry); } diff --git a/main/src/cgeo/geocaching/connector/ConnectorFactory.java b/main/src/cgeo/geocaching/connector/ConnectorFactory.java index e6ef829..918911a 100644 --- a/main/src/cgeo/geocaching/connector/ConnectorFactory.java +++ b/main/src/cgeo/geocaching/connector/ConnectorFactory.java @@ -1,6 +1,6 @@ package cgeo.geocaching.connector; -import cgeo.geocaching.ICache; +import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; import cgeo.geocaching.Trackable; @@ -15,13 +15,15 @@ import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.MapTokens; import cgeo.geocaching.connector.oc.OCApiConnector.ApiSupport; import cgeo.geocaching.connector.oc.OCApiLiveConnector; +import cgeo.geocaching.connector.oc.OCCZConnector; import cgeo.geocaching.connector.oc.OCConnector; import cgeo.geocaching.connector.ox.OXConnector; import cgeo.geocaching.connector.trackable.GeokretyConnector; +import cgeo.geocaching.connector.trackable.SwaggieConnector; import cgeo.geocaching.connector.trackable.TrackableConnector; import cgeo.geocaching.connector.trackable.TravelBugConnector; import cgeo.geocaching.connector.trackable.UnknownTrackableConnector; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Viewport; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; @@ -36,14 +38,14 @@ import java.util.Collections; import java.util.List; public final class ConnectorFactory { - private static final @NonNull UnknownConnector UNKNOWN_CONNECTOR = new UnknownConnector(); - private static final Collection<IConnector> CONNECTORS = Collections.unmodifiableCollection(Arrays.asList(new IConnector[] { + @NonNull private static final UnknownConnector UNKNOWN_CONNECTOR = new UnknownConnector(); + @NonNull private static final Collection<IConnector> CONNECTORS = Collections.unmodifiableCollection(Arrays.asList(new IConnector[] { GCConnector.getInstance(), ECConnector.getInstance(), new OCApiLiveConnector("opencaching.de", "www.opencaching.de", "OC", "CC BY-NC-ND, alle Logeinträge © jeweiliger Autor", R.string.oc_de_okapi_consumer_key, R.string.oc_de_okapi_consumer_secret, R.string.pref_connectorOCActive, R.string.pref_ocde_tokenpublic, R.string.pref_ocde_tokensecret, ApiSupport.current), - new OCConnector("OpenCaching.CZ", "www.opencaching.cz", "OZ"), + new OCCZConnector(), new OCApiLiveConnector("opencaching.org.uk", "www.opencaching.org.uk", "OK", "CC BY-NC-SA 2.5", R.string.oc_uk_okapi_consumer_key, R.string.oc_uk_okapi_consumer_secret, R.string.pref_connectorOCUKActive, R.string.pref_ocuk_tokenpublic, R.string.pref_ocuk_tokensecret, ApiSupport.oldapi), @@ -71,22 +73,31 @@ public final class ConnectorFactory { })); @NonNull public static final UnknownTrackableConnector UNKNOWN_TRACKABLE_CONNECTOR = new UnknownTrackableConnector(); + + @NonNull private static final Collection<TrackableConnector> TRACKABLE_CONNECTORS = Collections.unmodifiableCollection(Arrays.asList(new TrackableConnector[] { - new GeokretyConnector(), // GK must be first, as it overlaps with the secret codes of travel bugs - TravelBugConnector.getInstance(), + new GeokretyConnector(), + new SwaggieConnector(), + TravelBugConnector.getInstance(), // travel bugs last, as their secret codes overlap with other connectors UNKNOWN_TRACKABLE_CONNECTOR // must be last })); + @NonNull private static final Collection<ISearchByViewPort> searchByViewPortConns = getMatchingConnectors(ISearchByViewPort.class); + @NonNull private static final Collection<ISearchByCenter> searchByCenterConns = getMatchingConnectors(ISearchByCenter.class); + @NonNull private static final Collection<ISearchByKeyword> searchByKeywordConns = getMatchingConnectors(ISearchByKeyword.class); + @NonNull private static final Collection<ISearchByOwner> SEARCH_BY_OWNER_CONNECTORS = getMatchingConnectors(ISearchByOwner.class); + @NonNull private static final Collection<ISearchByFinder> SEARCH_BY_FINDER_CONNECTORS = getMatchingConnectors(ISearchByFinder.class); + @NonNull @SuppressWarnings("unchecked") private static <T extends IConnector> Collection<T> getMatchingConnectors(final Class<T> clazz) { final List<T> matching = new ArrayList<>(); @@ -98,26 +109,32 @@ public final class ConnectorFactory { return Collections.unmodifiableCollection(matching); } + @NonNull public static Collection<IConnector> getConnectors() { return CONNECTORS; } + @NonNull public static Collection<ISearchByCenter> getSearchByCenterConnectors() { return searchByCenterConns; } + @NonNull public static Collection<ISearchByKeyword> getSearchByKeywordConnectors() { return searchByKeywordConns; } + @NonNull public static Collection<ISearchByOwner> getSearchByOwnerConnectors() { return SEARCH_BY_OWNER_CONNECTORS; } + @NonNull public static Collection<ISearchByFinder> getSearchByFinderConnectors() { return SEARCH_BY_FINDER_CONNECTORS; } + @NonNull public static ILogin[] getActiveLiveConnectors() { final List<ILogin> liveConns = new ArrayList<>(); for (final IConnector conn : CONNECTORS) { @@ -143,11 +160,12 @@ public final class ConnectorFactory { return false; } - public static @NonNull - IConnector getConnector(final ICache cache) { + @NonNull + public static IConnector getConnector(final Geocache cache) { return getConnector(cache.getGeocode()); } + @NonNull public static TrackableConnector getConnector(final Trackable trackable) { return getTrackableConnector(trackable.getGeocode()); } @@ -162,8 +180,8 @@ public final class ConnectorFactory { return UNKNOWN_TRACKABLE_CONNECTOR; // avoid null checks by returning a non implementing connector } - public static @NonNull - IConnector getConnector(final String geocodeInput) { + @NonNull + public static IConnector getConnector(final String geocodeInput) { // this may come from user input final String geocode = StringUtils.trim(geocodeInput); if (geocode == null) { @@ -186,7 +204,8 @@ public final class ConnectorFactory { } /** @see ISearchByViewPort#searchByViewport */ - public static SearchResult searchByViewport(final @NonNull Viewport viewport, final MapTokens tokens) { + @NonNull + public static SearchResult searchByViewport(final @NonNull Viewport viewport, @NonNull final MapTokens tokens) { return SearchResult.parallelCombineActive(searchByViewPortConns, new Func1<ISearchByViewPort, SearchResult>() { @Override public SearchResult call(final ISearchByViewPort connector) { @@ -195,9 +214,13 @@ public final class ConnectorFactory { }); } - public static String getGeocodeFromURL(final String url) { + @Nullable + public static String getGeocodeFromURL(@Nullable final String url) { + if (url == null) { + return null; + } for (final IConnector connector : CONNECTORS) { - final String geocode = connector.getGeocodeFromUrl(url); + @Nullable final String geocode = connector.getGeocodeFromUrl(url); if (StringUtils.isNotBlank(geocode)) { return geocode; } @@ -205,6 +228,7 @@ public final class ConnectorFactory { return null; } + @NonNull public static Collection<TrackableConnector> getTrackableConnectors() { return TRACKABLE_CONNECTORS; } @@ -212,9 +236,9 @@ public final class ConnectorFactory { /** * Get the geocode of a trackable from a URL. * - * @param url * @return {@code null} if the URL cannot be decoded */ + @Nullable public static String getTrackableFromURL(final String url) { if (url == null) { return null; diff --git a/main/src/cgeo/geocaching/connector/GeocachingAustraliaConnector.java b/main/src/cgeo/geocaching/connector/GeocachingAustraliaConnector.java index 3992013..9377d6d 100644 --- a/main/src/cgeo/geocaching/connector/GeocachingAustraliaConnector.java +++ b/main/src/cgeo/geocaching/connector/GeocachingAustraliaConnector.java @@ -1,30 +1,32 @@ package cgeo.geocaching.connector; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; -public class GeocachingAustraliaConnector extends AbstractConnector { +class GeocachingAustraliaConnector extends AbstractConnector { @Override + @NonNull public String getName() { return "Geocaching Australia"; } @Override + @NonNull public String getCacheUrl(final @NonNull Geocache cache) { return getCacheUrlPrefix() + cache.getGeocode(); } @Override + @NonNull public String getHost() { return "geocaching.com.au"; } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return false; } @@ -34,6 +36,7 @@ public class GeocachingAustraliaConnector extends AbstractConnector { } @Override + @NonNull protected String getCacheUrlPrefix() { return "http://" + getHost() + "/cache/"; } diff --git a/main/src/cgeo/geocaching/connector/GeopeitusConnector.java b/main/src/cgeo/geocaching/connector/GeopeitusConnector.java index aa08485..4340cac 100644 --- a/main/src/cgeo/geocaching/connector/GeopeitusConnector.java +++ b/main/src/cgeo/geocaching/connector/GeopeitusConnector.java @@ -1,39 +1,42 @@ package cgeo.geocaching.connector; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; -public class GeopeitusConnector extends AbstractConnector { +class GeopeitusConnector extends AbstractConnector { @Override + @NonNull public String getName() { return "geopeitus.ee"; } @Override + @NonNull public String getCacheUrl(final @NonNull Geocache cache) { return getCacheUrlPrefix() + StringUtils.stripStart(cache.getGeocode().substring(2), "0"); } @Override + @NonNull public String getHost() { return "www.geopeitus.ee"; } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return false; } @Override - public boolean canHandle(@NonNull String geocode) { + public boolean canHandle(@NonNull final String geocode) { return StringUtils.startsWith(geocode, "GE") && isNumericId(geocode.substring(2)); } @Override + @NonNull protected String getCacheUrlPrefix() { return "http://" + getHost() + "/aare/"; } diff --git a/main/src/cgeo/geocaching/connector/IConnector.java b/main/src/cgeo/geocaching/connector/IConnector.java index e6b6674..74b1028 100644 --- a/main/src/cgeo/geocaching/connector/IConnector.java +++ b/main/src/cgeo/geocaching/connector/IConnector.java @@ -1,12 +1,12 @@ package cgeo.geocaching.connector; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.enumerations.LogType; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import java.util.Collection; import java.util.List; @@ -15,8 +15,8 @@ public interface IConnector { /** * get name for display (currently only used in links) * - * @return */ + @NonNull public String getName(); /** @@ -31,109 +31,96 @@ public interface IConnector { /** * Get the browser URL for the given cache. * - * @param cache - * @return */ + @Nullable public String getCacheUrl(final @NonNull Geocache cache); /** * get long browser URL for the given cache * - * @param cache - * @return */ + @Nullable public String getLongCacheUrl(final @NonNull Geocache cache); /** * enable/disable watchlist controls in cache details * - * @return */ public boolean supportsWatchList(); /** * Add the cache to the watchlist * - * @param cache * @return True - success/False - failure */ - public boolean addToWatchlist(Geocache cache); + public boolean addToWatchlist(@NonNull Geocache cache); /** * Remove the cache from the watchlist * - * @param cache * @return True - success/False - failure */ - public boolean removeFromWatchlist(Geocache cache); + public boolean removeFromWatchlist(@NonNull Geocache cache); /** * enable/disable favorite points controls in cache details * - * @return */ - public boolean supportsFavoritePoints(final Geocache cache); + public boolean supportsFavoritePoints(@NonNull final Geocache cache); /** * enable/disable logging controls in cache details * - * @return */ public boolean supportsLogging(); /** * enable/disable attaching image to log * - * @return */ public boolean supportsLogImages(); /** * Get an ILoggingManager to guide the logging process. * - * @return */ - public ILoggingManager getLoggingManager(final LogCacheActivity activity, final Geocache cache); + @NonNull + public ILoggingManager getLoggingManager(@NonNull final LogCacheActivity activity, @NonNull final Geocache cache); /** * Get host name of the connector server for dynamic loading of data. * - * @return */ + @NonNull public String getHost(); /** * Get cache data license text. This is displayed somewhere near the cache details. * - * @param cache - * @return */ + @NonNull public String getLicenseText(final @NonNull Geocache cache); /** * return true if this is a ZIP file containing a GPX file * - * @param fileName - * @return */ - public boolean isZippedGPXFile(final String fileName); + public boolean isZippedGPXFile(@NonNull final String fileName); /** * return true if coordinates of a cache are reliable. only implemented by GC connector * * @param cacheHasReliableLatLon * flag of the cache - * @return */ public boolean isReliableLatLon(boolean cacheHasReliableLatLon); /** * extract a geocode from the given URL, if this connector can handle that URL somehow * - * @param url - * @return */ - public String getGeocodeFromUrl(final String url); + @Nullable + public String getGeocodeFromUrl(@NonNull final String url); /** * enable/disable uploading personal note @@ -145,10 +132,9 @@ public interface IConnector { /** * Uploading personal note to website * - * @param cache * @return success */ - public boolean uploadPersonalNote(Geocache cache); + public boolean uploadPersonalNote(@NonNull Geocache cache); /** * enable/disable uploading modified coordinates to website @@ -160,25 +146,21 @@ public interface IConnector { /** * Resetting of modified coordinates on website to details * - * @param cache * @return success */ - public boolean deleteModifiedCoordinates(Geocache cache); + public boolean deleteModifiedCoordinates(@NonNull Geocache cache); /** * Uploading modified coordinates to website * - * @param cache - * @param wpt * @return success */ - public boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt); + public boolean uploadModifiedCoordinates(@NonNull Geocache cache, @NonNull Geopoint wpt); /** * Return {@code true} if this connector is active for online interaction (download details, do searches, ...). If * this is {@code false}, the connector will still be used for already stored offline caches. * - * @return */ public boolean isActive(); @@ -189,16 +171,14 @@ public interface IConnector { * @param cache a cache that this connector must be able to handle * @return <code>true</code> if the current user is the cache owner, <code>false</code> otherwise */ - public boolean isOwner(final ICache cache); + public boolean isOwner(@NonNull final Geocache cache); /** * Check if the cache information is complete enough to be * able to log online. * - * @param geocache - * @return */ - public boolean canLog(Geocache geocache); + public boolean canLog(@NonNull Geocache geocache); /** * Return the marker id of the caches for this connector. This creates the different backgrounds for cache markers @@ -213,42 +193,37 @@ public interface IConnector { * Get the list of <b>potentially</b> possible log types for a cache. Those may still be filtered further during the * actual logging activity. * - * @param geocache - * @return */ - public List<LogType> getPossibleLogTypes(Geocache geocache); + @NonNull + public List<LogType> getPossibleLogTypes(@NonNull Geocache geocache); /** * Get the GPX id for a waypoint when exporting. For some connectors there is an inherent name logic, * for others its just the 'prefix'. * - * @param prefix - * @return */ - public String getWaypointGpxId(String prefix, String geocode); + public String getWaypointGpxId(String prefix, @NonNull String geocode); /** * Get the 'prefix' (key) for a waypoint from the 'name' in the GPX file * - * @param name - * @return */ + @NonNull public String getWaypointPrefix(String name); /** * Get the maximum value for Terrain * - * @return */ public int getMaxTerrain(); /** * Get a user readable collection of all online features of this connector. * - * @return */ + @NonNull public Collection<String> getCapabilities(); - public @NonNull - List<UserAction> getUserActions(); + @NonNull + public List<UserAction> getUserActions(); } diff --git a/main/src/cgeo/geocaching/connector/ILoggingManager.java b/main/src/cgeo/geocaching/connector/ILoggingManager.java index c5586b3..2b0a067 100644 --- a/main/src/cgeo/geocaching/connector/ILoggingManager.java +++ b/main/src/cgeo/geocaching/connector/ILoggingManager.java @@ -1,9 +1,11 @@ package cgeo.geocaching.connector; -import cgeo.geocaching.Geocache; import cgeo.geocaching.TrackableLog; import cgeo.geocaching.enumerations.LogType; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import android.net.Uri; import java.util.Calendar; @@ -13,23 +15,18 @@ public interface ILoggingManager { /** * Post a log for a cache online - * - * @param cache - * @param logType - * @param date - * @param log + * * @param logPassword * optional, maybe null - * @param trackableLogs - * @return */ - LogResult postLog(Geocache cache, - LogType logType, - Calendar date, - String log, - String logPassword, - List<TrackableLog> trackableLogs); - + @NonNull + LogResult postLog(@NonNull LogType logType, + @NonNull Calendar date, + @NonNull String log, + @Nullable String logPassword, + @NonNull List<TrackableLog> trackableLogs); + + @NonNull ImageResult postLogImage(String logId, String imageCaption, String imageDescription, @@ -37,8 +34,10 @@ public interface ILoggingManager { public boolean hasLoaderError(); + @NonNull public List<TrackableLog> getTrackables(); + @NonNull public List<LogType> getPossibleLogTypes(); public void init(); diff --git a/main/src/cgeo/geocaching/connector/ImageResult.java b/main/src/cgeo/geocaching/connector/ImageResult.java index 9314cad..ec11a6d 100644 --- a/main/src/cgeo/geocaching/connector/ImageResult.java +++ b/main/src/cgeo/geocaching/connector/ImageResult.java @@ -2,20 +2,26 @@ package cgeo.geocaching.connector; import cgeo.geocaching.enumerations.StatusCode; +import org.eclipse.jdt.annotation.NonNull; + public class ImageResult { + @NonNull private final StatusCode postResult; + @NonNull private final String imageUri; - public ImageResult(StatusCode postResult, String imageUri) { + public ImageResult(@NonNull final StatusCode postResult, @NonNull final String imageUri) { this.postResult = postResult; this.imageUri = imageUri; } + @NonNull public StatusCode getPostResult() { return postResult; } + @NonNull public String getImageUri() { return imageUri; } diff --git a/main/src/cgeo/geocaching/connector/LogResult.java b/main/src/cgeo/geocaching/connector/LogResult.java index 62111a4..9d0cb61 100644 --- a/main/src/cgeo/geocaching/connector/LogResult.java +++ b/main/src/cgeo/geocaching/connector/LogResult.java @@ -2,20 +2,26 @@ package cgeo.geocaching.connector; import cgeo.geocaching.enumerations.StatusCode; +import org.eclipse.jdt.annotation.NonNull; + public class LogResult { + @NonNull private final StatusCode postLogResult; + @NonNull private final String logId; - public LogResult(StatusCode postLogResult, String logId) { + public LogResult(@NonNull final StatusCode postLogResult, @NonNull final String logId) { this.postLogResult = postLogResult; this.logId = logId; } + @NonNull public StatusCode getPostLogResult() { return postLogResult; } + @NonNull public String getLogId() { return logId; } diff --git a/main/src/cgeo/geocaching/connector/NoLoggingManager.java b/main/src/cgeo/geocaching/connector/NoLoggingManager.java index e2e5d4c..103db68 100644 --- a/main/src/cgeo/geocaching/connector/NoLoggingManager.java +++ b/main/src/cgeo/geocaching/connector/NoLoggingManager.java @@ -1,17 +1,19 @@ package cgeo.geocaching.connector; -import cgeo.geocaching.Geocache; import cgeo.geocaching.TrackableLog; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.StatusCode; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import android.net.Uri; import java.util.Calendar; import java.util.Collections; import java.util.List; -public class NoLoggingManager extends AbstractLoggingManager { +class NoLoggingManager extends AbstractLoggingManager { @Override public void init() { @@ -19,12 +21,14 @@ public class NoLoggingManager extends AbstractLoggingManager { } @Override - public LogResult postLog(Geocache cache, LogType logType, Calendar date, String log, String logPassword, List<TrackableLog> trackableLogs) { + @NonNull + public LogResult postLog(@NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log, @Nullable final String logPassword, @NonNull final List<TrackableLog> trackableLogs) { return new LogResult(StatusCode.LOG_POST_ERROR, ""); } @Override - public ImageResult postLogImage(String logId, String imageCaption, String imageDescription, Uri imageUri) { + @NonNull + public ImageResult postLogImage(final String logId, final String imageCaption, final String imageDescription, final Uri imageUri) { return new ImageResult(StatusCode.LOG_POST_ERROR, ""); } @@ -34,6 +38,7 @@ public class NoLoggingManager extends AbstractLoggingManager { } @Override + @NonNull public List<LogType> getPossibleLogTypes() { return Collections.emptyList(); } diff --git a/main/src/cgeo/geocaching/connector/UnknownConnector.java b/main/src/cgeo/geocaching/connector/UnknownConnector.java index 05593d7..cabf03e 100644 --- a/main/src/cgeo/geocaching/connector/UnknownConnector.java +++ b/main/src/cgeo/geocaching/connector/UnknownConnector.java @@ -1,30 +1,33 @@ package cgeo.geocaching.connector; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; -public class UnknownConnector extends AbstractConnector { +class UnknownConnector extends AbstractConnector { @Override + @NonNull public String getName() { return "Unknown caches"; } @Override - public String getCacheUrl(@NonNull Geocache cache) { - return null; // we have no url for these caches + @Nullable + public String getCacheUrl(@NonNull final Geocache cache) { + return null; } @Override + @NonNull public String getHost() { - return null; // we have no host for these caches + return StringUtils.EMPTY; // we have no host for these caches } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return false; } @@ -34,7 +37,14 @@ public class UnknownConnector extends AbstractConnector { } @Override + @NonNull protected String getCacheUrlPrefix() { + throw new IllegalStateException("getCacheUrl cannot be called on unknown caches"); + } + + @Override + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { return null; } diff --git a/main/src/cgeo/geocaching/connector/UserAction.java b/main/src/cgeo/geocaching/connector/UserAction.java index e9ee4a3..082e32c 100644 --- a/main/src/cgeo/geocaching/connector/UserAction.java +++ b/main/src/cgeo/geocaching/connector/UserAction.java @@ -1,6 +1,7 @@ package cgeo.geocaching.connector; import org.eclipse.jdt.annotation.NonNull; + import rx.functions.Action1; import android.app.Activity; @@ -8,25 +9,26 @@ import android.app.Activity; public class UserAction { public static class Context { + @NonNull public final String userName; + @NonNull public final Activity activity; - public Context(String userName, Activity activity) { + public Context(@NonNull final String userName, @NonNull final Activity activity) { this.userName = userName; this.activity = activity; } } public final int displayResourceId; - private final @NonNull - Action1<Context> runnable; + @NonNull private final Action1<Context> runnable; - public UserAction(int displayResourceId, final @NonNull Action1<Context> runnable) { + public UserAction(final int displayResourceId, final @NonNull Action1<Context> runnable) { this.displayResourceId = displayResourceId; this.runnable = runnable; } - public void run(Context context) { + public void run(@NonNull final Context context) { runnable.call(context); } } diff --git a/main/src/cgeo/geocaching/connector/WaymarkingConnector.java b/main/src/cgeo/geocaching/connector/WaymarkingConnector.java index 282ee31..3361341 100644 --- a/main/src/cgeo/geocaching/connector/WaymarkingConnector.java +++ b/main/src/cgeo/geocaching/connector/WaymarkingConnector.java @@ -1,41 +1,61 @@ package cgeo.geocaching.connector; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; -public class WaymarkingConnector extends AbstractConnector { +class WaymarkingConnector extends AbstractConnector { @Override + @NonNull public String getName() { return "Waymarking"; } @Override - public String getCacheUrl(@NonNull Geocache cache) { + @NonNull + public String getCacheUrl(@NonNull final Geocache cache) { return getCacheUrlPrefix() + cache.getGeocode(); } @Override + @NonNull public String getHost() { return "www.waymarking.com"; } @Override - public boolean isOwner(ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { // this connector has no user management return false; } @Override + @NonNull protected String getCacheUrlPrefix() { return "http://" + getHost() + "/waymarks/"; } @Override - public boolean canHandle(@NonNull String geocode) { + public boolean canHandle(@NonNull final String geocode) { return StringUtils.startsWith(geocode, "WM"); } + + @Override + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { + // coord.info URLs + String code = StringUtils.substringAfterLast(url, "coord.info/"); + if (code != null && canHandle(code)) { + return code; + } + // waymarking URLs http://www.waymarking.com/waymarks/WMNCDT_American_Legion_Flagpole_1983_University_of_Oregon + code = StringUtils.substringBetween(url, "waymarks/", "_"); + if (code != null && canHandle(code)) { + return code; + } + return null; + } } diff --git a/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java b/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java index 4da9705..dd2bc8d 100644 --- a/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java +++ b/main/src/cgeo/geocaching/connector/capability/FieldNotesCapability.java @@ -2,6 +2,8 @@ package cgeo.geocaching.connector.capability; import cgeo.geocaching.connector.IConnector; +import org.eclipse.jdt.annotation.NonNull; + import java.io.File; /** @@ -9,5 +11,5 @@ import java.io.File; * */ public interface FieldNotesCapability extends IConnector { - public boolean uploadFieldNotes(final File exportFile); + public boolean uploadFieldNotes(@NonNull final File exportFile); } diff --git a/main/src/cgeo/geocaching/connector/capability/ILogin.java b/main/src/cgeo/geocaching/connector/capability/ILogin.java index 4a839c8..437eec8 100644 --- a/main/src/cgeo/geocaching/connector/capability/ILogin.java +++ b/main/src/cgeo/geocaching/connector/capability/ILogin.java @@ -7,13 +7,10 @@ import android.os.Handler; public interface ILogin extends IConnector { - /** - * Contacts the server the connector belongs to - * and verifies/establishes authentication and - * retrieves information about the current user - * (Name, found caches) if applicable. - * + * Contacts the server the connector belongs to and verifies/establishes authentication and retrieves information + * about the current user (Name, found caches) if applicable. + * * @param handler * Handler to receive status feedback * @param fromActivity @@ -23,34 +20,34 @@ public interface ILogin extends IConnector { boolean login(Handler handler, Context fromActivity); /** - * Returns the status of the last {@link}login() request + * Log out of the connector if possible. + */ + void logout(); + + /** + * Returns the status of the last {@link #login(Handler, Context)} request. * - * @return */ boolean isLoggedIn(); /** * User-centered string describing the current login/connection status * - * @return */ String getLoginStatusString(); /** - * Name the user has in this connector or empty string if not applicable - * It might be necessary to execute login before this information is valid. + * Name the user has in this connector or empty string if not applicable. + * It might be necessary to execute {@link #login(Handler, Context)} before this information is valid. * - * @return */ String getUserName(); /** - * Number of caches the user has found in this connector - * Normally retrieved/updated with (@see login). - * Might be out dated as changes on the connectors site - * are generally not notified. + * Number of caches the user has found in this connector. + * Normally retrieved/updated with {@link #login(Handler, Context)}. + * Might be stale as changes on the connectors site are generally not notified. * - * @return */ int getCachesFound(); diff --git a/main/src/cgeo/geocaching/connector/capability/ISearchByCenter.java b/main/src/cgeo/geocaching/connector/capability/ISearchByCenter.java index adc36c7..576220d 100644 --- a/main/src/cgeo/geocaching/connector/capability/ISearchByCenter.java +++ b/main/src/cgeo/geocaching/connector/capability/ISearchByCenter.java @@ -2,8 +2,8 @@ package cgeo.geocaching.connector.capability; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.IConnector; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.location.Geopoint; import org.eclipse.jdt.annotation.NonNull; diff --git a/main/src/cgeo/geocaching/connector/capability/ISearchByViewPort.java b/main/src/cgeo/geocaching/connector/capability/ISearchByViewPort.java index 7981ba8..0137c3b 100644 --- a/main/src/cgeo/geocaching/connector/capability/ISearchByViewPort.java +++ b/main/src/cgeo/geocaching/connector/capability/ISearchByViewPort.java @@ -3,10 +3,11 @@ package cgeo.geocaching.connector.capability; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.IConnector; import cgeo.geocaching.connector.gc.MapTokens; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Viewport; import org.eclipse.jdt.annotation.NonNull; public interface ISearchByViewPort extends IConnector { - public SearchResult searchByViewport(final @NonNull Viewport viewport, final MapTokens tokens); + @NonNull + public SearchResult searchByViewport(final @NonNull Viewport viewport, @NonNull final MapTokens tokens); } diff --git a/main/src/cgeo/geocaching/connector/capability/IgnoreCapability.java b/main/src/cgeo/geocaching/connector/capability/IgnoreCapability.java new file mode 100644 index 0000000..5eca3f2 --- /dev/null +++ b/main/src/cgeo/geocaching/connector/capability/IgnoreCapability.java @@ -0,0 +1,14 @@ +package cgeo.geocaching.connector.capability; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.connector.IConnector; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * Connector capability to ignore caches. + */ +public interface IgnoreCapability extends IConnector { + public boolean canIgnoreCache(final @NonNull Geocache cache); + public void ignoreCache(final @NonNull Geocache cache); +} diff --git a/main/src/cgeo/geocaching/connector/ec/ECApi.java b/main/src/cgeo/geocaching/connector/ec/ECApi.java index 421d112..86f7717 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECApi.java +++ b/main/src/cgeo/geocaching/connector/ec/ECApi.java @@ -9,22 +9,25 @@ import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.files.GPX10Parser; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; +import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.SynchronizedDateFormat; import ch.boye.httpclientandroidlib.HttpResponse; +import com.fasterxml.jackson.databind.JsonNode; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; @@ -34,18 +37,27 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; -public class ECApi { +final class ECApi { + @NonNull private static final String API_HOST = "https://extremcaching.com/exports/"; + @NonNull private static final ECLogin ecLogin = ECLogin.getInstance(); + + @NonNull private static final SynchronizedDateFormat LOG_DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); - public static String getIdFromGeocode(final String geocode) { + private ECApi() { + // utility class with static methods + } + + static String getIdFromGeocode(final String geocode) { return StringUtils.removeStartIgnoreCase(geocode, "EC"); } - public static Geocache searchByGeoCode(final String geocode) { + @Nullable + static Geocache searchByGeoCode(final String geocode) { final Parameters params = new Parameters("id", getIdFromGeocode(geocode)); final HttpResponse response = apiRequest("gpx.php", params); @@ -56,7 +68,8 @@ public class ECApi { return null; } - public static Collection<Geocache> searchByBBox(final Viewport viewport) { + @NonNull + static Collection<Geocache> searchByBBox(final Viewport viewport) { if (viewport.getLatitudeSpan() == 0 || viewport.getLongitudeSpan() == 0) { return Collections.emptyList(); @@ -72,8 +85,8 @@ public class ECApi { return importCachesFromJSON(response); } - - public static Collection<Geocache> searchByCenter(final Geopoint center) { + @NonNull + static Collection<Geocache> searchByCenter(final Geopoint center) { final Parameters params = new Parameters("fnc", "center"); params.add("distance", "20"); @@ -84,11 +97,13 @@ public class ECApi { return importCachesFromJSON(response); } - public static LogResult postLog(final Geocache cache, final LogType logType, final Calendar date, final String log) { + @NonNull + static LogResult postLog(@NonNull final Geocache cache, @NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log) { return postLog(cache, logType, date, log, false); } - public static LogResult postLog(final Geocache cache, final LogType logType, final Calendar date, final String log, boolean isRetry) { + @NonNull + private static LogResult postLog(@NonNull final Geocache cache, @NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log, final boolean isRetry) { final Parameters params = new Parameters("cache_id", cache.getGeocode()); params.add("type", logType.type); params.add("log", log); @@ -99,7 +114,7 @@ public class ECApi { final HttpResponse response = Network.postRequest(uri, params); if (response == null) { - return new LogResult(StatusCode.LOG_POST_ERROR_EC, ""); + return new LogResult(StatusCode.LOG_POST_ERROR, ""); } if (!isRetry && response.getStatusLine().getStatusCode() == 403) { if (ecLogin.login() == StatusCode.NO_ERROR) { @@ -107,7 +122,7 @@ public class ECApi { } } if (response.getStatusLine().getStatusCode() != 200) { - return new LogResult(StatusCode.LOG_POST_ERROR_EC, ""); + return new LogResult(StatusCode.LOG_POST_ERROR, ""); } final String data = Network.getResponseDataAlways(response); @@ -119,18 +134,20 @@ public class ECApi { return new LogResult(StatusCode.NO_ERROR, uid); } - return new LogResult(StatusCode.LOG_POST_ERROR_EC, ""); + return new LogResult(StatusCode.LOG_POST_ERROR, ""); } - + @Nullable private static HttpResponse apiRequest(final Parameters params) { return apiRequest("api.php", params); } + @Nullable private static HttpResponse apiRequest(final String uri, final Parameters params) { return apiRequest(uri, params, false); } + @Nullable private static HttpResponse apiRequest(final String uri, final Parameters params, final boolean isRetry) { // add session and cgeo marker on every request if (!isRetry) { @@ -155,64 +172,66 @@ public class ECApi { return response; } + @NonNull private static Collection<Geocache> importCachesFromGPXResponse(final HttpResponse response) { if (response == null) { return Collections.emptyList(); } try { - return new GPX10Parser(StoredList.TEMPORARY_LIST_ID).parse(response.getEntity().getContent(), null); - } catch (Exception e) { + return new GPX10Parser(StoredList.TEMPORARY_LIST.id).parse(response.getEntity().getContent(), null); + } catch (final Exception e) { Log.e("Error importing gpx from extremcaching.com", e); return Collections.emptyList(); } } + @NonNull private static List<Geocache> importCachesFromJSON(final HttpResponse response) { if (response != null) { try { - final String data = Network.getResponseDataAlways(response); - if (StringUtils.isBlank(data) || StringUtils.equals(data, "[]")) { + final JsonNode json = JsonUtils.reader.readTree(Network.getResponseDataAlways(response)); + if (!json.isArray()) { return Collections.emptyList(); } - final JSONArray json = new JSONArray(data); - final int len = json.length(); - final List<Geocache> caches = new ArrayList<>(len); - for (int i = 0; i < len; i++) { - final Geocache cache = parseCache(json.getJSONObject(i)); + final List<Geocache> caches = new ArrayList<>(json.size()); + for (final JsonNode node: json) { + final Geocache cache = parseCache(node); if (cache != null) { caches.add(cache); } } return caches; - } catch (final JSONException e) { - Log.w("JSONResult", e); + } catch (IOException | ClassCastException e) { + Log.w("importCachesFromJSON", e); } } return Collections.emptyList(); } - private static Geocache parseCache(final JSONObject response) { - final Geocache cache = new Geocache(); - cache.setReliableLatLon(true); + @Nullable + private static Geocache parseCache(final JsonNode response) { try { - cache.setGeocode("EC" + response.getString("cache_id")); - cache.setName(response.getString("title")); - cache.setCoords(new Geopoint(response.getString("lat"), response.getString("lon"))); - cache.setType(getCacheType(response.getString("type"))); - cache.setDifficulty((float) response.getDouble("difficulty")); - cache.setTerrain((float) response.getDouble("terrain")); - cache.setSize(CacheSize.getById(response.getString("size"))); - cache.setFound(response.getInt("found") == 1); + final Geocache cache = new Geocache(); + cache.setReliableLatLon(true); + cache.setGeocode("EC" + response.get("cache_id").asText()); + cache.setName(response.get("title").asText()); + cache.setCoords(new Geopoint(response.get("lat").asText(), response.get("lon").asText())); + cache.setType(getCacheType(response.get("type").asText())); + cache.setDifficulty((float) response.get("difficulty").asDouble()); + cache.setTerrain((float) response.get("terrain").asDouble()); + cache.setSize(CacheSize.getById(response.get("size").asText())); + cache.setFound(response.get("found").asInt() == 1); DataStore.saveCache(cache, EnumSet.of(SaveFlag.CACHE)); - } catch (final JSONException e) { + return cache; + } catch (final NullPointerException e) { Log.e("ECApi.parseCache", e); return null; } - return cache; } + @NonNull private static CacheType getCacheType(final String cacheType) { if (cacheType.equalsIgnoreCase("Tradi")) { return CacheType.TRADITIONAL; diff --git a/main/src/cgeo/geocaching/connector/ec/ECConnector.java b/main/src/cgeo/geocaching/connector/ec/ECConnector.java index a266eee..68dbee7 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECConnector.java +++ b/main/src/cgeo/geocaching/connector/ec/ECConnector.java @@ -2,7 +2,6 @@ package cgeo.geocaching.connector.ec; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; @@ -16,9 +15,9 @@ import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.connector.gc.MapTokens; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.StatusCode; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.SettingsActivity; import cgeo.geocaching.utils.CancellableHandler; @@ -37,13 +36,18 @@ import java.util.regex.Pattern; public class ECConnector extends AbstractConnector implements ISearchByGeocode, ISearchByCenter, ISearchByViewPort, ILogin, ICredentials { + @NonNull private static final String CACHE_URL = "http://extremcaching.com/index.php/output-2/"; /** * Pattern for EC codes */ + @NonNull private final static Pattern PATTERN_EC_CODE = Pattern.compile("EC[0-9]+", Pattern.CASE_INSENSITIVE); + private final CgeoApplication app = CgeoApplication.getInstance(); + + @NonNull private final ECLogin ecLogin = ECLogin.getInstance(); private ECConnector() { @@ -57,27 +61,30 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, private static final @NonNull ECConnector INSTANCE = new ECConnector(); } - public static @NonNull - ECConnector getInstance() { + @NonNull + public static ECConnector getInstance() { return Holder.INSTANCE; } @Override - public boolean canHandle(@NonNull String geocode) { + public boolean canHandle(@NonNull final String geocode) { return ECConnector.PATTERN_EC_CODE.matcher(geocode).matches(); } @Override - public String getCacheUrl(@NonNull Geocache cache) { + @NonNull + public String getCacheUrl(@NonNull final Geocache cache) { return CACHE_URL + cache.getGeocode().replace("EC", ""); } @Override + @NonNull public String getName() { return "extremcaching.com"; } @Override + @NonNull public String getHost() { return "extremcaching.com"; } @@ -95,31 +102,28 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public SearchResult searchByViewport(@NonNull Viewport viewport, final MapTokens tokens) { + @NonNull + public SearchResult searchByViewport(@NonNull final Viewport viewport, @NonNull final MapTokens tokens) { final Collection<Geocache> caches = ECApi.searchByBBox(viewport); - if (caches == null) { - return null; - } final SearchResult searchResult = new SearchResult(caches); return searchResult.filterSearchResults(false, false, Settings.getCacheType()); } @Override - public SearchResult searchByCenter(@NonNull Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { + @NonNull + public SearchResult searchByCenter(@NonNull final Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { final Collection<Geocache> caches = ECApi.searchByCenter(center); - if (caches == null) { - return null; - } final SearchResult searchResult = new SearchResult(caches); return searchResult.filterSearchResults(false, false, Settings.getCacheType()); } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return false; } @Override + @NonNull protected String getCacheUrlPrefix() { return CACHE_URL; } @@ -130,7 +134,7 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean login(Handler handler, Context fromActivity) { + public boolean login(final Handler handler, final Context fromActivity) { // login final StatusCode status = ecLogin.login(); @@ -167,7 +171,7 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public int getCacheMapMarkerId(boolean disabled) { + public int getCacheMapMarkerId(final boolean disabled) { final String icons = Settings.getECIconSet(); if (StringUtils.equals(icons, "1")) { return disabled ? R.drawable.marker_disabled_other : R.drawable.marker_other; @@ -176,6 +180,7 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, } @Override + @NonNull public String getLicenseText(final @NonNull Geocache cache) { // NOT TO BE TRANSLATED return "© " + cache.getOwnerDisplayName() + ", <a href=\"" + getCacheUrl(cache) + "\">" + getName() + "</a>, CC BY-NC-ND 3.0, alle Logeinträge © jeweiliger Autor"; @@ -187,17 +192,19 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean canLog(Geocache cache) { + public boolean canLog(@NonNull final Geocache cache) { return true; } @Override - public ILoggingManager getLoggingManager(final LogCacheActivity activity, final Geocache cache) { + @NonNull + public ILoggingManager getLoggingManager(@NonNull final LogCacheActivity activity, @NonNull final Geocache cache) { return new ECLoggingManager(activity, this, cache); } @Override - public List<LogType> getPossibleLogTypes(Geocache geocache) { + @NonNull + public List<LogType> getPossibleLogTypes(@NonNull final Geocache geocache) { final List<LogType> logTypes = new ArrayList<>(); if (geocache.isEventCache()) { logTypes.add(LogType.WILL_ATTEND); @@ -227,4 +234,14 @@ public class ECConnector extends AbstractConnector implements ISearchByGeocode, return R.string.pref_ecpassword; } + @Override + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { + final String geocode = "EC" + StringUtils.substringAfter(url, "extremcaching.com/index.php/output-2/"); + if (canHandle(geocode)) { + return geocode; + } + return super.getGeocodeFromUrl(url); + } + } diff --git a/main/src/cgeo/geocaching/connector/ec/ECLoggingManager.java b/main/src/cgeo/geocaching/connector/ec/ECLoggingManager.java index ded2d71..34c3d1b 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECLoggingManager.java +++ b/main/src/cgeo/geocaching/connector/ec/ECLoggingManager.java @@ -7,19 +7,28 @@ import cgeo.geocaching.connector.AbstractLoggingManager; import cgeo.geocaching.connector.ImageResult; import cgeo.geocaching.connector.LogResult; import cgeo.geocaching.enumerations.LogType; +import cgeo.geocaching.enumerations.StatusCode; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import android.net.Uri; import java.util.Calendar; import java.util.List; -public class ECLoggingManager extends AbstractLoggingManager { +class ECLoggingManager extends AbstractLoggingManager { + @NonNull private final ECConnector connector; + + @NonNull private final Geocache cache; - private LogCacheActivity activity; - public ECLoggingManager(final LogCacheActivity activity, final ECConnector connector, final Geocache cache) { + @NonNull + private final LogCacheActivity activity; + + ECLoggingManager(@NonNull final LogCacheActivity activity, @NonNull final ECConnector connector, @NonNull final Geocache cache) { this.connector = connector; this.cache = cache; this.activity = activity; @@ -31,16 +40,19 @@ public class ECLoggingManager extends AbstractLoggingManager { } @Override - public final LogResult postLog(final Geocache cache, final LogType logType, final Calendar date, final String log, final String logPassword, final List<TrackableLog> trackableLogs) { + @NonNull + public final LogResult postLog(@NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log, @Nullable final String logPassword, @NonNull final List<TrackableLog> trackableLogs) { return ECApi.postLog(cache, logType, date, log); } @Override + @NonNull public final ImageResult postLogImage(final String logId, final String imageCaption, final String imageDescription, final Uri imageUri) { - return null; + return new ImageResult(StatusCode.LOG_POST_ERROR, ""); } @Override + @NonNull public List<LogType> getPossibleLogTypes() { return connector.getPossibleLogTypes(cache); } diff --git a/main/src/cgeo/geocaching/connector/ec/ECLogin.java b/main/src/cgeo/geocaching/connector/ec/ECLogin.java index 012bdc9..94b450b 100644 --- a/main/src/cgeo/geocaching/connector/ec/ECLogin.java +++ b/main/src/cgeo/geocaching/connector/ec/ECLogin.java @@ -7,34 +7,42 @@ import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; + +import com.fasterxml.jackson.databind.JsonNode; + import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.json.JSONException; -import org.json.JSONObject; + +import java.io.IOException; public class ECLogin extends AbstractLogin { private final CgeoApplication app = CgeoApplication.getInstance(); - protected String sessionId = null; + private String sessionId = null; private ECLogin() { // singleton } private static class SingletonHolder { - private static ECLogin INSTANCE = new ECLogin(); + @NonNull + private final static ECLogin INSTANCE = new ECLogin(); } + @NonNull public static ECLogin getInstance() { return SingletonHolder.INSTANCE; } @Override - protected StatusCode login(boolean retry) { + @NonNull + protected StatusCode login(final boolean retry) { final ImmutablePair<String, String> login = Settings.getCredentials(ECConnector.getInstance()); if (StringUtils.isEmpty(login.left) || StringUtils.isEmpty(login.right)) { @@ -46,9 +54,9 @@ public class ECLogin extends AbstractLogin { setActualStatus(app.getString(R.string.init_login_popup_working)); final Parameters params = new Parameters("user", login.left, "pass", login.right); - HttpResponse loginResponse = Network.postRequest("https://extremcaching.com/exports/apilogin.php", params); + final HttpResponse loginResponse = Network.postRequest("https://extremcaching.com/exports/apilogin.php", params); - String loginData = Network.getResponseData(loginResponse); + final String loginData = Network.getResponseData(loginResponse); if (StringUtils.isBlank(loginData)) { Log.e("ECLogin.login: Failed to retrieve login data"); @@ -80,10 +88,9 @@ public class ECLogin extends AbstractLogin { /** * Check if the user has been logged in when he retrieved the data. * - * @param data * @return <code>true</code> if user is logged in, <code>false</code> otherwise */ - public boolean getLoginStatus(@Nullable final String data) { + private boolean getLoginStatus(@Nullable final String data) { if (StringUtils.isBlank(data) || StringUtils.equals(data, "[]")) { Log.e("ECLogin.getLoginStatus: No or empty data given"); return false; @@ -93,18 +100,18 @@ public class ECLogin extends AbstractLogin { setActualStatus(app.getString(R.string.init_login_popup_ok)); try { - final JSONObject json = new JSONObject(data); + final JsonNode json = JsonUtils.reader.readTree(data); - final String sid = json.getString("sid"); + final String sid = json.get("sid").asText(); if (!StringUtils.isBlank(sid)) { sessionId = sid; setActualLoginStatus(true); - setActualUserName(json.getString("username")); - setActualCachesFound(json.getInt("found")); + setActualUserName(json.get("username").asText()); + setActualCachesFound(json.get("found").asInt()); return true; } resetLoginStatus(); - } catch (final JSONException e) { + } catch (IOException | NullPointerException e) { Log.e("ECLogin.getLoginStatus", e); } diff --git a/main/src/cgeo/geocaching/connector/gc/GCConnector.java b/main/src/cgeo/geocaching/connector/gc/GCConnector.java index 3b7c31e..b38d2e4 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConnector.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConnector.java @@ -3,7 +3,6 @@ package cgeo.geocaching.connector.gc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; @@ -20,10 +19,11 @@ import cgeo.geocaching.connector.capability.ISearchByGeocode; import cgeo.geocaching.connector.capability.ISearchByKeyword; import cgeo.geocaching.connector.capability.ISearchByOwner; import cgeo.geocaching.connector.capability.ISearchByViewPort; +import cgeo.geocaching.connector.capability.IgnoreCapability; import cgeo.geocaching.enumerations.StatusCode; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; @@ -48,20 +48,24 @@ import java.io.File; import java.util.List; import java.util.regex.Pattern; -public class GCConnector extends AbstractConnector implements ISearchByGeocode, ISearchByCenter, ISearchByViewPort, ISearchByKeyword, ILogin, ICredentials, ISearchByOwner, ISearchByFinder, FieldNotesCapability { +public class GCConnector extends AbstractConnector implements ISearchByGeocode, ISearchByCenter, ISearchByViewPort, ISearchByKeyword, ILogin, ICredentials, ISearchByOwner, ISearchByFinder, FieldNotesCapability, IgnoreCapability { + @NonNull private static final String CACHE_URL_SHORT = "http://coord.info/"; // Double slash is used to force open in browser + @NonNull private static final String CACHE_URL_LONG = "http://www.geocaching.com/seek/cache_details.aspx?wp="; /** * Pocket queries downloaded from the website use a numeric prefix. The pocket query creator Android app adds a * verbatim "pocketquery" prefix. */ + @NonNull private static final Pattern GPX_ZIP_FILE_PATTERN = Pattern.compile("((\\d{7,})|(pocketquery))" + "(_.+)?" + "\\.zip", Pattern.CASE_INSENSITIVE); /** * Pattern for GC codes */ + @NonNull private final static Pattern PATTERN_GC_CODE = Pattern.compile("GC[0-9A-Z]+", Pattern.CASE_INSENSITIVE); private GCConnector() { @@ -75,23 +79,25 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, private static final @NonNull GCConnector INSTANCE = new GCConnector(); } - public static @NonNull - GCConnector getInstance() { + @NonNull + public static GCConnector getInstance() { return Holder.INSTANCE; } @Override - public boolean canHandle(@NonNull String geocode) { + public boolean canHandle(@NonNull final String geocode) { return GCConnector.PATTERN_GC_CODE.matcher(geocode).matches(); } @Override - public String getLongCacheUrl(@NonNull Geocache cache) { + @NonNull + public String getLongCacheUrl(@NonNull final Geocache cache) { return CACHE_URL_LONG + cache.getGeocode(); } @Override - public String getCacheUrl(@NonNull Geocache cache) { + @NonNull + public String getCacheUrl(@NonNull final Geocache cache) { return CACHE_URL_SHORT + cache.getGeocode(); } @@ -121,21 +127,24 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public ILoggingManager getLoggingManager(final LogCacheActivity activity, final Geocache cache) { + @NonNull + public ILoggingManager getLoggingManager(@NonNull final LogCacheActivity activity, @NonNull final Geocache cache) { return new GCLoggingManager(activity, cache); } @Override - public boolean canLog(Geocache cache) { + public boolean canLog(@NonNull final Geocache cache) { return StringUtils.isNotBlank(cache.getCacheId()); } @Override + @NonNull public String getName() { return "geocaching.com"; } @Override + @NonNull public String getHost() { return "www.geocaching.com"; } @@ -178,28 +187,29 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public SearchResult searchByViewport(@NonNull Viewport viewport, final MapTokens tokens) { + @NonNull + public SearchResult searchByViewport(@NonNull final Viewport viewport, @NonNull final MapTokens tokens) { return GCMap.searchByViewport(viewport, tokens); } @Override - public boolean isZippedGPXFile(final String fileName) { + public boolean isZippedGPXFile(@NonNull final String fileName) { return GPX_ZIP_FILE_PATTERN.matcher(fileName).matches(); } @Override - public boolean isReliableLatLon(boolean cacheHasReliableLatLon) { + public boolean isReliableLatLon(final boolean cacheHasReliableLatLon) { return cacheHasReliableLatLon; } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { final String user = Settings.getUsername(); return StringUtils.isNotEmpty(user) && StringUtils.equalsIgnoreCase(cache.getOwnerUserId(), user); } @Override - public boolean addToWatchlist(Geocache cache) { + public boolean addToWatchlist(@NonNull final Geocache cache) { final boolean added = GCParser.addToWatchlist(cache); if (added) { DataStore.saveChangedCache(cache); @@ -208,7 +218,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean removeFromWatchlist(Geocache cache) { + public boolean removeFromWatchlist(@NonNull final Geocache cache) { final boolean removed = GCParser.removeFromWatchlist(cache); if (removed) { DataStore.saveChangedCache(cache); @@ -226,7 +236,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, * @return <code>true</code> if the cache was successfully added, <code>false</code> otherwise */ - public static boolean addToFavorites(Geocache cache) { + public static boolean addToFavorites(final Geocache cache) { final boolean added = GCParser.addToFavorites(cache); if (added) { DataStore.saveChangedCache(cache); @@ -244,7 +254,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, * @return <code>true</code> if the cache was successfully added, <code>false</code> otherwise */ - public static boolean removeFromFavorites(Geocache cache) { + public static boolean removeFromFavorites(final Geocache cache) { final boolean removed = GCParser.removeFromFavorites(cache); if (removed) { DataStore.saveChangedCache(cache); @@ -253,7 +263,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt) { + public boolean uploadModifiedCoordinates(@NonNull final Geocache cache, @NonNull final Geopoint wpt) { final boolean uploaded = GCParser.uploadModifiedCoordinates(cache, wpt); if (uploaded) { DataStore.saveChangedCache(cache); @@ -262,7 +272,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean deleteModifiedCoordinates(Geocache cache) { + public boolean deleteModifiedCoordinates(@NonNull final Geocache cache) { final boolean deleted = GCParser.deleteModifiedCoordinates(cache); if (deleted) { DataStore.saveChangedCache(cache); @@ -271,7 +281,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean uploadPersonalNote(Geocache cache) { + public boolean uploadPersonalNote(@NonNull final Geocache cache) { final boolean uploaded = GCParser.uploadPersonalNote(cache); if (uploaded) { DataStore.saveChangedCache(cache); @@ -280,31 +290,34 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public SearchResult searchByCenter(@NonNull Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { + public SearchResult searchByCenter(@NonNull final Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { return GCParser.searchByCoords(center, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver); } @Override - public boolean supportsFavoritePoints(final Geocache cache) { + public boolean supportsFavoritePoints(@NonNull final Geocache cache) { return !cache.getType().isEvent(); } @Override + @NonNull protected String getCacheUrlPrefix() { - return null; // UNUSED + return StringUtils.EMPTY; // UNUSED } @Override - public String getGeocodeFromUrl(String url) { + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { + final String noQueryString = StringUtils.substringBefore(url, "?"); // coord.info URLs - String code = StringUtils.substringAfterLast(url, "coord.info/"); - if (code != null && canHandle(code)) { - return code; + final String afterCoord = StringUtils.substringAfterLast(noQueryString, "coord.info/"); + if (canHandle(afterCoord)) { + return afterCoord; } // expanded geocaching.com URLs - code = StringUtils.substringBetween(url, "/geocache/", "_"); - if (code != null && canHandle(code)) { - return code; + final String afterGeocache = StringUtils.substringBetween(noQueryString, "/geocache/", "_"); + if (afterGeocache != null && canHandle(afterGeocache)) { + return afterGeocache; } return null; } @@ -315,7 +328,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public int getCacheMapMarkerId(boolean disabled) { + public int getCacheMapMarkerId(final boolean disabled) { if (disabled) { return R.drawable.marker_disabled; } @@ -323,14 +336,10 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean login(Handler handler, Context fromActivity) { + public boolean login(final Handler handler, final Context fromActivity) { // login final StatusCode status = GCLogin.getInstance().login(); - if (status == StatusCode.NO_ERROR) { - GCLogin.detectGcCustomDate(); - } - if (CgeoApplication.getInstance().showLoginToast && handler != null) { handler.sendMessage(handler.obtainMessage(0, status)); CgeoApplication.getInstance().showLoginToast = false; @@ -344,6 +353,11 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override + public void logout() { + GCLogin.getInstance().logout(); + } + + @Override public String getUserName() { return GCLogin.getInstance().getActualUserName(); } @@ -364,7 +378,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public String getWaypointGpxId(String prefix, String geocode) { + public String getWaypointGpxId(final String prefix, @NonNull final String geocode) { String gpxId = prefix; if (StringUtils.isNotBlank(geocode) && geocode.length() > 2) { gpxId += geocode.substring(2); @@ -373,7 +387,8 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public String getWaypointPrefix(String name) { + @NonNull + public String getWaypointPrefix(final String name) { String prefix = name; if (StringUtils.isNotBlank(prefix) && prefix.length() >= 2) { prefix = name.substring(0, 2); @@ -382,7 +397,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public SearchResult searchByKeyword(@NonNull String keyword, final @NonNull RecaptchaReceiver recaptchaReceiver) { + public SearchResult searchByKeyword(@NonNull final String keyword, final @NonNull RecaptchaReceiver recaptchaReceiver) { return GCParser.searchByKeyword(keyword, Settings.getCacheType(), Settings.isShowCaptcha(), recaptchaReceiver); } @@ -399,18 +414,18 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, @Override public @NonNull List<UserAction> getUserActions() { - List<UserAction> actions = super.getUserActions(); + final List<UserAction> actions = super.getUserActions(); actions.add(new UserAction(R.string.user_menu_open_browser, new Action1<UserAction.Context>() { @Override - public void call(cgeo.geocaching.connector.UserAction.Context context) { + public void call(final cgeo.geocaching.connector.UserAction.Context context) { context.activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.geocaching.com/profile/?u=" + Network.encode(context.userName)))); } })); actions.add(new UserAction(R.string.user_menu_send_message, new Action1<UserAction.Context>() { @Override - public void call(cgeo.geocaching.connector.UserAction.Context context) { + public void call(final cgeo.geocaching.connector.UserAction.Context context) { 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) { @@ -433,7 +448,7 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, } @Override - public boolean uploadFieldNotes(final File exportFile) { + public boolean uploadFieldNotes(@NonNull final File exportFile) { if (!GCLogin.getInstance().isActualLoginStatus()) { // no need to upload (possibly large file) if we're not logged in final StatusCode loginState = GCLogin.getInstance().login(); @@ -468,4 +483,14 @@ public class GCConnector extends AbstractConnector implements ISearchByGeocode, return true; } + @Override + public boolean canIgnoreCache(@NonNull final Geocache cache) { + return StringUtils.isNotEmpty(cache.getType().wptTypeId); + } + + @Override + public void ignoreCache(@NonNull final Geocache cache) { + GCParser.ignoreCache(cache); + } + } diff --git a/main/src/cgeo/geocaching/connector/gc/GCConstants.java b/main/src/cgeo/geocaching/connector/gc/GCConstants.java index 3f16156..1b4c5a6 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCConstants.java +++ b/main/src/cgeo/geocaching/connector/gc/GCConstants.java @@ -15,172 +15,175 @@ import java.util.regex.Pattern; public final class GCConstants { static final String GC_URL = "http://www.geocaching.com/"; - static final String GC_TILE_URL = "http://tiles.geocaching.com/"; + private static final String GC_TILE_URL = "http://tiles.geocaching.com/"; /** Live Map */ - public final static @NonNull String URL_LIVE_MAP = GC_URL + "map/default.aspx"; + final static @NonNull String URL_LIVE_MAP = GC_URL + "map/default.aspx"; /** Live Map pop-up */ - public final static @NonNull String URL_LIVE_MAP_DETAILS = GC_TILE_URL + "map.details"; + final static @NonNull String URL_LIVE_MAP_DETAILS = GC_TILE_URL + "map.details"; /** Caches in a tile */ - public final static @NonNull String URL_MAP_INFO = GC_TILE_URL + "map.info"; + final static @NonNull String URL_MAP_INFO = GC_TILE_URL + "map.info"; /** Tile itself */ - public final static @NonNull String URL_MAP_TILE = GC_TILE_URL + "map.png"; + final static @NonNull String URL_MAP_TILE = GC_TILE_URL + "map.png"; + /** Format used by geocaching.com when user is not logged in. */ + public static final String DEFAULT_GC_DATE = "MM/dd/yyyy"; /** * Patterns for parsing the result of a (detailed) search */ - public final static Pattern PATTERN_HINT = Pattern.compile("<div id=\"div_hint\"[^>]*>(.*?)</div>", Pattern.DOTALL); - public final static Pattern PATTERN_DESC = Pattern.compile("<span id=\"ctl00_ContentBody_LongDescription\">(.*?)</span>\\s*</div>\\s*<p>\\s*</p>\\s*<p id=\"ctl00_ContentBody_hints\">", Pattern.DOTALL); - public final static Pattern PATTERN_SHORTDESC = Pattern.compile("<span id=\"ctl00_ContentBody_ShortDescription\">(.*?)</span>\\s*</div>", Pattern.DOTALL); - public final static Pattern PATTERN_GEOCODE = Pattern.compile("class=\"CoordInfoCode\">(GC[0-9A-Z]+)</span>"); - public final static Pattern PATTERN_CACHEID = Pattern.compile("/seek/log\\.aspx\\?ID=(\\d+)"); - public final static Pattern PATTERN_GUID = Pattern.compile(Pattern.quote("&wid=") + "([0-9a-z\\-]+)" + Pattern.quote("&")); - public final static Pattern PATTERN_SIZE = Pattern.compile("<div id=\"ctl00_ContentBody_size\" class=\"CacheSize[^\"]*\">[^<]*<p[^>]*>[^S]*Size[^:]*:[^<]*<span[^>]*>[^<]*<img src=\"[^\"]*/icons/container/[a-z_]+\\.gif\" alt=\"\\w+: ([^\"]+)\"[^>]*>[^<]*<small>[^<]*</small>[^<]*</span>[^<]*</p>"); - public final static Pattern PATTERN_LATLON = Pattern.compile("<span id=\"uxLatLon\"[^>]*>(.*?)</span>"); - public final static Pattern PATTERN_LATLON_ORIG = Pattern.compile("\\{\"isUserDefined\":true[^}]+?\"oldLatLngDisplay\":\"([^\"]+)\"\\}"); - public final static Pattern PATTERN_LOCATION = Pattern.compile(Pattern.quote("<span id=\"ctl00_ContentBody_Location\">In ") + "(?:<a href=[^>]*>)?(.*?)<"); - public final static Pattern PATTERN_PERSONALNOTE = Pattern.compile("<span id=\"cache_note\"[^>]*>(.*?)</span>", Pattern.DOTALL); - public final static Pattern PATTERN_NAME = Pattern.compile("<span id=\"ctl00_ContentBody_CacheName\">(.*?)</span>"); - public final static Pattern PATTERN_DIFFICULTY = Pattern.compile("<span id=\"ctl00_ContentBody_uxLegendScale\"[^>]*>[^<]*<img src=\"[^\"]*/images/stars/stars([0-9_]+)\\.gif\""); - public final static Pattern PATTERN_TERRAIN = Pattern.compile("<span id=\"ctl00_ContentBody_Localize[\\d]+\"[^>]*>[^<]*<img src=\"[^\"]*/images/stars/stars([0-9_]+)\\.gif\""); - public final static Pattern PATTERN_OWNER_USERID = Pattern.compile("other caches <a href=\"/seek/nearest\\.aspx\\?u=(.*?)\">hidden</a> or"); - public final static Pattern PATTERN_FOUND = Pattern.compile("ctl00_ContentBody_GeoNav_logText\">(Found It|Attended)"); - public final static Pattern PATTERN_FOUND_ALTERNATIVE = Pattern.compile("<div class=\"StatusInformationWidget FavoriteWidget\""); - public final static Pattern PATTERN_FOUND_DATE = Pattern.compile(">Logged on: ([^<]+?)<"); - public final static Pattern PATTERN_OWNER_DISPLAYNAME = Pattern.compile("<div id=\"ctl00_ContentBody_mcd1\">[^<]+<a href=\"[^\"]+\">([^<]+)</a>"); - public final static Pattern PATTERN_TYPE = Pattern.compile("<a href=\"/seek/nearest.aspx\\?tx=([0-9a-f-]+)"); - public final static Pattern PATTERN_HIDDEN = Pattern.compile("<div id=\"ctl00_ContentBody_mcd2\">\\W*Hidden[\\s:]*([^<]+?)</div>"); - public final static Pattern PATTERN_HIDDENEVENT = Pattern.compile("Event\\s*Date\\s*:\\s*([^<]+)<div id=\"calLinks\">", Pattern.DOTALL); - public final static Pattern PATTERN_FAVORITE = Pattern.compile("<div id=\"pnlFavoriteCache\">"); // without 'class="hideMe"' inside the tag ! - public final static Pattern PATTERN_FAVORITECOUNT = Pattern.compile("<span class=\"favorite-value\">\\D*([0-9]+?)\\D*</span>"); - public final static Pattern PATTERN_COUNTLOGS = Pattern.compile("<span id=\"ctl00_ContentBody_lblFindCounts\"><p(.+?)</p></span>"); - public final static Pattern PATTERN_LOGBOOK = Pattern.compile("initalLogs = (\\{.+\\});"); // The "inital" typo really comes from gc.com site + final static Pattern PATTERN_HINT = Pattern.compile("<div id=\"div_hint\"[^>]*>(.*?)</div>", Pattern.DOTALL); + final static Pattern PATTERN_DESC = Pattern.compile("<span id=\"ctl00_ContentBody_LongDescription\">(.*?)</span>\\s*</div>\\s*<p>\\s*</p>\\s*<p id=\"ctl00_ContentBody_hints\">", Pattern.DOTALL); + final static Pattern PATTERN_SHORTDESC = Pattern.compile("<span id=\"ctl00_ContentBody_ShortDescription\">(.*?)</span>\\s*</div>", Pattern.DOTALL); + final static Pattern PATTERN_GEOCODE = Pattern.compile("class=\"CoordInfoCode\">(GC[0-9A-Z]+)</span>"); + final static Pattern PATTERN_CACHEID = Pattern.compile("/seek/log\\.aspx\\?ID=(\\d+)"); + final static Pattern PATTERN_GUID = Pattern.compile(Pattern.quote("&wid=") + "([0-9a-z\\-]+)" + Pattern.quote("&")); + final static Pattern PATTERN_SIZE = Pattern.compile("<div id=\"ctl00_ContentBody_size\" class=\"CacheSize[^\"]*\">[^<]*<p[^>]*>[^S]*Size[^:]*:[^<]*<span[^>]*>[^<]*<img src=\"[^\"]*/icons/container/[a-z_]+\\.gif\" alt=\"\\w+: ([^\"]+)\"[^>]*>[^<]*<small>[^<]*</small>[^<]*</span>[^<]*</p>"); + final static Pattern PATTERN_LATLON = Pattern.compile("<span id=\"uxLatLon\"[^>]*>(.*?)</span>"); + final static Pattern PATTERN_LATLON_ORIG = Pattern.compile("\\{\"isUserDefined\":true[^}]+?\"oldLatLngDisplay\":\"([^\"]+)\"\\}"); + final static Pattern PATTERN_LOCATION = Pattern.compile(Pattern.quote("<span id=\"ctl00_ContentBody_Location\">In ") + "(?:<a href=[^>]*>)?(.*?)<"); + final static Pattern PATTERN_PERSONALNOTE = Pattern.compile("<span id=\"cache_note\"[^>]*>(.*?)</span>", Pattern.DOTALL); + final static Pattern PATTERN_NAME = Pattern.compile("<span id=\"ctl00_ContentBody_CacheName\">(.*?)</span>"); + final static Pattern PATTERN_DIFFICULTY = Pattern.compile("<span id=\"ctl00_ContentBody_uxLegendScale\"[^>]*>[^<]*<img src=\"[^\"]*/images/stars/stars([0-9_]+)\\.gif\""); + final static Pattern PATTERN_TERRAIN = Pattern.compile("<span id=\"ctl00_ContentBody_Localize[\\d]+\"[^>]*>[^<]*<img src=\"[^\"]*/images/stars/stars([0-9_]+)\\.gif\""); + final static Pattern PATTERN_OWNER_USERID = Pattern.compile("other caches <a href=\"/seek/nearest\\.aspx\\?u=(.*?)\">hidden</a> or"); + final static Pattern PATTERN_FOUND = Pattern.compile("ctl00_ContentBody_GeoNav_logText\">(Found It|Attended)"); + final static Pattern PATTERN_FOUND_ALTERNATIVE = Pattern.compile("<div class=\"StatusInformationWidget FavoriteWidget\""); + final static Pattern PATTERN_OWNER_DISPLAYNAME = Pattern.compile("<div id=\"ctl00_ContentBody_mcd1\">[^<]+<a href=\"[^\"]+\">([^<]+)</a>"); + final static Pattern PATTERN_TYPE = Pattern.compile("<a href=\"/seek/nearest.aspx\\?tx=([0-9a-f-]+)"); + final static Pattern PATTERN_HIDDEN = Pattern.compile("<div id=\"ctl00_ContentBody_mcd2\">\\W*Hidden[\\s:]*([^<]+?)</div>"); + final static Pattern PATTERN_HIDDENEVENT = Pattern.compile("Event\\s*Date\\s*:\\s*([^<]+)<div id=\"calLinks\">", Pattern.DOTALL); + final static Pattern PATTERN_FAVORITE = Pattern.compile("<div id=\"pnlFavoriteCache\">"); // without 'class="hideMe"' inside the tag ! + final static Pattern PATTERN_FAVORITECOUNT = Pattern.compile("<span class=\"favorite-value\">\\D*([0-9]+?)\\D*</span>"); + final static Pattern PATTERN_COUNTLOGS = Pattern.compile("<span id=\"ctl00_ContentBody_lblFindCounts\"><p(.+?)</p></span>"); /** Two groups ! */ - public final static Pattern PATTERN_COUNTLOG = Pattern.compile("<img src=\"/images/logtypes/([0-9]+)\\.png\"[^>]+> (\\d*[,.]?\\d+)"); - public static final Pattern PATTERN_PREMIUMMEMBERS = Pattern.compile("<p class=\"Warning NoBottomSpacing\">This is a Premium Member Only cache.</p>"); - public final static Pattern PATTERN_ATTRIBUTES = Pattern.compile("Attributes\\s*</h3>[^<]*<div class=\"WidgetBody\">((?:[^<]*<img src=\"[^\"]+\" alt=\"[^\"]+\"[^>]*>)+?)[^<]*<p"); + final static Pattern PATTERN_COUNTLOG = Pattern.compile("<img src=\"/images/logtypes/([0-9]+)\\.png\"[^>]+> (\\d*[,.]?\\d+)"); + static final Pattern PATTERN_PREMIUMMEMBERS = Pattern.compile("<p class=\"Warning NoBottomSpacing\">This is a Premium Member Only cache.</p>"); + final static Pattern PATTERN_ATTRIBUTES = Pattern.compile("Attributes\\s*</h3>[^<]*<div class=\"WidgetBody\">((?:[^<]*<img src=\"[^\"]+\" alt=\"[^\"]+\"[^>]*>)+?)[^<]*<p"); /** Two groups ! */ - public final static Pattern PATTERN_ATTRIBUTESINSIDE = Pattern.compile("[^<]*<img src=\"([^\"]+)\" alt=\"([^\"]+?)\""); - public final static Pattern PATTERN_SPOILER_IMAGE = Pattern.compile("<a href=\"(http://imgcdn\\.geocaching\\.com[^.]+\\.(jpg|jpeg|png|gif))\"[^>]+>([^<]+)</a>(?:<br />([^<]+)<br /><br />)?"); - public final static Pattern PATTERN_INVENTORY = Pattern.compile("<span id=\"ctl00_ContentBody_uxTravelBugList_uxInventoryLabel\">\\W*Inventory[^<]*</span>[^<]*</h3>[^<]*<div class=\"WidgetBody\">([^<]*<ul>(([^<]*<li>[^<]*<a href=\"[^\"]+\"[^>]*>[^<]*<img src=\"[^\"]+\"[^>]*>[^<]*<span>[^<]+<\\/span>[^<]*<\\/a>[^<]*<\\/li>)+)[^<]*<\\/ul>)?"); - public final static Pattern PATTERN_INVENTORYINSIDE = Pattern.compile("[^<]*<li>[^<]*<a href=\"[a-z0-9\\-\\_\\.\\?\\/\\:\\@]*\\/track\\/details\\.aspx\\?guid=([0-9a-z\\-]+)[^\"]*\"[^>]*>[^<]*<img src=\"[^\"]+\"[^>]*>[^<]*<span>([^<]+)<\\/span>[^<]*<\\/a>[^<]*<\\/li>"); - public final static Pattern PATTERN_WATCHLIST = Pattern.compile(Pattern.quote("watchlist.aspx") + ".{1,50}" + Pattern.quote("action=rem")); - public final static Pattern PATTERN_RELATED_WEB_PAGE = Pattern.compile("id=\"ctl00_ContentBody_uxCacheUrl\" title=\"Related Web Page\" href=\"(.*?)\">"); + final static Pattern PATTERN_ATTRIBUTESINSIDE = Pattern.compile("[^<]*<img src=\"([^\"]+)\" alt=\"([^\"]+?)\""); + final static Pattern PATTERN_SPOILER_IMAGE = Pattern.compile("<a href=\"(http://img(?:cdn)?\\.geocaching\\.com[^.]+\\.(jpg|jpeg|png|gif))\"[^>]+>([^<]+)</a>(?:<br />([^<]+)<br /><br />)?"); + final static Pattern PATTERN_INVENTORY = Pattern.compile("<span id=\"ctl00_ContentBody_uxTravelBugList_uxInventoryLabel\">\\W*Inventory[^<]*</span>[^<]*</h3>[^<]*<div class=\"WidgetBody\">([^<]*<ul>(([^<]*<li>[^<]*<a href=\"[^\"]+\"[^>]*>[^<]*<img src=\"[^\"]+\"[^>]*>[^<]*<span>[^<]+<\\/span>[^<]*<\\/a>[^<]*<\\/li>)+)[^<]*<\\/ul>)?"); + final static Pattern PATTERN_INVENTORYINSIDE = Pattern.compile("[^<]*<li>[^<]*<a href=\"[a-z0-9\\-\\_\\.\\?\\/\\:\\@]*\\/track\\/details\\.aspx\\?guid=([0-9a-z\\-]+)[^\"]*\"[^>]*>[^<]*<img src=\"[^\"]+\"[^>]*>[^<]*<span>([^<]+)<\\/span>[^<]*<\\/a>[^<]*<\\/li>"); + final static Pattern PATTERN_WATCHLIST = Pattern.compile(Pattern.quote("watchlist.aspx") + ".{1,50}" + Pattern.quote("action=rem")); + final static Pattern PATTERN_RELATED_WEB_PAGE = Pattern.compile("id=\"ctl00_ContentBody_uxCacheUrl\" title=\"Related Web Page\" href=\"(.*?)\">"); // Info box top-right - public static final Pattern PATTERN_LOGIN_NAME = Pattern.compile("\"SignedInProfileLink\">(.*?)</a>"); - public static final Pattern PATTERN_MEMBER_STATUS = Pattern.compile("<span id=\"ctl00_litPMLevel\">([^<]+)</span>"); - public static final String MEMBER_STATUS_RENEW = "<a id=\"ctl00_hlRenew"; - public static final String MEMBER_STATUS_PM = "Premium Member"; + public static final Pattern PATTERN_LOGIN_NAME = Pattern.compile("class=\"li-user-info\"[^>]*>.*?<span>(.*?)</span>", Pattern.DOTALL); + public static final Pattern PATTERN_MEMBERSHIP = Pattern.compile("<dl class=\"membership-details\">.*?<dt>Membership</dt>.*?<dd>\\s*(\\p{Alpha}+)\\s*</dd>", Pattern.DOTALL); + public static final String MEMBER_STATUS_PREMIUM = "Premium"; + public static final String MEMBER_STATUS_CHARTER = "Charter"; /** Use replaceAll("[,.]","") on the resulting string before converting to an int */ - 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_MAP_LOGGED_IN = Pattern.compile("<a href=\"https?://www.geocaching.com/my/\" class=\"CommonUsername\""); + static final Pattern PATTERN_CACHES_FOUND = Pattern.compile("<span[^>]*>.*?([\\d,.]+) Finds?", Pattern.DOTALL); + static final Pattern PATTERN_AVATAR_IMAGE_PROFILE_PAGE = Pattern.compile("src=\"(https?://(img(?:cdn)?\\.geocaching\\.com|[^>\"]+\\.cloudfront\\.net)/avatar/[0-9a-f-]+\\.jpg)\"[^>]*alt=\""); + static final Pattern PATTERN_LOGIN_NAME_LOGIN_PAGE = Pattern.compile("ctl00_ContentBody_lbUsername\">.*<strong>(.*)</strong>"); + static final Pattern PATTERN_CUSTOMDATE = Pattern.compile("<option selected=\"selected\" value=\"([ /.Mdy-]+)\">"); + static final Pattern PATTERN_HOME_LOCATION = Pattern.compile("<input class=\"search-coordinates\"[^>]* value=\"(.*?)\""); + static final Pattern PATTERN_MAP_LOGGED_IN = Pattern.compile("<a href=\"https?://www.geocaching.com/my/\" class=\"CommonUsername\""); /** * Patterns for parsing trackables */ - public final static Pattern PATTERN_TRACKABLE_GUID = Pattern.compile("<a id=\"ctl00_ContentBody_lnkPrint\" title=\"[^\"]*\" href=\".*sheet\\.aspx\\?guid=([a-z0-9\\-]+)\"[^>]*>[^<]*</a>"); - public final static Pattern PATTERN_TRACKABLE_GEOCODE = Pattern.compile("<strong>(TB[0-9A-Z]+)[^<]*</strong> to reference this item."); + final static Pattern PATTERN_TRACKABLE_GUID = Pattern.compile("<a id=\"ctl00_ContentBody_lnkPrint\" title=\"[^\"]*\" href=\".*sheet\\.aspx\\?guid=([a-z0-9\\-]+)\"[^>]*>[^<]*</a>"); + final static Pattern PATTERN_TRACKABLE_GEOCODE = Pattern.compile("<strong>(TB[0-9A-Z]+)[^<]*</strong> to reference this item."); // multiple error codes, depending on the search term for the trackable code - public final static String ERROR_TB_DOES_NOT_EXIST = "does not exist in the system"; - public final static String ERROR_TB_ELEMENT_EXCEPTION = "ElementNotFound Exception"; - public final static String ERROR_TB_ARITHMETIC_OVERFLOW = "operation resulted in an overflow"; - public final static String ERROR_TB_NOT_ACTIVATED = "hasn't been activated"; + final static String ERROR_TB_DOES_NOT_EXIST = "does not exist in the system"; + final static String ERROR_TB_ELEMENT_EXCEPTION = "ElementNotFound Exception"; + final static String ERROR_TB_ARITHMETIC_OVERFLOW = "operation resulted in an overflow"; + final static String ERROR_TB_NOT_ACTIVATED = "hasn't been activated"; /** * some parts of the webpage don't correctly encode the name, therefore this pattern must be checked with a * trackable name that needs HTML encoding */ - public final static Pattern PATTERN_TRACKABLE_NAME = Pattern.compile("<span id=\"ctl00_ContentBody_lbHeading\">(.*?)</span>"); + final static Pattern PATTERN_TRACKABLE_NAME = Pattern.compile("<span id=\"ctl00_ContentBody_lbHeading\">(.*?)</span>"); /** Two groups ! */ - public final static Pattern PATTERN_TRACKABLE_OWNER = Pattern.compile("<a id=\"ctl00_ContentBody_BugDetails_BugOwner\"[^>]+href=\"https?://www.geocaching.com/profile/\\?guid=(.*?)\">(.*?)</a>"); - public final static Pattern PATTERN_TRACKABLE_RELEASES = Pattern.compile("<dt>\\W*Released:[^<]*</dt>[^<]*<dd>[^<]*<span id=\"ctl00_ContentBody_BugDetails_BugReleaseDate\">([^<]+)<\\/span>[^<]*</dd>"); - public final static Pattern PATTERN_TRACKABLE_ORIGIN = Pattern.compile("<dt>\\W*Origin:[^<]*</dt>[^<]*<dd>[^<]*<span id=\"ctl00_ContentBody_BugDetails_BugOrigin\">([^<]+)<\\/span>[^<]*</dd>"); + final static Pattern PATTERN_TRACKABLE_OWNER = Pattern.compile("<a id=\"ctl00_ContentBody_BugDetails_BugOwner\"[^>]+href=\"https?://www.geocaching.com/profile/\\?guid=(.*?)\">(.*?)</a>"); + final static Pattern PATTERN_TRACKABLE_RELEASES = Pattern.compile("<dt>\\W*Released:[^<]*</dt>[^<]*<dd>[^<]*<span id=\"ctl00_ContentBody_BugDetails_BugReleaseDate\">([^<]+)<\\/span>[^<]*</dd>"); + final static Pattern PATTERN_TRACKABLE_ORIGIN = Pattern.compile("<dt>\\W*Origin:[^<]*</dt>[^<]*<dd>[^<]*<span id=\"ctl00_ContentBody_BugDetails_BugOrigin\">([^<]+)<\\/span>[^<]*</dd>"); /** Two groups ! */ - public final static Pattern PATTERN_TRACKABLE_SPOTTEDCACHE = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\" title=\"[^\"]*\" href=\"[^\"]*/seek/cache_details.aspx\\?guid=([a-z0-9\\-]+)\">In ([^<]+)</a>[^<]*</dd>"); + final static Pattern PATTERN_TRACKABLE_SPOTTEDCACHE = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\" title=\"[^\"]*\" href=\"[^\"]*/seek/cache_details.aspx\\?guid=([a-z0-9\\-]+)\">In ([^<]+)</a>[^<]*</dd>"); /** Two groups ! */ - public final static Pattern PATTERN_TRACKABLE_SPOTTEDUSER = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\" href=\"[^\"]*/profile/\\?guid=([a-z0-9\\-]+)\">In the hands of ([^<]+).</a>[^<]*</dd>"); - public final static Pattern PATTERN_TRACKABLE_SPOTTEDUNKNOWN = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\">Unknown Location[^<]*</a>[^<]*</dd>"); - public final static Pattern PATTERN_TRACKABLE_SPOTTEDOWNER = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\">In the hands of the owner[^<]*</a>[^<]*</dd>"); - public final static Pattern PATTERN_TRACKABLE_GOAL = Pattern.compile("<div id=\"TrackableGoal\">[^<]*<p>(.*?)</p>[^<]*</div>", Pattern.DOTALL); + final static Pattern PATTERN_TRACKABLE_SPOTTEDUSER = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\" href=\"[^\"]*/profile/\\?guid=([a-z0-9\\-]+)\">In the hands of ([^<]+).</a>[^<]*</dd>"); + final static Pattern PATTERN_TRACKABLE_SPOTTEDUNKNOWN = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\">Unknown Location[^<]*</a>[^<]*</dd>"); + final static Pattern PATTERN_TRACKABLE_SPOTTEDOWNER = Pattern.compile("<dt>\\W*Recently Spotted:[^<]*</dt>[^<]*<dd>[^<]*<a id=\"ctl00_ContentBody_BugDetails_BugLocation\">In the hands of the owner[^<]*</a>[^<]*</dd>"); + final static Pattern PATTERN_TRACKABLE_GOAL = Pattern.compile("<div id=\"TrackableGoal\">[^<]*<p>(.*?)</p>[^<]*</div>", Pattern.DOTALL); /** Four groups */ - public final static Pattern PATTERN_TRACKABLE_DETAILSIMAGE = Pattern.compile("<h3>\\W*About This Item[^<]*</h3>[^<]*<div id=\"TrackableDetails\">([^<]*<p>([^<]*<img id=\"ctl00_ContentBody_BugDetails_BugImage\" class=\"[^\"]+\" src=\"([^\"]+)\"[^>]*>)?[^<]*</p>)?[^<]*<p[^>]*>(.*)</p>[^<]*</div> <div id=\"ctl00_ContentBody_BugDetails_uxAbuseReport\">"); - public final static Pattern PATTERN_TRACKABLE_ICON = Pattern.compile("<img id=\"ctl00_ContentBody_BugTypeImage\" class=\"TravelBugHeaderIcon\" src=\"([^\"]+)\"[^>]*>"); - public final static Pattern PATTERN_TRACKABLE_TYPE = Pattern.compile("<img id=\"ctl00_ContentBody_BugTypeImage\" class=\"TravelBugHeaderIcon\" src=\"[^\"]+\" alt=\"([^\"]+)\"[^>]*>"); - public final static Pattern PATTERN_TRACKABLE_DISTANCE = Pattern.compile("<h4[^>]*\\W*Tracking History \\(([0-9.,]+(km|mi))[^\\)]*\\)"); - public final static Pattern PATTERN_TRACKABLE_LOG = Pattern.compile("<tr class=\"Data BorderTop .+?/images/logtypes/([^.]+)\\.png[^>]+> ([^<]+)</td>.+?guid.+?>([^<]+)</a>.+?(?:guid=([^\"]+)\">(<span[^>]+>)?([^<]+)</.+?)?<td colspan=\"4\">\\s*<div>(.*?)</div>\\s*(?:<ul.+?ul>)?\\s*</td>\\s*</tr>"); - public final static Pattern PATTERN_TRACKABLE_LOG_IMAGES = Pattern.compile("<li><a href=\"([^\"]+)\".+?LogImgTitle.+?>([^<]+)</"); + final static Pattern PATTERN_TRACKABLE_DETAILSIMAGE = Pattern.compile("<h3>\\W*About This Item[^<]*</h3>[^<]*<div id=\"TrackableDetails\">([^<]*<p>([^<]*<img id=\"ctl00_ContentBody_BugDetails_BugImage\" class=\"[^\"]+\" src=\"([^\"]+)\"[^>]*>)?[^<]*</p>)?[^<]*<p[^>]*>(.*)</p>[^<]*</div> <div id=\"ctl00_ContentBody_BugDetails_uxAbuseReport\">"); + final static Pattern PATTERN_TRACKABLE_ICON = Pattern.compile("<img id=\"ctl00_ContentBody_BugTypeImage\" class=\"TravelBugHeaderIcon\" src=\"([^\"]+)\"[^>]*>"); + final static Pattern PATTERN_TRACKABLE_TYPE = Pattern.compile("<img id=\"ctl00_ContentBody_BugTypeImage\" class=\"TravelBugHeaderIcon\" src=\"[^\"]+\" alt=\"([^\"]+)\"[^>]*>"); + final static Pattern PATTERN_TRACKABLE_DISTANCE = Pattern.compile("<h4[^>]*\\W*Tracking History \\(([0-9.,]+(km|mi))[^\\)]*\\)"); + final static Pattern PATTERN_TRACKABLE_LOG = Pattern.compile("<tr class=\"Data BorderTop .+?/images/logtypes/([^.]+)\\.png[^>]+> ([^<]+)</td>.+?guid.+?>([^<]+)</a>.+?(?:guid=([^\"]+)\">(<span[^>]+>)?([^<]+)</.+?)?<td colspan=\"4\">\\s*<div>(.*?)</div>\\s*(?:<ul.+?ul>)?\\s*</td>\\s*</tr>"); + final static Pattern PATTERN_TRACKABLE_LOG_IMAGES = Pattern.compile("<li><a href=\"([^\"]+)\".+?LogImgTitle.+?>([^<]*)</"); /** * Patterns for parsing the result of a search (next) */ - public final static Pattern PATTERN_SEARCH_TYPE = Pattern.compile("<td class=\"Merge\">.*?<img src=\"[^\"]*/images/wpttypes/[^.]+\\.gif\" alt=\"([^\"]+)\" title=\"[^\"]+\"[^>]*>[^<]*</a>"); - public final static Pattern PATTERN_SEARCH_GUIDANDDISABLED = Pattern.compile("SearchResultsWptType.*?<a href=\"[^\"]*\" class=\"lnk ([^\"]*)\"><span>([^<]*)</span>[^|]*[|][^|]*[|]([^<]*)<"); + final static Pattern PATTERN_SEARCH_TYPE = Pattern.compile("<td class=\"Merge\">.*?<img src=\"[^\"]*/images/wpttypes/[^.]+\\.gif\" alt=\"([^\"]+)\" title=\"[^\"]+\"[^>]*>[^<]*</a>"); + final static Pattern PATTERN_SEARCH_GUIDANDDISABLED = Pattern.compile("SearchResultsWptType.*?<a href=\"[^\"]*\" class=\"lnk ([^\"]*)\"><span>([^<]*)</span>[^|]*[|][^|]*[|]([^<]*)<"); /** Two groups **/ - public final static Pattern PATTERN_SEARCH_TRACKABLES = Pattern.compile("<a id=\"ctl00_ContentBody_dlResults_ctl[0-9]+_uxTravelBugList\" class=\"tblist\" data-tbcount=\"([0-9]+)\" data-id=\"[^\"]*\"[^>]*>(.*)</a>"); + final static Pattern PATTERN_SEARCH_TRACKABLES = Pattern.compile("<a id=\"ctl00_ContentBody_dlResults_ctl[0-9]+_uxTravelBugList\" class=\"tblist\" data-tbcount=\"([0-9]+)\" data-id=\"[^\"]*\"[^>]*>(.*)</a>"); /** Second group used */ - public final static Pattern PATTERN_SEARCH_TRACKABLESINSIDE = Pattern.compile("(<img src=\"[^\"]+\" alt=\"([^\"]+)\" title=\"[^\"]*\" />[^<]*)"); - public final static Pattern PATTERN_SEARCH_DIRECTION_DISTANCE = Pattern.compile("<img src=\"/images/icons/compass/([^\\.]+)\\.gif\"[^>]*>[^<]*<br />([^<]+)</span>"); - public final static Pattern PATTERN_SEARCH_DIFFICULTY_TERRAIN = Pattern.compile("<span class=\"small\">([0-5]([\\.,]5)?)/([0-5]([\\.,]5)?)</span><br />"); - public final static Pattern PATTERN_SEARCH_CONTAINER = Pattern.compile("<img src=\"/images/icons/container/([^\\.]+)\\.gif\""); - public final static Pattern PATTERN_SEARCH_GEOCODE = Pattern.compile("\\|\\W*(GC[0-9A-Z]+)[^\\|]*\\|"); - public final static Pattern PATTERN_SEARCH_ID = Pattern.compile("name=\"CID\"[^v]*value=\"(\\d+)\""); - public final static Pattern PATTERN_SEARCH_FAVORITE = Pattern.compile("favorite-rank\">([0-9,.]+)</span>"); - 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=([^\"]+)\"[^>]*>"); + final static Pattern PATTERN_SEARCH_TRACKABLESINSIDE = Pattern.compile("(<img src=\"[^\"]+\" alt=\"([^\"]+)\" title=\"[^\"]*\" />[^<]*)"); + final static Pattern PATTERN_SEARCH_DIRECTION_DISTANCE = Pattern.compile("<img src=\"/images/icons/compass/([^\\.]+)\\.gif\"[^>]*>[^<]*<br />([^<]+)</span>"); + final static Pattern PATTERN_SEARCH_DIFFICULTY_TERRAIN = Pattern.compile("<span class=\"small\">([0-5]([\\.,]5)?)/([0-5]([\\.,]5)?)</span><br />"); + final static Pattern PATTERN_SEARCH_CONTAINER = Pattern.compile("<img src=\"/images/icons/container/([^\\.]+)\\.gif\""); + final static Pattern PATTERN_SEARCH_GEOCODE = Pattern.compile("\\|\\W*(GC[0-9A-Z]+)[^\\|]*\\|"); + final static Pattern PATTERN_SEARCH_ID = Pattern.compile("name=\"CID\"[^v]*value=\"(\\d+)\""); + final static Pattern PATTERN_SEARCH_FAVORITE = Pattern.compile("favorite-rank\">([0-9,.]+)</span>"); + final static Pattern PATTERN_SEARCH_TOTALCOUNT = Pattern.compile("<span>Total Records\\D*(\\d+)<"); + 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 style=\"width:70px\">[^<]+<span class=\"small\">([^<]+)</span>"); + final static Pattern PATTERN_SEARCH_HIDDEN_DATE = Pattern.compile("<td style=\"width:70px\">[^<]+<span class=\"small\">([^<]+)</span>"); + final static Pattern PATTERN_SEARCH_POST_ACTION = Pattern.compile("<form name=\"aspnetForm\" method=\"post\" action=\"(.*)\" id=\"aspnetForm\""); /** * Patterns for waypoints */ - public final static Pattern PATTERN_WPTYPE = Pattern.compile("\\/wpttypes\\/sm\\/(.+)\\.jpg"); - public final static Pattern PATTERN_WPPREFIXORLOOKUPORLATLON = Pattern.compile(">([^<]*<[^>]+>)?([^<]+)(<[^>]+>[^<]*)?<\\/td>"); - public final static Pattern PATTERN_WPNAME = Pattern.compile(">[^<]*<a[^>]+>([^<]*)<\\/a>"); - public final static Pattern PATTERN_WPNOTE = Pattern.compile("colspan=\"6\">(.*)" + Pattern.quote("</td>"), Pattern.DOTALL); + final static Pattern PATTERN_WPTYPE = Pattern.compile("\\/wpttypes\\/sm\\/(.+)\\.jpg"); + final static Pattern PATTERN_WPPREFIXORLOOKUPORLATLON = Pattern.compile(">([^<]*<[^>]+>)?([^<]+)(<[^>]+>[^<]*)?<\\/td>"); + final static Pattern PATTERN_WPNAME = Pattern.compile(">[^<]*<a[^>]+>([^<]*)<\\/a>"); + final static Pattern PATTERN_WPNOTE = Pattern.compile("colspan=\"6\">(.*)" + Pattern.quote("</td>"), Pattern.DOTALL); /** * Patterns for different purposes */ /** replace line break and paragraph tags */ - public final static Pattern PATTERN_LINEBREAK = Pattern.compile("<(br|p)[^>]*>"); - public final static Pattern PATTERN_TYPEBOX = Pattern.compile("<select name=\"ctl00\\$ContentBody\\$LogBookPanel1\\$ddLogType\" id=\"ctl00_ContentBody_LogBookPanel1_ddLogType\"[^>]*>" + final static Pattern PATTERN_LINEBREAK = Pattern.compile("<(br|p)[^>]*>"); + final static Pattern PATTERN_TYPEBOX = Pattern.compile("<select name=\"ctl00\\$ContentBody\\$LogBookPanel1\\$ddLogType\" id=\"ctl00_ContentBody_LogBookPanel1_ddLogType\"[^>]*>" + "(([^<]*<option[^>]*>[^<]+</option>)+)[^<]*</select>", Pattern.CASE_INSENSITIVE); - public final static Pattern PATTERN_TYPE2 = Pattern.compile("<option( selected=\"selected\")? value=\"(\\d+)\">[^<]+</option>", Pattern.CASE_INSENSITIVE); + final static Pattern PATTERN_TYPE2 = Pattern.compile("<option( selected=\"selected\")? value=\"(\\d+)\">[^<]+</option>", Pattern.CASE_INSENSITIVE); // FIXME: pattern is over specified - public final static Pattern PATTERN_TRACKABLE = Pattern.compile("<tr id=\"ctl00_ContentBody_LogBookPanel1_uxTrackables_repTravelBugs_ctl[0-9]+_row\"[^>]*>" + final static Pattern PATTERN_TRACKABLE = Pattern.compile("<tr id=\"ctl00_ContentBody_LogBookPanel1_uxTrackables_repTravelBugs_ctl[0-9]+_row\"[^>]*>" + "[^<]*<td>[^<]*<a href=\"[^\"]+\">([A-Z0-9]+)</a>[^<]*</td>[^<]*<td>([^<]+)</td>[^<]*<td>" + "[^<]*<select name=\"ctl00\\$ContentBody\\$LogBookPanel1\\$uxTrackables\\$repTravelBugs\\$ctl([0-9]+)\\$ddlAction\"[^>]*>" + "([^<]*<option value=\"([0-9]+)(_[a-z]+)?\">[^<]+</option>)+" + "[^<]*</select>[^<]*</td>[^<]*</tr>", Pattern.CASE_INSENSITIVE); - public final static Pattern PATTERN_MAINTENANCE = Pattern.compile("<span id=\"ctl00_ContentBody_LogBookPanel1_lbConfirm\"[^>]*>([^<]*<font[^>]*>)?([^<]+)(</font>[^<]*)?</span>", Pattern.CASE_INSENSITIVE); - public final static Pattern PATTERN_OK1 = Pattern.compile("<h2[^>]*>[^<]*<span id=\"ctl00_ContentBody_lbHeading\"[^>]*>[^<]*</span>[^<]*</h2>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); - public final static Pattern PATTERN_OK2 = Pattern.compile("<div id=[\"|']ctl00_ContentBody_LogBookPanel1_ViewLogPanel[\"|']>", Pattern.CASE_INSENSITIVE); - public final static Pattern PATTERN_IMAGE_UPLOAD_URL = Pattern.compile("title=\"Click for Larger Image\"\\s*src=\"(.*?)\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); - public final static Pattern PATTERN_VIEWSTATEFIELDCOUNT = Pattern.compile("id=\"__VIEWSTATEFIELDCOUNT\"[^(value)]+value=\"(\\d+)\"[^>]+>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); - public final static Pattern PATTERN_VIEWSTATES = Pattern.compile("id=\"__VIEWSTATE(\\d*)\"[^(value)]+value=\"([^\"]+)\"[^>]+>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); - public final static Pattern PATTERN_USERTOKEN = Pattern.compile("userToken\\s*=\\s*'([^']+)'"); - public final static Pattern PATTERN_LIST_PQ = Pattern.compile(Pattern.quote("(") + "(\\d+)" + Pattern.quote(")") + ".+?guid=(.+?)\".*?title=\"(.+?)\""); + final static Pattern PATTERN_MAINTENANCE = Pattern.compile("<span id=\"ctl00_ContentBody_LogBookPanel1_lbConfirm\"[^>]*>([^<]*<font[^>]*>)?([^<]+)(</font>[^<]*)?</span>", Pattern.CASE_INSENSITIVE); + final static Pattern PATTERN_OK1 = Pattern.compile("<h2[^>]*>[^<]*<span id=\"ctl00_ContentBody_lbHeading\"[^>]*>[^<]*</span>[^<]*</h2>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + final static Pattern PATTERN_OK2 = Pattern.compile("<div id=[\"|']ctl00_ContentBody_LogBookPanel1_ViewLogPanel[\"|']>", Pattern.CASE_INSENSITIVE); + final static Pattern PATTERN_IMAGE_UPLOAD_URL = Pattern.compile("title=\"Click for Larger Image\"\\s*src=\"(.*?)\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + final static Pattern PATTERN_VIEWSTATEFIELDCOUNT = Pattern.compile("id=\"__VIEWSTATEFIELDCOUNT\"[^(value)]+value=\"(\\d+)\"[^>]+>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + final static Pattern PATTERN_VIEWSTATES = Pattern.compile("id=\"__VIEWSTATE(\\d*)\"[^(value)]+value=\"([^\"]+)\"[^>]+>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + final static Pattern PATTERN_USERTOKEN = Pattern.compile("userToken\\s*=\\s*'([^']+)'"); + final static Pattern PATTERN_LIST_PQ = Pattern.compile(Pattern.quote("(") + "(\\d+)" + Pattern.quote(")") + ".+?guid=(.+?)\".*?title=\"(.+?)\""); /** Live Map since 14.02.2012 */ - public final static Pattern PATTERN_USERSESSION = Pattern.compile("UserSession\\('([^']+)'"); - public final static Pattern PATTERN_SESSIONTOKEN = Pattern.compile("sessionToken:'([^']+)'"); + final static Pattern PATTERN_USERSESSION = Pattern.compile("UserSession\\('([^']+)'"); + final static Pattern PATTERN_SESSIONTOKEN = Pattern.compile("sessionToken:'([^']+)'"); - public final static Pattern PATTERN_LOG_IMAGE_UPLOAD = Pattern.compile("/seek/upload\\.aspx\\?LID=(\\d+)", Pattern.CASE_INSENSITIVE); + static final Pattern PATTERN_LOG_GUID = Pattern.compile("<a id=\"ctl00_ContentBody_LogBookPanel1_WaypointLink\"[^>]* href=\"https?://www\\.geocaching\\.com/seek/cache_details\\.aspx\\?guid=([0-9a-f-]+)\""); + final static Pattern PATTERN_LOG_IMAGE_UPLOAD = Pattern.compile("/seek/upload\\.aspx\\?LID=(\\d+)", Pattern.CASE_INSENSITIVE); - 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=\"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,"; - public final static String STRING_CACHEDETAILS = "id=\"cacheDetails\""; + final static String STRING_PREMIUMONLY_2 = "Sorry, the owner of this listing has made it viewable to Premium Members only."; + final static String STRING_PREMIUMONLY_1 = "has chosen to make this cache listing visible to Premium Members only."; + final static String STRING_UNPUBLISHED_OTHER = "you cannot view this cache listing until it has been published"; + final static String STRING_UNPUBLISHED_FROM_SEARCH = "class=\"UnpublishedCacheSearchWidget"; // do not include closing brace as the CSS can contain additional styles + final static String STRING_UNKNOWN_ERROR = "An Error Has Occurred"; + final static String STRING_DISABLED = "<li>This cache is temporarily unavailable."; + final static String STRING_ARCHIVED = "<li>This cache has been archived,"; + final static String STRING_CACHEDETAILS = "id=\"cacheDetails\""; /** Number of logs to retrieve from GC.com */ - public final static int NUMBER_OF_LOGS = 35; + final static int NUMBER_OF_LOGS = 35; /** Maximum number of chars for personal note. **/ public final static int PERSONAL_NOTE_MAX_CHARS = 500; diff --git a/main/src/cgeo/geocaching/connector/gc/GCLoggingManager.java b/main/src/cgeo/geocaching/connector/gc/GCLoggingManager.java index bf021b9..bdea155 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCLoggingManager.java +++ b/main/src/cgeo/geocaching/connector/gc/GCLoggingManager.java @@ -1,5 +1,6 @@ package cgeo.geocaching.connector.gc; +import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.R; @@ -14,9 +15,11 @@ import cgeo.geocaching.loaders.UrlLoader; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.TextUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import android.net.Uri; @@ -28,24 +31,24 @@ import java.util.Calendar; import java.util.Collections; import java.util.List; -public class GCLoggingManager extends AbstractLoggingManager implements LoaderManager.LoaderCallbacks<String> { +class GCLoggingManager extends AbstractLoggingManager implements LoaderManager.LoaderCallbacks<String> { private final LogCacheActivity activity; private final Geocache cache; private String[] viewstates; - private List<TrackableLog> trackables; + @NonNull private List<TrackableLog> trackables = Collections.emptyList(); private List<LogType> possibleLogTypes; private boolean hasLoaderError = true; - public GCLoggingManager(final LogCacheActivity activity, final Geocache cache) { + GCLoggingManager(final LogCacheActivity activity, final Geocache cache) { this.activity = activity; this.cache = cache; } @Nullable @Override - public Loader<String> onCreateLoader(int arg0, Bundle arg1) { + public Loader<String> onCreateLoader(final int arg0, final Bundle arg1) { if (!Settings.isLogin()) { // allow offline logging ActivityMixin.showToast(activity, activity.getResources().getString(R.string.err_login)); return null; @@ -54,15 +57,24 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } @Override - public void onLoadFinished(Loader<String> arg0, String page) { - + public void onLoadFinished(final Loader<String> arg0, final String page) { if (page == null) { hasLoaderError = true; } else { - viewstates = GCLogin.getViewstates(page); trackables = GCParser.parseTrackableLog(page); possibleLogTypes = GCParser.parseTypes(page); + if (StringUtils.isBlank(cache.getGuid())) { + // Acquire the cache GUID from the log page. This will not only complete the information in the database, + // but also allow the user to post a rating using GCVote since it requires the GUID to do so. + final String guid = TextUtils.getMatch(page, GCConstants.PATTERN_LOG_GUID, null); + if (StringUtils.isNotBlank(guid)) { + cache.setGuid(guid); + DataStore.saveChangedCache(cache); + } else { + Log.w("Could not acquire GUID from log page for " + cache.getGeocode()); + } + } hasLoaderError = possibleLogTypes.isEmpty(); } @@ -71,7 +83,7 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } @Override - public void onLoaderReset(Loader<String> arg0) { + public void onLoaderReset(final Loader<String> arg0) { // nothing to do } @@ -81,7 +93,8 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } @Override - public LogResult postLog(Geocache cache, LogType logType, Calendar date, String log, String logPassword, List<TrackableLog> trackableLogs) { + @NonNull + public LogResult postLog(@NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log, @Nullable final String logPassword, @NonNull final List<TrackableLog> trackableLogs) { try { final ImmutablePair<StatusCode, String> postResult = GCParser.postLog(cache.getGeocode(), cache.getCacheId(), viewstates, logType, @@ -96,7 +109,7 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } } return new LogResult(postResult.left, postResult.right); - } catch (Exception e) { + } catch (final Exception e) { Log.e("GCLoggingManager.postLog", e); } @@ -104,11 +117,12 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } @Override - public ImageResult postLogImage(String logId, String imageCaption, String imageDescription, Uri imageUri) { + @NonNull + public ImageResult postLogImage(final String logId, final String imageCaption, final String imageDescription, final Uri imageUri) { if (StringUtils.isNotBlank(imageUri.getPath())) { - ImmutablePair<StatusCode, String> imageResult = GCParser.uploadLogImage(logId, imageCaption, imageDescription, imageUri); + final ImmutablePair<StatusCode, String> imageResult = GCParser.uploadLogImage(logId, imageCaption, imageDescription, imageUri); return new ImageResult(imageResult.left, imageResult.right); } @@ -122,6 +136,7 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } @Override + @NonNull public List<TrackableLog> getTrackables() { if (hasLoaderError) { return Collections.emptyList(); @@ -130,6 +145,7 @@ public class GCLoggingManager extends AbstractLoggingManager implements LoaderMa } @Override + @NonNull public List<LogType> getPossibleLogTypes() { if (hasLoaderError) { return Collections.emptyList(); diff --git a/main/src/cgeo/geocaching/connector/gc/GCLogin.java b/main/src/cgeo/geocaching/connector/gc/GCLogin.java index 9f430c0..16de511 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCLogin.java +++ b/main/src/cgeo/geocaching/connector/gc/GCLogin.java @@ -11,6 +11,7 @@ import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; +import cgeo.geocaching.utils.RxUtils; import cgeo.geocaching.utils.TextUtils; import ch.boye.httpclientandroidlib.HttpResponse; @@ -22,47 +23,22 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import rx.Observable; +import rx.functions.Action0; import android.graphics.drawable.Drawable; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; +import java.util.GregorianCalendar; import java.util.Locale; -import java.util.Map; +import java.util.regex.Matcher; public class GCLogin extends AbstractLogin { - private static final String DEFAULT_CUSTOM_DATE_FORMAT = "MM/dd/yyyy"; + private final static String ENGLISH = "<a href=\"#\">English</a>"; - private final static String ENGLISH = "<a href=\"#\">English ▼</a>"; - - private final static Map<String, SimpleDateFormat> GC_CUSTOM_DATE_FORMATS; - public static final String LANGUAGE_CHANGE_URI = "http://www.geocaching.com/my/souvenirs.aspx"; - - static { - final String[] formats = new String[] { - 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" - }; - - final Map<String, SimpleDateFormat> map = new HashMap<>(); - - for (final String format : formats) { - map.put(format, new SimpleDateFormat(format, Locale.ENGLISH)); - } - - GC_CUSTOM_DATE_FORMATS = Collections.unmodifiableMap(map); - } + private static final String LANGUAGE_CHANGE_URI = "http://www.geocaching.com/my/souvenirs.aspx"; private GCLogin() { // singleton @@ -76,8 +52,14 @@ public class GCLogin extends AbstractLogin { private static final GCLogin INSTANCE = new GCLogin(); } + private static StatusCode resetGcCustomDate(final StatusCode statusCode) { + Settings.setGcCustomDate(GCConstants.DEFAULT_GC_DATE); + return statusCode; + } + @Override - protected StatusCode login(boolean retry) { + @NonNull + protected StatusCode login(final boolean retry) { final ImmutablePair<String, String> credentials = Settings.getGcCredentials(); final String username = credentials.left; final String password = credentials.right; @@ -85,7 +67,7 @@ public class GCLogin extends AbstractLogin { if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { clearLoginInfo(); Log.e("Login.login: No login information stored"); - return StatusCode.NO_LOGIN_INFO_STORED; + return resetGcCustomDate(StatusCode.NO_LOGIN_INFO_STORED); } setActualStatus(CgeoApplication.getInstance().getString(R.string.init_login_popup_working)); @@ -105,6 +87,9 @@ public class GCLogin extends AbstractLogin { if (switchToEnglish(loginData) && retry) { return login(false); } + setHomeLocation(); + refreshMemberStatus(); + detectGcCustomDate(); return StatusCode.NO_ERROR; // logged in } @@ -141,17 +126,20 @@ public class GCLogin extends AbstractLogin { } Log.i("Successfully logged in Geocaching.com as " + username + " (" + Settings.getGCMemberStatus() + ')'); Settings.setCookieStore(Cookies.dumpCookieStore()); + setHomeLocation(); + refreshMemberStatus(); + detectGcCustomDate(); return StatusCode.NO_ERROR; // logged in } - if (loginData.contains("Your username/password combination does not match.")) { + if (loginData.contains("your username or password is incorrect")) { Log.i("Failed to log in Geocaching.com as " + username + " because of wrong username/password"); - return StatusCode.WRONG_LOGIN_DATA; // wrong login + return resetGcCustomDate(StatusCode.WRONG_LOGIN_DATA); // wrong login } if (loginData.contains("You must validate your account before you can log in.")) { Log.i("Failed to log in Geocaching.com as " + username + " because account needs to be validated first"); - return StatusCode.UNVALIDATED_ACCOUNT; + return resetGcCustomDate(StatusCode.UNVALIDATED_ACCOUNT); } Log.i("Failed to log in Geocaching.com as " + username + " for some unknown reason"); @@ -160,7 +148,7 @@ public class GCLogin extends AbstractLogin { return login(false); } - return StatusCode.UNKNOWN_ERROR; // can't login + return resetGcCustomDate(StatusCode.UNKNOWN_ERROR); // can't login } public StatusCode logout() { @@ -175,14 +163,16 @@ public class GCLogin extends AbstractLogin { return StatusCode.NO_ERROR; } + private static String removeDotAndComma(final String str) { + return StringUtils.replaceChars(str, ".,", null); + } /** * Check if the user has been logged in when he retrieved the data. * - * @param page * @return <code>true</code> if user is logged in, <code>false</code> otherwise */ - public boolean getLoginStatus(@Nullable final String page) { + boolean getLoginStatus(@Nullable final String page) { if (StringUtils.isBlank(page)) { Log.e("Login.checkLogin: No page given"); return false; @@ -201,15 +191,11 @@ public class GCLogin extends AbstractLogin { setActualUserName(TextUtils.getMatch(page, GCConstants.PATTERN_LOGIN_NAME, true, "???")); int cachesCount = 0; try { - cachesCount = Integer.parseInt(TextUtils.getMatch(page, GCConstants.PATTERN_CACHES_FOUND, true, "0").replaceAll("[,.]", "")); + cachesCount = Integer.parseInt(removeDotAndComma(TextUtils.getMatch(page, GCConstants.PATTERN_CACHES_FOUND, true, "0"))); } catch (final NumberFormatException e) { Log.e("getLoginStatus: bad cache count", e); } setActualCachesFound(cachesCount); - Settings.setGCMemberStatus(TextUtils.getMatch(page, GCConstants.PATTERN_MEMBER_STATUS, true, null)); - if (page.contains(GCConstants.MEMBER_STATUS_RENEW)) { - Settings.setGCMemberStatus(GCConstants.MEMBER_STATUS_PM); - } return true; } @@ -231,7 +217,7 @@ public class GCLogin extends AbstractLogin { * @param previousPage the content of the last loaded page * @return <code>true</code> if a switch was necessary and succesfully performed (non-English -> English) */ - private boolean switchToEnglish(String previousPage) { + private boolean switchToEnglish(final String previousPage) { if (previousPage != null && previousPage.contains(ENGLISH)) { Log.i("Geocaching.com language already set to English"); // get find count @@ -256,21 +242,16 @@ public class GCLogin extends AbstractLogin { return false; } - public Observable<Drawable> downloadAvatarAndGetMemberStatus() { + public Observable<Drawable> downloadAvatar() { try { final String responseData = StringUtils.defaultString(Network.getResponseData(Network.getRequest("http://www.geocaching.com/my/"))); final String profile = TextUtils.replaceWhitespace(responseData); - Settings.setGCMemberStatus(TextUtils.getMatch(profile, GCConstants.PATTERN_MEMBER_STATUS, true, null)); - if (profile.contains(GCConstants.MEMBER_STATUS_RENEW)) { - Settings.setGCMemberStatus(GCConstants.MEMBER_STATUS_PM); - } - - setActualCachesFound(Integer.parseInt(TextUtils.getMatch(profile, GCConstants.PATTERN_CACHES_FOUND, true, "-1").replaceAll("[,.]", ""))); + setActualCachesFound(Integer.parseInt(removeDotAndComma(TextUtils.getMatch(profile, GCConstants.PATTERN_CACHES_FOUND, true, "-1")))); final String avatarURL = TextUtils.getMatch(profile, GCConstants.PATTERN_AVATAR_IMAGE_PROFILE_PAGE, false, null); if (avatarURL != null) { - final HtmlImage imgGetter = new HtmlImage("", false, 0, false); + final HtmlImage imgGetter = new HtmlImage(HtmlImage.SHARED, false, 0, false); return imgGetter.fetchDrawable(avatarURL.replace("avatar", "user/large")).cast(Drawable.class); } // No match? There may be no avatar set by user. @@ -281,12 +262,47 @@ public class GCLogin extends AbstractLogin { return null; } + @Nullable + static String retrieveHomeLocation() { + final String result = Network.getResponseData(Network.getRequest("https://www.geocaching.com/account/settings/homelocation")); + return TextUtils.getMatch(result, GCConstants.PATTERN_HOME_LOCATION, null); + } + + private static void setHomeLocation() { + RxUtils.networkScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + final String homeLocationStr = retrieveHomeLocation(); + if (StringUtils.isNotBlank(homeLocationStr) && !StringUtils.equals(homeLocationStr, Settings.getHomeLocation())) { + assert homeLocationStr != null; + Log.i("Setting home location to " + homeLocationStr); + Settings.setHomeLocation(homeLocationStr); + } + } + }); + } + + private static void refreshMemberStatus() { + RxUtils.networkScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + final String page = StringUtils.defaultString(Network.getResponseData(Network.getRequest("https://www.geocaching.com/account/settings/membership"))); + final Matcher match = GCConstants.PATTERN_MEMBERSHIP.matcher(page); + if (match.find()) { + Log.d("Setting member status to " + match.group(1)); + Settings.setGCMemberStatus(match.group(1)); + } else { + Log.w("Cannot determine member status"); + } + } + }); + } + /** * Detect user date settings on geocaching.com */ - public static void detectGcCustomDate() { - - final String result = Network.getResponseData(Network.getRequest("http://www.geocaching.com/account/ManagePreferences.aspx")); + private static void detectGcCustomDate() { + final String result = Network.getResponseData(Network.getRequest("https://www.geocaching.com/account/settings/preferences")); if (null == result) { Log.w("Login.detectGcCustomDate: result is null"); @@ -300,40 +316,15 @@ public class GCLogin extends AbstractLogin { } public static Date parseGcCustomDate(final String input, final String format) throws ParseException { - if (StringUtils.isBlank(input)) { - throw new ParseException("Input is null", 0); - } - - final String trimmed = input.trim(); - - if (GC_CUSTOM_DATE_FORMATS.containsKey(format)) { - try { - return GC_CUSTOM_DATE_FORMATS.get(format).parse(trimmed); - } catch (final ParseException e) { - } - } - - for (final SimpleDateFormat sdf : GC_CUSTOM_DATE_FORMATS.values()) { - try { - return sdf.parse(trimmed); - } catch (final ParseException e) { - } - } - - throw new ParseException("No matching pattern", 0); + return new SimpleDateFormat(format, Locale.ENGLISH).parse(input.trim()); } - public static Date parseGcCustomDate(final String input) throws ParseException { + static Date parseGcCustomDate(final String input) throws ParseException { return parseGcCustomDate(input, Settings.getGcCustomDate()); } - public static SimpleDateFormat getCustomGcDateFormat() { - final String format = Settings.getGcCustomDate(); - if (GC_CUSTOM_DATE_FORMATS.containsKey(format)) { - return GC_CUSTOM_DATE_FORMATS.get(format); - } - - return GC_CUSTOM_DATE_FORMATS.get(DEFAULT_CUSTOM_DATE_FORMAT); + static String formatGcCustomDate(final int year, final int month, final int day) { + return new SimpleDateFormat(Settings.getGcCustomDate(), Locale.ENGLISH).format(new GregorianCalendar(year, month - 1, day).getTime()); } /** @@ -341,7 +332,7 @@ public class GCLogin extends AbstractLogin { * - Array is null * - or all elements are null or empty strings */ - public static boolean isEmpty(String[] a) { + public static boolean isEmpty(final String[] a) { if (a == null) { return true; } @@ -359,7 +350,7 @@ public class GCLogin extends AbstractLogin { * * @return String[] with all view states */ - public static String[] getViewstates(String page) { + public static String[] getViewstates(final String page) { // Get the number of viewstates. // If there is only one viewstate, __VIEWSTATEFIELDCOUNT is not present @@ -407,7 +398,7 @@ public class GCLogin extends AbstractLogin { /** * put viewstates into request parameters */ - public static void putViewstates(final Parameters params, final String[] viewstates) { + static void putViewstates(final Parameters params, final String[] viewstates) { if (ArrayUtils.isEmpty(viewstates)) { return; } @@ -424,17 +415,15 @@ public class GCLogin extends AbstractLogin { * transfers the viewstates variables from a page (response) to parameters * (next request) */ - public static void transferViewstates(final String page, final Parameters params) { + static void transferViewstates(final String page, final Parameters params) { putViewstates(params, getViewstates(page)); } /** * POST HTTP request. Do the request a second time if the user is not logged in * - * @param uri - * @return */ - public String postRequestLogged(final String uri, final Parameters params) { + String postRequestLogged(final String uri, final Parameters params) { final String data = Network.getResponseData(Network.postRequest(uri, params)); if (getLoginStatus(data)) { @@ -452,12 +441,9 @@ public class GCLogin extends AbstractLogin { /** * GET HTTP request. Do the request a second time if the user is not logged in * - * @param uri - * @param params - * @return */ @Nullable - public String getRequestLogged(@NonNull final String uri, @Nullable final Parameters params) { + String getRequestLogged(@NonNull final String uri, @Nullable final Parameters params) { final HttpResponse response = Network.getRequest(uri, params); final String data = Network.getResponseData(response, canRemoveWhitespace(uri)); @@ -478,8 +464,6 @@ public class GCLogin extends AbstractLogin { * Unfortunately the cache details page contains user generated whitespace in the personal note, therefore we cannot * remove the white space from cache details pages. * - * @param uri - * @return */ private static boolean canRemoveWhitespace(final String uri) { return !StringUtils.contains(uri, "cache_details"); @@ -490,8 +474,8 @@ public class GCLogin extends AbstractLogin { * * @return first is user session, second is session token */ - public @NonNull - MapTokens getMapTokens() { + @NonNull + public MapTokens getMapTokens() { final String data = getRequestLogged(GCConstants.URL_LIVE_MAP, null); final String userSession = TextUtils.getMatch(data, GCConstants.PATTERN_USERSESSION, ""); final String sessionToken = TextUtils.getMatch(data, GCConstants.PATTERN_SESSIONTOKEN, ""); diff --git a/main/src/cgeo/geocaching/connector/gc/GCMap.java b/main/src/cgeo/geocaching/connector/gc/GCMap.java index 27ce06e..243d84c 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCMap.java +++ b/main/src/cgeo/geocaching/connector/gc/GCMap.java @@ -1,35 +1,40 @@ package cgeo.geocaching.connector.gc; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; import cgeo.geocaching.SearchResult; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.enumerations.LiveMapStrategy.Strategy; -import cgeo.geocaching.enumerations.LiveMapStrategy.StrategyFlag; import cgeo.geocaching.enumerations.StatusCode; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter.Format; -import cgeo.geocaching.geopoint.Units; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.files.ParserException; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter.Format; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.location.Viewport; +import cgeo.geocaching.maps.LivemapStrategy; +import cgeo.geocaching.maps.LivemapStrategy.Flag; import cgeo.geocaching.network.Parameters; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Formatter; +import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.LeastRecentlyUsedMap; import cgeo.geocaching.utils.Log; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; +import org.eclipse.jdt.annotation.NonNull; import rx.Observable; import rx.functions.Func2; import android.graphics.Bitmap; +import java.io.IOException; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; @@ -43,7 +48,7 @@ import java.util.Set; public class GCMap { private static Viewport lastSearchViewport = null; - public static SearchResult searchByGeocodes(Set<String> geocodes) { + public static SearchResult searchByGeocodes(final Set<String> geocodes) { final SearchResult result = new SearchResult(); final String geocodeList = StringUtils.join(geocodes.toArray(), "|"); @@ -60,53 +65,41 @@ public class GCMap { // {"name":"HP: Hannover - Sahlkamp","gc":"GC2Q97X","g":"a09149ca-00e0-4aa2-b332-db2b4dfb18d2","available":true,"archived":false,"subrOnly":false,"li":false,"fp":"0","difficulty":{"text":1.0,"value":"1"},"terrain":{"text":1.5,"value":"1_5"},"hidden":"5/29/2011","container":{"text":"Small","value":"small.gif"},"type":{"text":"Traditional Cache","value":2},"owner":{"text":"GeoM@n","value":"1deaa69e-6bcc-421d-95a1-7d32b468cb82"}}] // } - final JSONObject json = new JSONObject(data); - final String status = json.getString("status"); + final ObjectNode json = (ObjectNode) JsonUtils.reader.readTree(data); + final String status = json.path("status").asText(); if (StringUtils.isBlank(status)) { - - throw new JSONException("No status inside JSON"); + throw new ParserException("No status inside JSON"); } if ("success".compareTo(status) != 0) { - throw new JSONException("Wrong status inside JSON"); + throw new ParserException("Wrong status inside JSON"); } - final JSONArray dataArray = json.getJSONArray("data"); + final ArrayNode dataArray = (ArrayNode) json.get("data"); if (dataArray == null) { - throw new JSONException("No data inside JSON"); + throw new ParserException("No data inside JSON"); } final ArrayList<Geocache> caches = new ArrayList<>(); - for (int j = 0; j < dataArray.length(); j++) { + for (final JsonNode dataObject: dataArray) { final Geocache cache = new Geocache(); - - JSONObject dataObject = dataArray.getJSONObject(j); - cache.setName(dataObject.getString("name")); - cache.setGeocode(dataObject.getString("gc")); - cache.setGuid(dataObject.getString("g")); // 34c2e609-5246-4f91-9029-d6c02b0f2a82" - cache.setDisabled(!dataObject.getBoolean("available")); - cache.setArchived(dataObject.getBoolean("archived")); - cache.setPremiumMembersOnly(dataObject.getBoolean("subrOnly")); + cache.setName(dataObject.path("name").asText()); + cache.setGeocode(dataObject.path("gc").asText()); + cache.setGuid(dataObject.path("g").asText()); // 34c2e609-5246-4f91-9029-d6c02b0f2a82" + cache.setDisabled(!dataObject.path("available").asBoolean()); + cache.setArchived(dataObject.path("archived").asBoolean()); + cache.setPremiumMembersOnly(dataObject.path("subrOnly").asBoolean()); // "li" seems to be "false" always - cache.setFavoritePoints(Integer.parseInt(dataObject.getString("fp"))); - JSONObject difficultyObj = dataObject.getJSONObject("difficulty"); - cache.setDifficulty(Float.parseFloat(difficultyObj.getString("text"))); // 3.5 - JSONObject terrainObj = dataObject.getJSONObject("terrain"); - cache.setTerrain(Float.parseFloat(terrainObj.getString("text"))); // 1.5 - cache.setHidden(GCLogin.parseGcCustomDate(dataObject.getString("hidden"), "MM/dd/yyyy")); // 7/23/2001 - JSONObject containerObj = dataObject.getJSONObject("container"); - cache.setSize(CacheSize.getById(containerObj.getString("text"))); // Regular - JSONObject typeObj = dataObject.getJSONObject("type"); - cache.setType(CacheType.getByPattern(typeObj.getString("text"))); // Traditional Cache - JSONObject ownerObj = dataObject.getJSONObject("owner"); - cache.setOwnerDisplayName(ownerObj.getString("text")); + cache.setFavoritePoints(Integer.parseInt(dataObject.path("fp").asText())); + cache.setDifficulty(Float.parseFloat(dataObject.path("difficulty").path("text").asText())); // 3.5 + cache.setTerrain(Float.parseFloat(dataObject.path("terrain").path("text").asText())); // 1.5 + cache.setHidden(GCLogin.parseGcCustomDate(dataObject.path("hidden").asText(), "MM/dd/yyyy")); // 7/23/2001 + cache.setSize(CacheSize.getById(dataObject.path("container").path("text").asText())); // Regular + cache.setType(CacheType.getByPattern(dataObject.path("type").path("text").asText())); // Traditional Cache + cache.setOwnerDisplayName(dataObject.path("owner").path("text").asText()); caches.add(cache); } result.addAndPutInCache(caches); - } catch (JSONException e) { - result.setError(StatusCode.UNKNOWN_ERROR); - } catch (ParseException e) { - result.setError(StatusCode.UNKNOWN_ERROR); - } catch (NumberFormatException e) { + } catch (ParserException | ParseException | IOException | NumberFormatException ignored) { result.setError(StatusCode.UNKNOWN_ERROR); } return result; @@ -117,7 +110,7 @@ public class GCMap { * Retrieved data. * @return SearchResult. Never null. */ - public static SearchResult parseMapJSON(final String data, Tile tile, Bitmap bitmap, final Strategy strategy) { + public static SearchResult parseMapJSON(final String data, final Tile tile, final Bitmap bitmap, final LivemapStrategy strategy) { final SearchResult searchResult = new SearchResult(); try { @@ -125,7 +118,7 @@ public class GCMap { final LeastRecentlyUsedMap<String, String> nameCache = new LeastRecentlyUsedMap.LruCache<>(2000); // JSON id, cache name if (StringUtils.isEmpty(data)) { - throw new JSONException("No page given"); + throw new ParserException("No page given"); } // Example JSON information @@ -134,34 +127,33 @@ public class GCMap { // "data":{"55_55":[{"i":"gEaR","n":"Spiel & Sport"}],"55_54":[{"i":"gEaR","n":"Spiel & Sport"}],"17_25":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"55_53":[{"i":"gEaR","n":"Spiel & Sport"}],"17_27":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"17_26":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"57_53":[{"i":"gEaR","n":"Spiel & Sport"}],"57_55":[{"i":"gEaR","n":"Spiel & Sport"}],"3_62":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"3_61":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"57_54":[{"i":"gEaR","n":"Spiel & Sport"}],"3_60":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"15_27":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"15_26":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"15_25":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"4_60":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"4_61":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"4_62":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"16_25":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"16_26":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"16_27":[{"i":"Rkzt","n":"EDSSW: Rathaus "}],"2_62":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"2_60":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"2_61":[{"i":"gOWz","n":"Baumarktserie - Wer Wo Was -"}],"56_53":[{"i":"gEaR","n":"Spiel & Sport"}],"56_54":[{"i":"gEaR","n":"Spiel & Sport"}],"56_55":[{"i":"gEaR","n":"Spiel & Sport"}]} // } - final JSONObject json = new JSONObject(data); + final ObjectNode json = (ObjectNode) JsonUtils.reader.readTree(data); - final JSONArray grid = json.getJSONArray("grid"); - if (grid == null || grid.length() != (UTFGrid.GRID_MAXY + 1)) { - throw new JSONException("No grid inside JSON"); + final ArrayNode grid = (ArrayNode) json.get("grid"); + if (grid == null || grid.size() != (UTFGrid.GRID_MAXY + 1)) { + throw new ParserException("No grid inside JSON"); } - final JSONArray keys = json.getJSONArray("keys"); + final ArrayNode keys = (ArrayNode) json.get("keys"); if (keys == null) { - throw new JSONException("No keys inside JSON"); + throw new ParserException("No keys inside JSON"); } - final JSONObject dataObject = json.getJSONObject("data"); + final ObjectNode dataObject = (ObjectNode) json.get("data"); if (dataObject == null) { - throw new JSONException("No data inside JSON"); + throw new ParserException("No data inside JSON"); } // iterate over the data and construct all caches in this tile - Map<String, List<UTFGridPosition>> positions = new HashMap<>(); // JSON id as key - Map<String, List<UTFGridPosition>> singlePositions = new HashMap<>(); // JSON id as key - - for (int i = 1; i < keys.length(); i++) { // index 0 is empty - String key = keys.getString(i); - if (StringUtils.isNotBlank(key)) { - UTFGridPosition pos = UTFGridPosition.fromString(key); - JSONArray dataForKey = dataObject.getJSONArray(key); - for (int j = 0; j < dataForKey.length(); j++) { - JSONObject cacheInfo = dataForKey.getJSONObject(j); - String id = cacheInfo.getString("i"); - nameCache.put(id, cacheInfo.getString("n")); + final Map<String, List<UTFGridPosition>> positions = new HashMap<>(); // JSON id as key + final Map<String, List<UTFGridPosition>> singlePositions = new HashMap<>(); // JSON id as key + + for (final JsonNode rawKey: keys) { + final String key = rawKey.asText(); + if (StringUtils.isNotBlank(key)) { // index 0 is empty + final UTFGridPosition pos = UTFGridPosition.fromString(key); + final ArrayNode dataForKey = (ArrayNode) dataObject.get(key); + for (final JsonNode cacheInfo: dataForKey) { + final String id = cacheInfo.get("i").asText(); + nameCache.put(id, cacheInfo.get("n").asText()); List<UTFGridPosition> listOfPositions = positions.get(id); List<UTFGridPosition> singleListOfPositions = singlePositions.get(id); @@ -174,7 +166,7 @@ public class GCMap { } listOfPositions.add(pos); - if (dataForKey.length() == 1) { + if (dataForKey.size() == 1) { singleListOfPositions.add(pos); } @@ -183,18 +175,18 @@ public class GCMap { } final ArrayList<Geocache> caches = new ArrayList<>(); - for (Entry<String, List<UTFGridPosition>> entry : positions.entrySet()) { - String id = entry.getKey(); - List<UTFGridPosition> pos = entry.getValue(); - UTFGridPosition xy = UTFGrid.getPositionInGrid(pos); - Geocache cache = new Geocache(); + for (final Entry<String, List<UTFGridPosition>> entry : positions.entrySet()) { + final String id = entry.getKey(); + final List<UTFGridPosition> pos = entry.getValue(); + final UTFGridPosition xy = UTFGrid.getPositionInGrid(pos); + final Geocache cache = new Geocache(); cache.setDetailed(false); cache.setReliableLatLon(false); cache.setGeocode(id); cache.setName(nameCache.get(id)); cache.setCoords(tile.getCoord(xy), tile.getZoomLevel()); - if (strategy.flags.contains(StrategyFlag.PARSE_TILES) && bitmap != null) { - for (UTFGridPosition singlePos : singlePositions.get(id)) { + if (strategy.flags.contains(LivemapStrategy.Flag.PARSE_TILES) && bitmap != null) { + for (final UTFGridPosition singlePos : singlePositions.get(id)) { if (IconDecoder.parseMapPNG(cache, bitmap, singlePos, tile.getZoomLevel())) { break; // cache parsed } @@ -220,9 +212,7 @@ public class GCMap { searchResult.addAndPutInCache(caches); Log.d("Retrieved " + searchResult.getCount() + " caches for tile " + tile.toString()); - } catch (RuntimeException e) { - Log.e("GCMap.parseMapJSON", e); - } catch (JSONException e) { + } catch (RuntimeException | ParserException | IOException e) { Log.e("GCMap.parseMapJSON", e); } @@ -236,19 +226,19 @@ public class GCMap { * Area to search * @param tokens * Live map tokens - * @return */ + @NonNull public static SearchResult searchByViewport(final Viewport viewport, final MapTokens tokens) { - int speed = (int) CgeoApplication.getInstance().currentGeo().getSpeed() * 60 * 60 / 1000; // in km/h - Strategy strategy = Settings.getLiveMapStrategy(); - if (strategy == Strategy.AUTO) { - strategy = speed >= 30 ? Strategy.FAST : Strategy.DETAILED; + final int speed = (int) Sensors.getInstance().currentGeo().getSpeed() * 60 * 60 / 1000; // in km/h + LivemapStrategy strategy = Settings.getLiveMapStrategy(); + if (strategy == LivemapStrategy.AUTO) { + strategy = speed >= 30 ? LivemapStrategy.FAST : LivemapStrategy.DETAILED; } - SearchResult result = searchByViewport(viewport, tokens, strategy); + final SearchResult result = searchByViewport(viewport, tokens, strategy); if (Settings.isDebug()) { - StringBuilder text = new StringBuilder(Formatter.SEPARATOR).append(strategy.getL10n()).append(Formatter.SEPARATOR).append(Units.getSpeed(speed)); + final StringBuilder text = new StringBuilder(Formatter.SEPARATOR).append(strategy.getL10n()).append(Formatter.SEPARATOR).append(Units.getSpeed(speed)); result.setUrl(result.getUrl() + text); } @@ -266,9 +256,9 @@ public class GCMap { * Live map tokens * @param strategy * Strategy for data retrieval and parsing, @see Strategy - * @return */ - private static SearchResult searchByViewport(final Viewport viewport, final MapTokens tokens, final Strategy strategy) { + @NonNull + private static SearchResult searchByViewport(final Viewport viewport, final MapTokens tokens, final LivemapStrategy strategy) { Log.d("GCMap.searchByViewport" + viewport.toString()); final SearchResult searchResult = new SearchResult(); @@ -277,7 +267,7 @@ public class GCMap { searchResult.setUrl(viewport.getCenter().format(Format.LAT_LON_DECMINUTE)); } - if (strategy.flags.contains(StrategyFlag.LOAD_TILES)) { + if (strategy.flags.contains(LivemapStrategy.Flag.LOAD_TILES)) { final Set<Tile> tiles = Tile.getTilesForViewport(viewport); if (Settings.isDebug()) { @@ -347,11 +337,11 @@ public class GCMap { } } - if (strategy.flags.contains(StrategyFlag.SEARCH_NEARBY) && Settings.isGCPremiumMember()) { + if (strategy.flags.contains(Flag.SEARCH_NEARBY) && Settings.isGCPremiumMember()) { final Geopoint center = viewport.getCenter(); if ((lastSearchViewport == null) || !lastSearchViewport.contains(center)) { //FIXME We don't have a RecaptchaReceiver!? - SearchResult search = GCParser.searchByCoords(center, Settings.getCacheType(), false, null); + final SearchResult search = GCParser.searchByCoords(center, Settings.getCacheType(), false, null); if (search != null && !search.isEmpty()) { final Set<String> geocodes = search.getGeocodes(); lastSearchViewport = DataStore.getBounds(geocodes); @@ -365,11 +355,10 @@ public class GCMap { /** * Creates a list of caches types to filter on the live map (exclusion string) - * + * * @param typeToDisplay * - cache type to omit from exclusion list so it gets displayed - * @return - * + * * cache types for live map filter: * 2 = traditional, 9 = ape, 5 = letterbox * 3 = multi @@ -377,8 +366,8 @@ public class GCMap { * 4 = virtual, 11 = webcam, 137 = earth * 8 = mystery, 1858 = whereigo */ - private static String getCacheTypeFilter(CacheType typeToDisplay) { - Set<String> filterTypes = new HashSet<>(); + private static String getCacheTypeFilter(final CacheType typeToDisplay) { + final Set<String> filterTypes = new HashSet<>(); // Put all types in set, remove what should be visible in a second step filterTypes.addAll(Arrays.asList("2", "9", "5", "3", "6", "453", "13", "1304", "4", "11", "137", "8", "1858")); switch (typeToDisplay) { diff --git a/main/src/cgeo/geocaching/connector/gc/GCParser.java b/main/src/cgeo/geocaching/connector/gc/GCParser.java index c771049..7fef8b4 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCParser.java +++ b/main/src/cgeo/geocaching/connector/gc/GCParser.java @@ -22,14 +22,15 @@ import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.files.LocParser; import cgeo.geocaching.gcvote.GCVote; import cgeo.geocaching.gcvote.GCVoteRating; -import cgeo.geocaching.geopoint.DistanceParser; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.location.DistanceParser; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.network.Network; 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; import cgeo.geocaching.utils.RxUtils; @@ -38,15 +39,16 @@ import cgeo.geocaching.utils.TextUtils; import ch.boye.httpclientandroidlib.HttpResponse; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import rx.Observable; import rx.Observable.OnSubscribe; @@ -54,11 +56,15 @@ import rx.Subscriber; import rx.functions.Action1; import rx.functions.Func0; import rx.functions.Func2; +import rx.schedulers.Schedulers; import android.net.Uri; import android.text.Html; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.text.Collator; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; @@ -66,16 +72,22 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumSet; -import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; public abstract class GCParser { - private final static SynchronizedDateFormat dateTbIn1 = new SynchronizedDateFormat("EEEEE, dd MMMMM yyyy", Locale.ENGLISH); // Saturday, 28 March 2009 - private final static SynchronizedDateFormat dateTbIn2 = new SynchronizedDateFormat("EEEEE, MMMMM dd, yyyy", Locale.ENGLISH); // Saturday, March 28, 2009 + @NonNull + private final static SynchronizedDateFormat DATE_TB_IN_1 = new SynchronizedDateFormat("EEEEE, dd MMMMM yyyy", Locale.ENGLISH); // Saturday, 28 March 2009 + + @NonNull + private final static SynchronizedDateFormat DATE_TB_IN_2 = new SynchronizedDateFormat("EEEEE, MMMMM dd, yyyy", Locale.ENGLISH); // Saturday, March 28, 2009 + @NonNull + private final static ImmutablePair<StatusCode, Geocache> UNKNOWN_PARSE_ERROR = ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); + + @Nullable private static SearchResult parseSearch(final String url, final String pageContent, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(pageContent)) { Log.e("GCParser.parseSearch: No page given"); @@ -125,12 +137,12 @@ public abstract class GCParser { page = page.substring(startPos + 1, endPos - startPos + 1); // cut between <table> and </table> - final String[] rows = page.split("<tr class="); - final int rows_count = rows.length; + final String[] rows = StringUtils.splitByWholeSeparator(page, "<tr class="); + final int rowsCount = rows.length; int excludedCaches = 0; final ArrayList<Geocache> caches = new ArrayList<>(); - for (int z = 1; z < rows_count; z++) { + for (int z = 1; z < rowsCount; z++) { final Geocache cache = new Geocache(); final String row = rows[z]; @@ -161,7 +173,7 @@ public abstract class GCParser { } } catch (final RuntimeException e) { // failed to parse GUID and/or Disabled - Log.w("GCParser.parseSearch: Failed to parse GUID and/or Disabled data"); + Log.w("GCParser.parseSearch: Failed to parse GUID and/or Disabled data", e); } if (Settings.isExcludeDisabledCaches() && (cache.isDisabled() || cache.isArchived())) { @@ -173,11 +185,11 @@ public abstract class GCParser { cache.setGeocode(TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_GEOCODE, true, 1, cache.getGeocode(), true)); // cache type - cache.setType(CacheType.getByPattern(TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_TYPE, true, 1, null, true))); + cache.setType(CacheType.getByPattern(TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_TYPE, null))); // cache direction - image if (Settings.getLoadDirImg()) { - final String direction = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, 1, null, false); + final String direction = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, null); if (direction != null) { cache.setDirectionImg(direction); } @@ -187,7 +199,7 @@ public abstract class GCParser { final String distance = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, 2, null, false); if (distance != null) { cache.setDistance(DistanceParser.parseDistance(distance, - !Settings.isUseImperialUnits())); + !Settings.useImperialUnits())); } // difficulty/terrain @@ -204,19 +216,19 @@ public abstract class GCParser { } // size - final String container = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_CONTAINER, false, 1, null, false); + final String container = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_CONTAINER, false, null); cache.setSize(CacheSize.getById(container)); // date hidden, makes sorting event caches easier - final String dateHidden = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_HIDDEN_DATE, false, 1, null, false); + final String dateHidden = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_HIDDEN_DATE, false, null); if (StringUtils.isNotBlank(dateHidden)) { try { - Date date = GCLogin.parseGcCustomDate(dateHidden); + final Date date = GCLogin.parseGcCustomDate(dateHidden); if (date != null) { cache.setHidden(date); } - } catch (ParseException e) { - Log.e("Error parsing event date from search"); + } catch (final ParseException e) { + Log.e("Error parsing event date from search", e); } } @@ -235,6 +247,7 @@ public abstract class GCParser { } if (StringUtils.isNotBlank(inventoryPre)) { + assert inventoryPre != null; final MatcherWrapper matcherTbsInside = new MatcherWrapper(GCConstants.PATTERN_SEARCH_TRACKABLESINSIDE, inventoryPre); while (matcherTbsInside.find()) { if (matcherTbsInside.groupCount() == 2 && @@ -266,7 +279,7 @@ public abstract class GCParser { cache.setFavoritePoints(Integer.parseInt(result)); } } catch (final NumberFormatException e) { - Log.w("GCParser.parseSearch: Failed to parse favorite count"); + Log.w("GCParser.parseSearch: Failed to parse favorite count", e); } caches.add(cache); @@ -280,7 +293,7 @@ public abstract class GCParser { searchResult.setTotalCountGC(Integer.parseInt(result) - excludedCaches); } } catch (final NumberFormatException e) { - Log.w("GCParser.parseSearch: Failed to parse cache count"); + Log.w("GCParser.parseSearch: Failed to parse cache count", e); } String recaptchaText = null; @@ -291,6 +304,13 @@ public abstract class GCParser { if (!cids.isEmpty() && (Settings.isGCPremiumMember() || showCaptcha) && ((recaptchaReceiver == null || StringUtils.isBlank(recaptchaReceiver.getChallenge())) || StringUtils.isNotBlank(recaptchaText))) { Log.i("Trying to get .loc for " + cids.size() + " caches"); + final Observable<Set<Geocache>> storedCaches = Observable.defer(new Func0<Observable<Set<Geocache>>>() { + @Override + public Observable<Set<Geocache>> call() { + return Observable.just(DataStore.loadCaches(Geocache.getGeocodes(caches), LoadFlags.LOAD_CACHE_OR_DB)); + } + }).subscribeOn(Schedulers.io()).cache(); + storedCaches.subscribe(); // Force asynchronous start of database loading try { // get coordinates for parsed caches @@ -306,41 +326,41 @@ public abstract class GCParser { params.put("recaptcha_challenge_field", recaptchaReceiver.getChallenge()); params.put("recaptcha_response_field", recaptchaText); } - params.put("ctl00$ContentBody$uxDownloadLoc", "Download Waypoints"); + params.put("Download", "Download Waypoints"); - final String coordinates = Network.getResponseData(Network.postRequest("http://www.geocaching.com/seek/nearest.aspx", params), false); + // retrieve target url + final String queryUrl = TextUtils.getMatch(pageContent, GCConstants.PATTERN_SEARCH_POST_ACTION, ""); - if (StringUtils.contains(coordinates, "You have not agreed to the license agreement. The license agreement is required before you can start downloading GPX or LOC files from Geocaching.com")) { - Log.i("User has not agreed to the license agreement. Can\'t download .loc file."); - searchResult.setError(StatusCode.UNAPPROVED_LICENSE); - return searchResult; - } + if (StringUtils.isEmpty(queryUrl)) { + Log.w("Loc download url not found"); + } else { - LocParser.parseLoc(searchResult, coordinates); + final String coordinates = Network.getResponseData(Network.postRequest("http://www.geocaching.com/seek/" + queryUrl, params), false); - } catch (final RuntimeException e) { - Log.e("GCParser.parseSearch.CIDs", e); - } - } + if (StringUtils.contains(coordinates, "You have not agreed to the license agreement. The license agreement is required before you can start downloading GPX or LOC files from Geocaching.com")) { + Log.i("User has not agreed to the license agreement. Can\'t download .loc file."); + searchResult.setError(StatusCode.UNAPPROVED_LICENSE); + return searchResult; + } - // get direction images - if (Settings.getLoadDirImg()) { - final Set<Geocache> cachesReloaded = searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB); - for (final Geocache cache : cachesReloaded) { - if (cache.getCoords() == null && StringUtils.isNotEmpty(cache.getDirectionImg())) { - DirectionImage.getDrawable(cache.getDirectionImg()); + LocParser.parseLoc(searchResult, coordinates, storedCaches.toBlocking().single()); } + + } catch (final RuntimeException e) { + Log.e("GCParser.parseSearch.CIDs", e); } } return searchResult; } + @Nullable private static Float parseStars(final String value) { final float floatValue = Float.parseFloat(StringUtils.replaceChars(value, ',', '.')); return floatValue >= 0.5 && floatValue <= 5.0 ? floatValue : null; } + @Nullable static SearchResult parseCache(final String page, final CancellableHandler handler) { final ImmutablePair<StatusCode, Geocache> parsed = parseCacheFromText(page, handler); // attention: parseCacheFromText already stores implicitly through searchResult.addCache @@ -366,12 +386,13 @@ public abstract class GCParser { return new SearchResult(cache); } + @NonNull static SearchResult parseAndSaveCacheFromText(final String page, @Nullable final CancellableHandler handler) { final ImmutablePair<StatusCode, Geocache> parsed = parseCacheFromText(page, handler); final SearchResult result = new SearchResult(parsed.left); if (parsed.left == StatusCode.NO_ERROR) { result.addAndPutInCache(Collections.singletonList(parsed.right)); - DataStore.saveLogsWithoutTransaction(parsed.right.getGeocode(), getLogs(page, Logs.ALL).toBlocking().toIterable()); + DataStore.saveLogs(parsed.right.getGeocode(), getLogs(parseUserToken(page), Logs.ALL).toBlocking().toIterable()); } return result; } @@ -380,17 +401,20 @@ public abstract class GCParser { * Parse cache from text and return either an error code or a cache object in a pair. Note that inline logs are * not parsed nor saved, while the cache itself is. * - * @param pageIn the page text to parse - * @param handler the handler to send the progress notifications to - * @return a pair, with a {@link StatusCode} on the left, and a non-nulll cache objet on the right - * iff the status code is {@link StatusCode.NO_ERROR}. + * @param pageIn + * the page text to parse + * @param handler + * the handler to send the progress notifications to + * @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 cgeo.geocaching.enumerations.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)) { @@ -403,12 +427,12 @@ 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 String personalNoteWithLineBreaks = ""; - MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_PERSONALNOTE, pageIn); + final MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_PERSONALNOTE, pageIn); if (matcher.find()) { personalNoteWithLineBreaks = matcher.group(1).trim(); } @@ -446,7 +470,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); @@ -490,7 +514,7 @@ public abstract class GCParser { } } catch (final ParseException e) { // failed to parse cache hidden date - Log.w("GCParser.parseCache: Failed to parse cache hidden (event) date"); + Log.w("GCParser.parseCache: Failed to parse cache hidden (event) date", e); } // favorite @@ -547,17 +571,17 @@ public abstract class GCParser { final String longDescription = TextUtils.getMatch(page, GCConstants.PATTERN_DESC, true, ""); String relatedWebPage = TextUtils.getMatch(page, GCConstants.PATTERN_RELATED_WEB_PAGE, true, ""); if (StringUtils.isNotEmpty(relatedWebPage)) { - relatedWebPage = String.format("<a href=\"%s\"><b>%s</b></a><br/><br/>", relatedWebPage, relatedWebPage); + relatedWebPage = String.format("<br/><br/><a href=\"%s\"><b>%s</b></a>", relatedWebPage, relatedWebPage); } - cache.setDescription(relatedWebPage + longDescription); + cache.setDescription(longDescription + relatedWebPage); // cache attributes try { + final ArrayList<String> attributes = new ArrayList<>(); final String attributesPre = TextUtils.getMatch(page, GCConstants.PATTERN_ATTRIBUTES, true, null); - if (null != attributesPre) { + if (attributesPre != null) { final MatcherWrapper matcherAttributesInside = new MatcherWrapper(GCConstants.PATTERN_ATTRIBUTESINSIDE, attributesPre); - final ArrayList<String> attributes = new ArrayList<>(); while (matcherAttributesInside.find()) { if (matcherAttributesInside.groupCount() > 1 && !matcherAttributesInside.group(2).equalsIgnoreCase("blank")) { // by default, use the tooltip of the attribute @@ -575,17 +599,17 @@ public abstract class GCParser { attributes.add(attribute); } } - cache.setAttributes(attributes); } + cache.setAttributes(attributes); } catch (final RuntimeException e) { // failed to parse cache attributes - Log.w("GCParser.parseCache: Failed to parse cache attributes"); + Log.w("GCParser.parseCache: Failed to parse cache attributes", e); } // cache spoilers try { if (CancellableHandler.isCancelled(handler)) { - return null; + return UNKNOWN_PARSE_ERROR; } CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_spoilers); @@ -608,7 +632,7 @@ public abstract class GCParser { } } catch (final RuntimeException e) { // failed to parse cache spoilers - Log.w("GCParser.parseCache: Failed to parse cache spoilers"); + Log.w("GCParser.parseCache: Failed to parse cache spoilers", e); } // cache inventory @@ -642,7 +666,7 @@ public abstract class GCParser { } } catch (final RuntimeException e) { // failed to parse cache inventory - Log.w("GCParser.parseCache: Failed to parse cache inventory (2)"); + Log.w("GCParser.parseCache: Failed to parse cache inventory (2)", e); } // cache logs counts @@ -664,7 +688,7 @@ public abstract class GCParser { } } catch (final NumberFormatException e) { // failed to parse logs - Log.w("GCParser.parseCache: Failed to parse cache log count"); + Log.w("GCParser.parseCache: Failed to parse cache log count", e); } // waypoints - reset collection @@ -680,13 +704,13 @@ public abstract class GCParser { cache.addOrChangeWaypoint(waypoint, false); cache.setUserModifiedCoords(true); } - } catch (final Geopoint.GeopointException e) { + } catch (final Geopoint.GeopointException ignored) { } 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); @@ -707,10 +731,15 @@ public abstract class GCParser { wpList = wpList.substring(wpBegin + 7, wpEnd); } - final String[] wpItems = wpList.split("<tr"); + final String[] wpItems = StringUtils.splitByWholeSeparator(wpList, "<tr"); - for (int j = 1; j < wpItems.length; j++) { - String[] wp = wpItems[j].split("<td"); + for (int j = 1; j < wpItems.length; j += 2) { + final String[] wp = StringUtils.splitByWholeSeparator(wpItems[j], "<td"); + assert wp != null; + if (wp.length < 8) { + Log.e("GCParser.cacheParseFromText: not enough waypoint columns in table"); + continue; + } // waypoint name // res is null during the unit tests @@ -730,39 +759,42 @@ 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)); } - j++; - if (wpItems.length > j) { - wp = wpItems[j].split("<td"); - } + if (wpItems.length >= j) { + final String[] wpNote = StringUtils.splitByWholeSeparator(wpItems[j + 1], "<td"); + assert wpNote != null; + if (wpNote.length < 4) { + Log.d("GCParser.cacheParseFromText: not enough waypoint columns in table to extract note"); + continue; + } - // waypoint note - waypoint.setNote(TextUtils.getMatch(wp[3], GCConstants.PATTERN_WPNOTE, waypoint.getNote())); + // waypoint note + waypoint.setNote(TextUtils.getMatch(wpNote[3], GCConstants.PATTERN_WPNOTE, waypoint.getNote())); + } cache.addOrChangeWaypoint(waypoint, false); } } } - cache.parseWaypointsFromNote(); - // last check for necessary cache conditions if (StringUtils.isBlank(cache.getGeocode())) { - return ImmutablePair.of(StatusCode.UNKNOWN_ERROR, null); + return UNKNOWN_PARSE_ERROR; } cache.setDetailedUpdatedNow(); return ImmutablePair.of(StatusCode.NO_ERROR, cache); } + @Nullable private static String getNumberString(final String numberWithPunctuation) { return StringUtils.replaceChars(numberWithPunctuation, ".,", ""); } - public static SearchResult searchByNextPage(final SearchResult search, boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { + @Nullable + public static SearchResult searchByNextPage(final SearchResult search, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (search == null) { return null; } @@ -817,14 +849,12 @@ public abstract class GCParser { /** * Possibly hide caches found or hidden by user. This mutates its params argument when possible. * - * @param params - * the parameters to mutate, or null to create a new Parameters if needed - * @param my - * @param addF + * @param params the parameters to mutate, or null to create a new Parameters if needed + * @param my {@code true} if the user's caches must be forcibly included regardless of their settings * @return the original params if not null, maybe augmented with f=1, or a new Parameters with f=1 or null otherwise */ - private static Parameters addFToParams(final Parameters params, final boolean my, final boolean addF) { - if (!my && Settings.isExcludeMyCaches() && addF) { + private static Parameters addFToParams(final Parameters params, final boolean my) { + if (!my && Settings.isExcludeMyCaches()) { if (params == null) { return new Parameters("f", "1"); } @@ -835,22 +865,14 @@ public abstract class GCParser { return params; } - /** - * @param cacheType - * @param listId - * @param showCaptcha - * @param params - * the parameters to add to the request URI - * @param recaptchaReceiver - * @return - */ @Nullable - private static SearchResult searchByAny(final CacheType cacheType, final boolean my, final boolean showCaptcha, final Parameters params, RecaptchaReceiver recaptchaReceiver) { + private static SearchResult searchByAny(@NonNull final CacheType cacheType, final boolean my, final boolean showCaptcha, final Parameters params, final RecaptchaReceiver recaptchaReceiver) { insertCacheType(params, cacheType); final String uri = "http://www.geocaching.com/seek/nearest.aspx"; - final String fullUri = uri + "?" + addFToParams(params, my, true); - final String page = GCLogin.getInstance().getRequestLogged(uri, addFToParams(params, my, true)); + final Parameters paramsWithF = addFToParams(params, my); + final String fullUri = uri + "?" + paramsWithF; + final String page = GCLogin.getInstance().getRequestLogged(uri, paramsWithF); if (StringUtils.isBlank(page)) { Log.e("GCParser.searchByAny: No data from server"); @@ -871,12 +893,12 @@ public abstract class GCParser { return search; } - public static SearchResult searchByCoords(final @NonNull Geopoint coords, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { + public static SearchResult searchByCoords(final @NonNull Geopoint coords, @NonNull final CacheType cacheType, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { final Parameters params = new Parameters("lat", Double.toString(coords.getLatitude()), "lng", Double.toString(coords.getLongitude())); return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver); } - public static SearchResult searchByKeyword(final @NonNull String keyword, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { + public static SearchResult searchByKeyword(final @NonNull String keyword, @NonNull final CacheType cacheType, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(keyword)) { Log.e("GCParser.searchByKeyword: No keyword given"); return null; @@ -894,7 +916,7 @@ public abstract class GCParser { return false; } - public static SearchResult searchByUsername(final String userName, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { + public static SearchResult searchByUsername(final String userName, @NonNull final CacheType cacheType, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(userName)) { Log.e("GCParser.searchByUsername: No user name given"); return null; @@ -905,7 +927,7 @@ public abstract class GCParser { return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, recaptchaReceiver); } - public static SearchResult searchByPocketQuery(final String pocketGuid, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { + public static SearchResult searchByPocketQuery(final String pocketGuid, @NonNull final CacheType cacheType, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(pocketGuid)) { Log.e("GCParser.searchByPocket: No guid name given"); return null; @@ -916,7 +938,7 @@ public abstract class GCParser { return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver); } - public static SearchResult searchByOwner(final String userName, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { + public static SearchResult searchByOwner(final String userName, @NonNull final CacheType cacheType, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) { if (StringUtils.isBlank(userName)) { Log.e("GCParser.searchByOwner: No user name given"); return null; @@ -926,34 +948,6 @@ public abstract class GCParser { return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, recaptchaReceiver); } - public static SearchResult searchByAddress(final String address, final CacheType cacheType, final boolean showCaptcha, RecaptchaReceiver recaptchaReceiver) { - if (StringUtils.isBlank(address)) { - Log.e("GCParser.searchByAddress: No address given"); - return null; - } - try { - final JSONObject response = Network.requestJSON("http://www.geocaching.com/api/geocode", new Parameters("q", address)); - if (response == null) { - return null; - } - if (!StringUtils.equalsIgnoreCase(response.getString("status"), "success")) { - return null; - } - if (!response.has("data")) { - return null; - } - final JSONObject data = response.getJSONObject("data"); - if (data == null) { - return null; - } - return searchByCoords(new Geopoint(data.getDouble("lat"), data.getDouble("lng")), cacheType, showCaptcha, recaptchaReceiver); - } catch (final JSONException e) { - Log.w("GCParser.searchByAddress", e); - } - - return null; - } - @Nullable public static Trackable searchTrackable(final String geocode, final String guid, final String id) { if (StringUtils.isBlank(geocode) && StringUtils.isBlank(guid) && StringUtils.isBlank(id)) { @@ -990,52 +984,59 @@ public abstract class GCParser { return trackable; } - public static List<PocketQueryList> searchPocketQueryList() { - - final Parameters params = new Parameters(); + /** + * Observable that fetches a list of pocket queries. Returns a single element (which may be an empty list). + * Executes on the network scheduler. + */ + public static final Observable<List<PocketQueryList>> searchPocketQueryListObservable = Observable.defer(new Func0<Observable<List<PocketQueryList>>>() { + @Override + public Observable<List<PocketQueryList>> call() { + final Parameters params = new Parameters(); - final String page = GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/pocket/default.aspx", params); + final String page = GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/pocket/default.aspx", params); - if (StringUtils.isBlank(page)) { - Log.e("GCParser.searchPocketQueryList: No data from server"); - return null; - } + if (StringUtils.isBlank(page)) { + Log.e("GCParser.searchPocketQueryList: No data from server"); + return Observable.just(Collections.<PocketQueryList>emptyList()); + } - String subPage = StringUtils.substringAfter(page, "class=\"PocketQueryListTable"); - if (StringUtils.isEmpty(subPage)) { - Log.e("GCParser.searchPocketQueryList: class \"PocketQueryListTable\" not found on page"); - return Collections.emptyList(); - } + final String subPage = StringUtils.substringAfter(page, "class=\"PocketQueryListTable"); + if (StringUtils.isEmpty(subPage)) { + Log.e("GCParser.searchPocketQueryList: class \"PocketQueryListTable\" not found on page"); + return Observable.just(Collections.<PocketQueryList>emptyList()); + } - List<PocketQueryList> list = new ArrayList<>(); + final List<PocketQueryList> list = new ArrayList<>(); - final MatcherWrapper matcherPocket = new MatcherWrapper(GCConstants.PATTERN_LIST_PQ, subPage); + final MatcherWrapper matcherPocket = new MatcherWrapper(GCConstants.PATTERN_LIST_PQ, subPage); - while (matcherPocket.find()) { - int maxCaches; - try { - maxCaches = Integer.parseInt(matcherPocket.group(1)); - } catch (NumberFormatException e) { - maxCaches = 0; - Log.e("GCParser.searchPocketQueryList: Unable to parse max caches", e); + while (matcherPocket.find()) { + int maxCaches; + try { + maxCaches = Integer.parseInt(matcherPocket.group(1)); + } catch (final NumberFormatException e) { + maxCaches = 0; + Log.e("GCParser.searchPocketQueryList: Unable to parse max caches", e); + } + final String guid = Html.fromHtml(matcherPocket.group(2)).toString(); + final String name = Html.fromHtml(matcherPocket.group(3)).toString(); + final PocketQueryList pqList = new PocketQueryList(guid, name, maxCaches); + list.add(pqList); } - final String guid = Html.fromHtml(matcherPocket.group(2)).toString(); - final String name = Html.fromHtml(matcherPocket.group(3)).toString(); - final PocketQueryList pqList = new PocketQueryList(guid, name, maxCaches); - list.add(pqList); - } - // just in case, lets sort the resulting list - Collections.sort(list, new Comparator<PocketQueryList>() { + // just in case, lets sort the resulting list + final Collator collator = TextUtils.getCollator(); + Collections.sort(list, new Comparator<PocketQueryList>() { - @Override - public int compare(PocketQueryList left, PocketQueryList right) { - return String.CASE_INSENSITIVE_ORDER.compare(left.getName(), right.getName()); - } - }); + @Override + public int compare(final PocketQueryList left, final PocketQueryList right) { + return collator.compare(left.getName(), right.getName()); + } + }); - return list; - } + return Observable.just(list); + } + }).subscribeOn(RxUtils.networkScheduler); public static ImmutablePair<StatusCode, String> postLog(final String geocode, final String cacheid, final String[] viewstates, final LogType logType, final int year, final int month, final int day, @@ -1061,7 +1062,7 @@ public abstract class GCParser { "__EVENTARGUMENT", "", "__LASTFOCUS", "", "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id), - "ctl00$ContentBody$LogBookPanel1$uxDateVisited", GCLogin.getCustomGcDateFormat().format(new GregorianCalendar(year, month - 1, day).getTime()), + "ctl00$ContentBody$LogBookPanel1$uxDateVisited", GCLogin.formatGcCustomDate(year, month, day), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Month", Integer.toString(month), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Day", Integer.toString(day), "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Year", Integer.toString(year), @@ -1147,6 +1148,11 @@ public abstract class GCParser { Log.e("GCParser.postLog.confim", e); } + if (page == null) { + Log.e("GCParser.postLog: didn't get response"); + return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, ""); + } + try { final MatcherWrapper matcherOk = new MatcherWrapper(GCConstants.PATTERN_OK1, page); @@ -1160,7 +1166,7 @@ public abstract class GCParser { gcLogin.getLoginStatus(page); // the log-successful-page contains still the old value if (gcLogin.getActualCachesFound() >= 0) { - gcLogin.setActualCachesFound(gcLogin.getActualCachesFound() + 1); + gcLogin.setActualCachesFound(gcLogin.getActualCachesFound() + (logType.isFoundLog() ? 1 : 0)); } final String logID = TextUtils.getMatch(page, GCConstants.PATTERN_LOG_IMAGE_UPLOAD, ""); @@ -1211,6 +1217,11 @@ public abstract class GCParser { final File image = new File(imageUri.getPath()); final String response = Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$ImageUploadControl1$uxFileUpload", "image/jpeg", image)); + if (response == null) { + Log.e("GCParser.uploadLogIMage: didn't get response for image upload"); + return ImmutablePair.of(StatusCode.LOGIMAGE_POST_ERROR, null); + } + final MatcherWrapper matcherUrl = new MatcherWrapper(GCConstants.PATTERN_IMAGE_UPLOAD_URL, response); if (matcherUrl.find()) { @@ -1257,7 +1268,7 @@ public abstract class GCParser { params.put("ctl00$ContentBody$LogBookPanel1$uxDateVisited", ""); } else { params.put("ctl00$ContentBody$LogBookPanel1$DateTimeLogged", Integer.toString(month) + "/" + Integer.toString(day) + "/" + Integer.toString(year)); - params.put("ctl00$ContentBody$LogBookPanel1$uxDateVisited", GCLogin.getCustomGcDateFormat().format(new GregorianCalendar(year, month - 1, day).getTime())); + params.put("ctl00$ContentBody$LogBookPanel1$uxDateVisited", GCLogin.formatGcCustomDate(year, month, day)); } params.put( "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day), @@ -1417,7 +1428,10 @@ public abstract class GCParser { } private static String getUserToken(final Geocache cache) { - final String page = requestHtmlPage(cache.getGeocode(), null, "n"); + return parseUserToken(requestHtmlPage(cache.getGeocode(), null, "n")); + } + + private static String parseUserToken(final String page) { return TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, ""); } @@ -1483,7 +1497,7 @@ public abstract class GCParser { } } catch (final RuntimeException e) { // failed to parse trackable owner name - Log.w("GCParser.parseTrackable: Failed to parse trackable owner name"); + Log.w("GCParser.parseTrackable: Failed to parse trackable owner name", e); } // trackable origin @@ -1514,20 +1528,20 @@ public abstract class GCParser { } } catch (final RuntimeException e) { // failed to parse trackable last known place - Log.w("GCParser.parseTrackable: Failed to parse trackable last known place"); + Log.w("GCParser.parseTrackable: Failed to parse trackable last known place", e); } // released date - can be missing on the page final String releaseString = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_RELEASES, false, null); if (releaseString != null) { try { - trackable.setReleased(dateTbIn1.parse(releaseString)); - } catch (ParseException e) { + trackable.setReleased(DATE_TB_IN_1.parse(releaseString)); + } catch (final ParseException ignored) { if (trackable.getReleased() == null) { try { - trackable.setReleased(dateTbIn2.parse(releaseString)); - } catch (ParseException e1) { - Log.e("Could not parse trackable release " + releaseString); + trackable.setReleased(DATE_TB_IN_2.parse(releaseString)); + } catch (final ParseException e) { + Log.e("Could not parse trackable release " + releaseString, e); } } } @@ -1538,14 +1552,14 @@ public abstract class GCParser { if (null != distance) { try { trackable.setDistance(DistanceParser.parseDistance(distance, - !Settings.isUseImperialUnits())); + !Settings.useImperialUnits())); } catch (final NumberFormatException e) { Log.e("GCParser.parseTrackable: Failed to parse distance", e); } } // 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 { @@ -1558,12 +1572,12 @@ public abstract class GCParser { 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) { // failed to parse trackable details & image - Log.w("GCParser.parseTrackable: Failed to parse trackable details & image"); + Log.w("GCParser.parseTrackable: Failed to parse trackable details & image", e); } if (StringUtils.isEmpty(trackable.getDetails()) && page.contains(GCConstants.ERROR_TB_NOT_ACTIVATED)) { trackable.setDetails(CgeoApplication.getInstance().getString(R.string.trackable_not_activated)); @@ -1585,7 +1599,7 @@ public abstract class GCParser { long date = 0; try { date = GCLogin.parseGcCustomDate(matcherLogs.group(2)).getTime(); - } catch (final ParseException e) { + } catch (final ParseException ignored) { } final LogEntry logDone = new LogEntry( @@ -1630,7 +1644,7 @@ public abstract class GCParser { return trackable; } - private static String convertLinks(String input) { + private static String convertLinks(final String input) { if (input == null) { return null; } @@ -1656,23 +1670,19 @@ public abstract class GCParser { /** * Extract special logs (friends, own) through seperate request. * - * @param page - * The page to extrat userToken from - * @param logType - * The logType to request + * @param userToken the user token extracted from the web page + * @param logType the logType to request * @return Observable<LogEntry> The logs */ - private static Observable<LogEntry> getLogs(final String page, final Logs logType) { + private static Observable<LogEntry> getLogs(final String userToken, final Logs logType) { + if (userToken.isEmpty()) { + Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); + return Observable.empty(); + } + return Observable.defer(new Func0<Observable<LogEntry>>() { @Override public Observable<LogEntry> call() { - final MatcherWrapper userTokenMatcher = new MatcherWrapper(GCConstants.PATTERN_USERTOKEN, page); - if (!userTokenMatcher.find()) { - Log.e("GCParser.loadLogsFromDetails: unable to extract userToken"); - return Observable.empty(); - } - - final String userToken = userTokenMatcher.group(1); final Parameters params = new Parameters( "tkn", userToken, "idx", "1", @@ -1691,77 +1701,67 @@ public abstract class GCParser { Log.e("GCParser.loadLogsFromDetails: error " + statusCode + " when requesting log information"); return Observable.empty(); } - String rawResponse = Network.getResponseData(response); - if (rawResponse == null) { + final InputStream responseStream = Network.getResponseStream(response); + if (responseStream == null) { Log.e("GCParser.loadLogsFromDetails: unable to read whole response"); return Observable.empty(); } - return parseLogs(logType != Logs.ALL, rawResponse); + return parseLogs(logType != Logs.ALL, responseStream); } }).subscribeOn(RxUtils.networkScheduler); } - private static Observable<LogEntry> parseLogs(final boolean markAsFriendsLog, final String rawResponse) { + private static Observable<LogEntry> parseLogs(final boolean markAsFriendsLog, final InputStream responseStream) { return Observable.create(new OnSubscribe<LogEntry>() { @Override public void call(final Subscriber<? super LogEntry> subscriber) { - // for non logged in users the log book is not shown - if (StringUtils.isBlank(rawResponse)) { - subscriber.onCompleted(); - return; - } - try { - final JSONObject resp = new JSONObject(rawResponse); - if (!resp.getString("status").equals("success")) { - Log.e("GCParser.loadLogsFromDetails: status is " + resp.getString("status")); + final ObjectNode resp = (ObjectNode) JsonUtils.reader.readTree(responseStream); + if (!resp.path("status").asText().equals("success")) { + Log.e("GCParser.loadLogsFromDetails: status is " + resp.path("status").asText("[absent]")); subscriber.onCompleted(); return; } - final JSONArray data = resp.getJSONArray("data"); - - for (int index = 0; index < data.length(); index++) { - final JSONObject entry = data.getJSONObject(index); - + final ArrayNode data = (ArrayNode) resp.get("data"); + for (final JsonNode entry: data) { // FIXME: use the "LogType" field instead of the "LogTypeImage" one. - final String logIconNameExt = entry.optString("LogTypeImage", ".gif"); + final String logIconNameExt = entry.path("LogTypeImage").asText(".gif"); final String logIconName = logIconNameExt.substring(0, logIconNameExt.length() - 4); - long date = 0; + final long date; try { - date = GCLogin.parseGcCustomDate(entry.getString("Visited")).getTime(); - } catch (final ParseException e) { - Log.e("GCParser.loadLogsFromDetails: failed to parse log date."); + date = GCLogin.parseGcCustomDate(entry.get("Visited").asText()).getTime(); + } catch (ParseException | NullPointerException e) { + Log.e("GCParser.loadLogsFromDetails: failed to parse log date", e); + continue; } // TODO: we should update our log data structure to be able to record // proper coordinates, and make them clickable. In the meantime, it is // better to integrate those coordinates into the text rather than not // display them at all. - final String latLon = entry.getString("LatLonString"); - final String logText = (StringUtils.isEmpty(latLon) ? "" : (latLon + "<br/><br/>")) + TextUtils.removeControlCharacters(entry.getString("LogText")); + final String latLon = entry.path("LatLonString").asText(); + final String logText = (StringUtils.isEmpty(latLon) ? "" : (latLon + "<br/><br/>")) + TextUtils.removeControlCharacters(entry.path("LogText").asText()); final LogEntry logDone = new LogEntry( - TextUtils.removeControlCharacters(entry.getString("UserName")), + TextUtils.removeControlCharacters(entry.path("UserName").asText()), date, LogType.getByIconName(logIconName), logText); - logDone.found = entry.getInt("GeocacheFindCount"); + logDone.found = entry.path("GeocacheFindCount").asInt(); logDone.friend = markAsFriendsLog; - final JSONArray images = entry.getJSONArray("Images"); - for (int i = 0; i < images.length(); i++) { - final JSONObject image = images.getJSONObject(i); - final String url = "http://imgcdn.geocaching.com/cache/log/large/" + image.getString("FileName"); - final String title = TextUtils.removeControlCharacters(image.getString("Name")); + final ArrayNode images = (ArrayNode) entry.get("Images"); + for (final JsonNode image: images) { + final String url = "http://imgcdn.geocaching.com/cache/log/large/" + image.path("FileName").asText(); + final String title = TextUtils.removeControlCharacters(image.path("Name").asText()); final Image logImage = new Image(url, title); logDone.addLogImage(logImage); } subscriber.onNext(logDone); } - } catch (final JSONException e) { - // failed to parse logs + } catch (final IOException e) { Log.w("GCParser.loadLogsFromDetails: Failed to parse cache logs", e); } subscriber.onCompleted(); @@ -1770,7 +1770,7 @@ public abstract class GCParser { } @NonNull - public static List<LogType> parseTypes(String page) { + public static List<LogType> parseTypes(final String page) { if (StringUtils.isEmpty(page)) { return Collections.emptyList(); } @@ -1801,9 +1801,10 @@ public abstract class GCParser { return types; } + @NonNull public static List<TrackableLog> parseTrackableLog(final String page) { if (StringUtils.isEmpty(page)) { - return null; + return Collections.emptyList(); } String table = StringUtils.substringBetween(page, "<table id=\"tblTravelBugs\"", "</table>"); @@ -1816,7 +1817,7 @@ public abstract class GCParser { table = StringUtils.substringBetween(table, "<tbody>", "</tbody>"); if (StringUtils.isBlank(table)) { Log.e("GCParser.parseTrackableLog: tbody not found on page"); - return null; + return Collections.emptyList(); } final List<TrackableLog> trackableLogs = new ArrayList<>(); @@ -1865,16 +1866,12 @@ public abstract class GCParser { return; } - final Observable<LogEntry> logs = getLogs(page, Logs.ALL); - Observable<LogEntry> specialLogs; - if (Settings.isFriendLogsWanted()) { - CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_logs); - specialLogs = Observable.merge(getLogs(page, Logs.FRIENDS), - getLogs(page, Logs.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 String userToken = parseUserToken(page); + final Observable<LogEntry> logs = getLogs(userToken, Logs.ALL); + final Observable<LogEntry> ownLogs = getLogs(userToken, Logs.OWN).cache(); + final Observable<LogEntry> specialLogs = Settings.isFriendLogsWanted() ? + Observable.merge(getLogs(userToken, 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 @@ -1886,9 +1883,19 @@ public abstract class GCParser { mergedLogs.subscribe(new Action1<List<LogEntry>>() { @Override public void call(final List<LogEntry> logEntries) { - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), logEntries); + DataStore.saveLogs(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); @@ -1901,7 +1908,7 @@ public abstract class GCParser { } // Wait for completion of logs parsing, retrieving and merging - mergedLogs.toBlocking().last(); + RxUtils.waitForCompletion(mergedLogs); } /** @@ -1923,77 +1930,82 @@ public abstract class GCParser { } } - public static boolean uploadModifiedCoordinates(Geocache cache, Geopoint wpt) { + public static boolean uploadModifiedCoordinates(final Geocache cache, final Geopoint wpt) { return editModifiedCoordinates(cache, wpt); } - public static boolean deleteModifiedCoordinates(Geocache cache) { + public static boolean deleteModifiedCoordinates(final Geocache cache) { return editModifiedCoordinates(cache, null); } - public static boolean editModifiedCoordinates(Geocache cache, Geopoint wpt) { + public static boolean editModifiedCoordinates(final Geocache cache, final Geopoint wpt) { final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } - try { - JSONObject jo; - if (wpt != null) { - jo = new JSONObject().put("dto", (new JSONObject().put("ut", userToken) - .put("data", new JSONObject() - .put("lat", wpt.getLatitudeE6() / 1E6) - .put("lng", wpt.getLongitudeE6() / 1E6)))); - } else { - jo = new JSONObject().put("dto", (new JSONObject().put("ut", userToken))); - } - - final String uriSuffix = wpt != null ? "SetUserCoordinate" : "ResetUserCoordinate"; + final ObjectNode jo = new ObjectNode(JsonUtils.factory); + final ObjectNode dto = jo.putObject("dto").put("ut", userToken); + if (wpt != null) { + dto.putObject("data").put("lat", wpt.getLatitudeE6() / 1E6).put("lng", wpt.getLongitudeE6() / 1E6); + } - final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/"; - final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo); - Log.i("Sending to " + uriPrefix + uriSuffix + " :" + jo.toString()); + final String uriSuffix = wpt != null ? "SetUserCoordinate" : "ResetUserCoordinate"; - if (response != null && response.getStatusLine().getStatusCode() == 200) { - Log.i("GCParser.editModifiedCoordinates - edited on GC.com"); - return true; - } + final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/"; + final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo); - } catch (final JSONException e) { - Log.e("Unknown exception with json wrap code", e); + if (response != null && response.getStatusLine().getStatusCode() == 200) { + Log.i("GCParser.editModifiedCoordinates - edited on GC.com"); + return true; } + Log.e("GCParser.deleteModifiedCoordinates - cannot delete modified coords"); return false; } - public static boolean uploadPersonalNote(Geocache cache) { + public static boolean uploadPersonalNote(final Geocache cache) { final String userToken = getUserToken(cache); if (StringUtils.isEmpty(userToken)) { return false; } - try { - final JSONObject jo = new JSONObject() - .put("dto", (new JSONObject() - .put("et", StringUtils.defaultString(cache.getPersonalNote())) - .put("ut", userToken))); - - final String uriSuffix = "SetUserCacheNote"; + final ObjectNode jo = new ObjectNode(JsonUtils.factory); + jo.putObject("dto").put("et", StringUtils.defaultString(cache.getPersonalNote())).put("ut", userToken); - final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/"; - final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo); - Log.i("Sending to " + uriPrefix + uriSuffix + " :" + jo.toString()); + final String uriSuffix = "SetUserCacheNote"; - if (response != null && response.getStatusLine().getStatusCode() == 200) { - Log.i("GCParser.uploadPersonalNote - uploaded to GC.com"); - return true; - } + final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/"; + final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo); - } catch (final JSONException e) { - Log.e("Unknown exception with json wrap code", e); + if (response != null && response.getStatusLine().getStatusCode() == 200) { + Log.i("GCParser.uploadPersonalNote - uploaded to GC.com"); + return true; } + Log.e("GCParser.uploadPersonalNote - cannot upload personal note"); return false; } + public static boolean ignoreCache(@NonNull final Geocache cache) { + final String uri = "http://www.geocaching.com/bookmarks/ignore.aspx?guid=" + cache.getGuid() + "&WptTypeID=" + cache.getType().wptTypeId; + final String page = GCLogin.getInstance().postRequestLogged(uri, null); + + if (StringUtils.isBlank(page)) { + Log.e("GCParser.ignoreCache: No data from server"); + return false; + } + + final String[] viewstates = GCLogin.getViewstates(page); + + final Parameters params = new Parameters( + "__EVENTTARGET", "", + "__EVENTARGUMENT", "", + "ctl00$ContentBody$btnYes", "Yes. Ignore it."); + + GCLogin.putViewstates(params, viewstates); + final String response = Network.getResponseData(Network.postRequest(uri, params)); + + return StringUtils.contains(response, "<p class=\"Success\">"); + } } diff --git a/main/src/cgeo/geocaching/connector/gc/GCSmiliesProvider.java b/main/src/cgeo/geocaching/connector/gc/GCSmiliesProvider.java index eba9301..071c3b0 100644 --- a/main/src/cgeo/geocaching/connector/gc/GCSmiliesProvider.java +++ b/main/src/cgeo/geocaching/connector/gc/GCSmiliesProvider.java @@ -1,5 +1,7 @@ package cgeo.geocaching.connector.gc; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; public class GCSmiliesProvider { public enum Smiley { @@ -24,9 +26,10 @@ public class GCSmiliesProvider { DISAPPROVE("V"), QUESTION("?"); + @NonNull public final String text; - Smiley(final String text) { + Smiley(@NonNull final String text) { this.text = text; } @@ -35,12 +38,14 @@ public class GCSmiliesProvider { } } + @NonNull public static Smiley[] getSmilies() { return Smiley.values(); } - public static Smiley getSmiley(int itemId) { - for (Smiley smiley : getSmilies()) { + @Nullable + public static Smiley getSmiley(final int itemId) { + for (final Smiley smiley : getSmilies()) { if (smiley.getItemId() == itemId) { return smiley; } diff --git a/main/src/cgeo/geocaching/connector/gc/IconDecoder.java b/main/src/cgeo/geocaching/connector/gc/IconDecoder.java index c6a2afc..95bb77d 100644 --- a/main/src/cgeo/geocaching/connector/gc/IconDecoder.java +++ b/main/src/cgeo/geocaching/connector/gc/IconDecoder.java @@ -10,7 +10,7 @@ import android.graphics.Bitmap; * icon decoder for cache icons * */ -public abstract class IconDecoder { +abstract class IconDecoder { private static final int CT_TRADITIONAL = 0; private static final int CT_MULTI = 1; private static final int CT_MYSTERY = 2; @@ -25,7 +25,7 @@ public abstract class IconDecoder { private static final int CT_VIRTUAL = 11; private static final int CT_LETTERBOX = 12; - public static boolean parseMapPNG(final Geocache cache, Bitmap bitmap, UTFGridPosition xy, int zoomlevel) { + static boolean parseMapPNG(final Geocache cache, final Bitmap bitmap, final UTFGridPosition xy, final int zoomlevel) { final int topX = xy.getX() * 4; final int topY = xy.getY() * 4; final int bitmapWidth = bitmap.getWidth(); @@ -43,24 +43,24 @@ public abstract class IconDecoder { numberOfDetections = 13; } - int[] pngType = new int[numberOfDetections]; + final int[] pngType = new int[numberOfDetections]; for (int x = topX; x < topX + 4; x++) { for (int y = topY; y < topY + 4; y++) { - int color = bitmap.getPixel(x, y); + final int color = bitmap.getPixel(x, y); if ((color >>> 24) != 255) { continue; //transparent pixels (or semi_transparent) are only shadows of border } - int r = (color & 0xFF0000) >> 16; - int g = (color & 0xFF00) >> 8; - int b = color & 0xFF; + final int r = (color & 0xFF0000) >> 16; + final int g = (color & 0xFF00) >> 8; + final int b = color & 0xFF; if (isPixelDuplicated(r, g, b, zoomlevel)) { continue; } - int type; + final int type; if (zoomlevel < 12) { type = getCacheTypeFromPixel11(r, g, b); } else { @@ -143,7 +143,7 @@ public abstract class IconDecoder { * zoom level of map * @return true if parsing should not be performed */ - private static boolean isPixelDuplicated(int r, int g, int b, int zoomlevel) { + private static boolean isPixelDuplicated(final int r, final int g, final int b, final int zoomlevel) { if (zoomlevel < 12) { if (((r == g) && (g == b)) || ((r == 233) && (g == 233) && (b == 234))) { return true; @@ -194,7 +194,7 @@ public abstract class IconDecoder { * Blue component of pixel (from 0 - 255) * @return Value from 0 to 6 representing detected type or state of the cache. */ - private static int getCacheTypeFromPixel13(int r, int g, int b) { + private static int getCacheTypeFromPixel13(final int r, final int g, final int b) { if (b < 130) { if (r < 41) { return CT_MYSTERY; @@ -256,7 +256,7 @@ public abstract class IconDecoder { * Blue component of pixel (from 0 - 255) * @return Value from 0 to 6 representing detected type or state of the cache. */ - private static int getCacheTypeFromPixel14(int r, int g, int b) { + private static int getCacheTypeFromPixel14(final int r, final int g, final int b) { if (b < 128) { if (r < 214) { if (b < 37) { @@ -489,7 +489,7 @@ public abstract class IconDecoder { * Blue component of pixel (from 0 - 255) * @return Value from 0 to 4 representing detected type or state of the cache. */ - private static int getCacheTypeFromPixel11(int r, int g, int b) { + private static int getCacheTypeFromPixel11(final int r, final int g, final int b) { if (g < 136) { if (r < 90) { return g < 111 ? CT_MYSTERY : CT_TRADITIONAL; diff --git a/main/src/cgeo/geocaching/connector/gc/MapTokens.java b/main/src/cgeo/geocaching/connector/gc/MapTokens.java index 78ce4cb..b41533d 100644 --- a/main/src/cgeo/geocaching/connector/gc/MapTokens.java +++ b/main/src/cgeo/geocaching/connector/gc/MapTokens.java @@ -4,11 +4,11 @@ import android.util.Pair; /** * Wrapper type to make map tokens more type safe than with a String array. - * + * */ public final class MapTokens extends Pair<String, String> { - public MapTokens(String userSession, String sessionToken) { + MapTokens(final String userSession, final String sessionToken) { super(userSession, sessionToken); } diff --git a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java index 5bcfd17..1258ddd 100644 --- a/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java +++ b/main/src/cgeo/geocaching/connector/gc/RecaptchaHandler.java @@ -11,7 +11,7 @@ import cgeo.geocaching.utils.RxUtils; import org.apache.commons.io.IOUtils; import rx.Observable; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action1; import rx.functions.Func0; @@ -42,9 +42,9 @@ public class RecaptchaHandler extends Handler { } private void loadChallenge(final ImageView imageView, final View reloadButton, final boolean needsFetch) { - 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() { if (needsFetch) { recaptchaReceiver.fetchChallenge(); } @@ -53,7 +53,7 @@ public class RecaptchaHandler extends Handler { 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); @@ -64,7 +64,7 @@ public class RecaptchaHandler extends Handler { return Observable.empty(); } }); - AndroidObservable.bindActivity(activity, captcha).subscribeOn(RxUtils.networkScheduler).subscribe(new Action1<Bitmap>() { + AppObservable.bindActivity(activity, captcha).subscribeOn(RxUtils.networkScheduler).subscribe(new Action1<Bitmap>() { @Override public void call(final Bitmap bitmap) { imageView.setImageBitmap(bitmap); diff --git a/main/src/cgeo/geocaching/connector/gc/Tile.java b/main/src/cgeo/geocaching/connector/gc/Tile.java index 18fe65c..dd6371b 100644 --- a/main/src/cgeo/geocaching/connector/gc/Tile.java +++ b/main/src/cgeo/geocaching/connector/gc/Tile.java @@ -1,8 +1,8 @@ package cgeo.geocaching.connector.gc; import cgeo.geocaching.ICoordinates; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.utils.LeastRecentlyUsedSet; @@ -15,7 +15,6 @@ import org.eclipse.jdt.annotation.NonNull; import rx.Observable; import rx.functions.Func0; -import rx.util.async.Async; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -35,13 +34,13 @@ import java.util.Set; */ public class Tile { - public static final int TILE_SIZE = 256; - public static final int ZOOMLEVEL_MAX = 18; + static final int TILE_SIZE = 256; + static final int ZOOMLEVEL_MAX = 18; public static final int ZOOMLEVEL_MIN = 0; public static final int ZOOMLEVEL_MIN_PERSONALIZED = 12; - static final int[] NUMBER_OF_TILES = new int[ZOOMLEVEL_MAX - ZOOMLEVEL_MIN + 1]; - static final int[] NUMBER_OF_PIXELS = new int[ZOOMLEVEL_MAX - ZOOMLEVEL_MIN + 1]; + private static final int[] NUMBER_OF_TILES = new int[ZOOMLEVEL_MAX - ZOOMLEVEL_MIN + 1]; + private static final int[] NUMBER_OF_PIXELS = new int[ZOOMLEVEL_MAX - ZOOMLEVEL_MIN + 1]; static { for (int z = ZOOMLEVEL_MIN; z <= ZOOMLEVEL_MAX; z++) { NUMBER_OF_TILES[z] = 1 << z; @@ -56,11 +55,11 @@ public class Tile { private final int zoomLevel; private final Viewport viewPort; - public Tile(Geopoint origin, int zoomlevel) { + public Tile(final Geopoint origin, final int zoomlevel) { this(calcX(origin, clippedZoomlevel(zoomlevel)), calcY(origin, clippedZoomlevel(zoomlevel)), clippedZoomlevel(zoomlevel)); } - private Tile(int tileX, int tileY, int zoomlevel) { + private Tile(final int tileX, final int tileY, final int zoomlevel) { this.zoomLevel = clippedZoomlevel(zoomlevel); @@ -74,7 +73,7 @@ public class Tile { return zoomLevel; } - private static int clippedZoomlevel(int zoomlevel) { + private static int clippedZoomlevel(final int zoomlevel) { return Math.max(Math.min(zoomlevel, ZOOMLEVEL_MAX), ZOOMLEVEL_MIN); } @@ -95,7 +94,7 @@ public class Tile { */ private static int calcY(final Geopoint origin, final int zoomlevel) { // Optimization from Bing - double sinLatRad = Math.sin(Math.toRadians(origin.getLatitude())); + final double sinLatRad = Math.sin(Math.toRadians(origin.getLatitude())); // The cut of the fractional part instead of rounding to the nearest integer is intentional and part of the algorithm return (int) ((0.5 - Math.log((1 + sinLatRad) / (1 - sinLatRad)) / (4 * Math.PI)) * NUMBER_OF_TILES[zoomlevel]); } @@ -115,13 +114,13 @@ public class Tile { * href="http://developers.cloudmade.com/projects/tiles/examples/convert-coordinates-to-tile-numbers">Cloudmade</a> */ @NonNull - public Geopoint getCoord(UTFGridPosition pos) { + Geopoint getCoord(final UTFGridPosition pos) { - double pixX = tileX * TILE_SIZE + pos.x * 4; - double pixY = tileY * TILE_SIZE + pos.y * 4; + final double pixX = tileX * TILE_SIZE + pos.x * 4; + final double pixY = tileY * TILE_SIZE + pos.y * 4; - double lonDeg = ((360.0 * pixX) / NUMBER_OF_PIXELS[this.zoomLevel]) - 180.0; - double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * pixY / NUMBER_OF_PIXELS[this.zoomLevel]))); + final double lonDeg = ((360.0 * pixX) / NUMBER_OF_PIXELS[this.zoomLevel]) - 180.0; + final double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * pixY / NUMBER_OF_PIXELS[this.zoomLevel]))); return new Geopoint(Math.toDegrees(latRad), lonDeg); } @@ -144,17 +143,16 @@ public class Tile { * First point * @param right * Second point - * @return */ - public static int calcZoomLon(final Geopoint left, final Geopoint right, final int numberOfTiles) { + static int calcZoomLon(final Geopoint left, final Geopoint right, final int numberOfTiles) { int zoom = (int) Math.floor( Math.log(360.0 * numberOfTiles / (2.0 * Math.abs(left.getLongitude() - right.getLongitude()))) / Math.log(2) ); - Tile tileLeft = new Tile(left, zoom); - Tile tileRight = new Tile(right, zoom); + final Tile tileLeft = new Tile(left, zoom); + final Tile tileRight = new Tile(right, zoom); if (Math.abs(tileLeft.tileX - tileRight.tileX) < (numberOfTiles - 1)) { zoom += 1; @@ -177,9 +175,8 @@ public class Tile { * First point * @param top * Second point - * @return */ - public static int calcZoomLat(final Geopoint bottom, final Geopoint top, final int numberOfTiles) { + static int calcZoomLat(final Geopoint bottom, final Geopoint top, final int numberOfTiles) { int zoom = (int) Math.ceil( Math.log(2.0 * Math.PI * numberOfTiles / ( @@ -190,8 +187,8 @@ public class Tile { ) / Math.log(2) ); - Tile tileBottom = new Tile(bottom, zoom); - Tile tileTop = new Tile(top, zoom); + final Tile tileBottom = new Tile(bottom, zoom); + final Tile tileTop = new Tile(top, zoom); if (Math.abs(tileBottom.tileY - tileTop.tileY) > (numberOfTiles - 1)) { zoom -= 1; @@ -200,7 +197,7 @@ public class Tile { return Math.min(zoom, ZOOMLEVEL_MAX); } - private static double tanGrad(double angleGrad) { + private static double tanGrad(final double angleGrad) { return Math.tan(angleGrad / 180.0 * Math.PI); } @@ -208,15 +205,13 @@ public class Tile { * Calculates the inverted hyperbolic sine * (after Bronstein, Semendjajew: Taschenbuch der Mathematik) * - * @param x - * @return */ - private static double asinh(double x) { + private static double asinh(final double x) { return Math.log(x + Math.sqrt(x * x + 1.0)); } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) { return true; } @@ -238,14 +233,15 @@ public class Tile { * * @return An observable with one element, which may be <tt>null</tt>. */ - public static Observable<String> requestMapInfo(final String url, final Parameters params, final String referer) { + + static Observable<String> requestMapInfo(final String url, final Parameters params, final String referer) { final HttpResponse response = Network.getRequest(url, params, new Parameters("Referer", referer)); - return Async.start(new Func0<String>() { + return Observable.defer(new Func0<Observable<String>>() { @Override - public String call() { - return Network.getResponseData(response); + public Observable<String> call() { + return Observable.just(Network.getResponseData(response)); } - }, RxUtils.networkScheduler); + }).subscribeOn(RxUtils.networkScheduler); } /** Request .png image for a tile. Return as soon as the request has been made, before the answer has been @@ -253,19 +249,19 @@ public class Tile { * * @return An observable with one element, which may be <tt>null</tt>. */ - public static Observable<Bitmap> requestMapTile(final Parameters params) { + static Observable<Bitmap> requestMapTile(final Parameters params) { final HttpResponse response = Network.getRequest(GCConstants.URL_MAP_TILE, params, new Parameters("Referer", GCConstants.URL_LIVE_MAP)); - return Async.start(new Func0<Bitmap>() { + return Observable.defer(new Func0<Observable<Bitmap>>() { @Override - public Bitmap call() { + public Observable<Bitmap> call() { try { - return response != null ? BitmapFactory.decodeStream(response.getEntity().getContent()) : null; - } catch (IOException e) { + return Observable.just(response != null ? BitmapFactory.decodeStream(response.getEntity().getContent()) : null); + } catch (final IOException e) { Log.e("Tile.requestMapTile() ", e); - return null; + return Observable.just(null); } } - }, RxUtils.computationScheduler); + }).subscribeOn(RxUtils.computationScheduler); } public boolean containsPoint(final @NonNull ICoordinates point) { @@ -280,8 +276,6 @@ public class Tile { * Calculate needed tiles for the given viewport to cover it with * max 2x2 tiles * - * @param viewport - * @return */ protected static Set<Tile> getTilesForViewport(final Viewport viewport) { return getTilesForViewport(viewport, 2, Tile.ZOOMLEVEL_MIN); @@ -292,26 +286,22 @@ public class Tile { * You can define the minimum number of tiles on the longer axis * and/or the minimum zoom level. * - * @param viewport - * @param tilesOnAxis - * @param minZoom - * @return */ protected static Set<Tile> getTilesForViewport(final Viewport viewport, final int tilesOnAxis, final int minZoom) { - Set<Tile> tiles = new HashSet<>(); - int zoom = Math.max( + final Set<Tile> tiles = new HashSet<>(); + final int zoom = Math.max( Math.min(Tile.calcZoomLon(viewport.bottomLeft, viewport.topRight, tilesOnAxis), Tile.calcZoomLat(viewport.bottomLeft, viewport.topRight, tilesOnAxis)), minZoom); - Tile tileBottomLeft = new Tile(viewport.bottomLeft, zoom); - Tile tileTopRight = new Tile(viewport.topRight, zoom); + final Tile tileBottomLeft = new Tile(viewport.bottomLeft, zoom); + final Tile tileTopRight = new Tile(viewport.topRight, zoom); - int xLow = Math.min(tileBottomLeft.getX(), tileTopRight.getX()); - int xHigh = Math.max(tileBottomLeft.getX(), tileTopRight.getX()); + final int xLow = Math.min(tileBottomLeft.getX(), tileTopRight.getX()); + final int xHigh = Math.max(tileBottomLeft.getX(), tileTopRight.getX()); - int yLow = Math.min(tileBottomLeft.getY(), tileTopRight.getY()); - int yHigh = Math.max(tileBottomLeft.getY(), tileTopRight.getY()); + final int yLow = Math.min(tileBottomLeft.getY(), tileTopRight.getY()); + final int yHigh = Math.max(tileBottomLeft.getY(), tileTopRight.getY()); for (int xNum = xLow; xNum <= xHigh; xNum++) { for (int yNum = yLow; yNum <= yHigh; yNum++) { @@ -324,8 +314,6 @@ public class Tile { public static class TileCache extends LeastRecentlyUsedSet<Tile> { - private static final long serialVersionUID = -1942301031192719547L; - public TileCache() { super(64); } diff --git a/main/src/cgeo/geocaching/connector/gc/UTFGrid.java b/main/src/cgeo/geocaching/connector/gc/UTFGrid.java index 89a3de8..4db0519 100644 --- a/main/src/cgeo/geocaching/connector/gc/UTFGrid.java +++ b/main/src/cgeo/geocaching/connector/gc/UTFGrid.java @@ -3,22 +3,22 @@ package cgeo.geocaching.connector.gc; import java.util.List; /** - * + * * @see <a href="https://github.com/mapbox/mbtiles-spec/blob/master/1.1/utfgrid.md">Mapbox</a> - * + * */ -public final class UTFGrid { +final class UTFGrid { - public static final int GRID_MAXX = 63; - public static final int GRID_MAXY = 63; + static final int GRID_MAXX = 63; + static final int GRID_MAXY = 63; /** Calculate from a list of positions (x/y) the coords */ - public static UTFGridPosition getPositionInGrid(List<UTFGridPosition> positions) { + static UTFGridPosition getPositionInGrid(final List<UTFGridPosition> positions) { int minX = GRID_MAXX; int maxX = 0; int minY = GRID_MAXY; int maxY = 0; - for (UTFGridPosition pos : positions) { + for (final UTFGridPosition pos : positions) { minX = Math.min(minX, pos.x); maxX = Math.max(maxX, pos.x); minY = Math.min(minY, pos.y); diff --git a/main/src/cgeo/geocaching/connector/gc/UTFGridPosition.java b/main/src/cgeo/geocaching/connector/gc/UTFGridPosition.java index eff193a..f5cd208 100644 --- a/main/src/cgeo/geocaching/connector/gc/UTFGridPosition.java +++ b/main/src/cgeo/geocaching/connector/gc/UTFGridPosition.java @@ -8,13 +8,13 @@ import java.util.regex.Pattern; /** * Representation of a position inside an UTFGrid */ -public final class UTFGridPosition { +final class UTFGridPosition { - public final int x; - public final int y; + final int x; + final int y; private final static Pattern PATTERN_JSON_KEY = Pattern.compile("[^\\d]*" + "(\\d+),\\s*(\\d+)" + "[^\\d]*"); // (12, 34) - public UTFGridPosition(final int x, final int y) { + UTFGridPosition(final int x, final int y) { if (x < 0 || x > UTFGrid.GRID_MAXX) { throw new IllegalArgumentException("x outside bounds"); } @@ -36,9 +36,8 @@ public final class UTFGridPosition { /** * @param key * Key in the format (xx, xx) - * @return */ - static UTFGridPosition fromString(String key) { + static UTFGridPosition fromString(final String key) { final MatcherWrapper matcher = new MatcherWrapper(UTFGridPosition.PATTERN_JSON_KEY, key); try { if (matcher.matches()) { @@ -46,7 +45,7 @@ public final class UTFGridPosition { final int y = Integer.parseInt(matcher.group(2)); return new UTFGridPosition(x, y); } - } catch (NumberFormatException e) { + } catch (final NumberFormatException ignored) { } return new UTFGridPosition(0, 0); } diff --git a/main/src/cgeo/geocaching/connector/gc/UncertainProperty.java b/main/src/cgeo/geocaching/connector/gc/UncertainProperty.java index 71adcbd..204fc89 100644 --- a/main/src/cgeo/geocaching/connector/gc/UncertainProperty.java +++ b/main/src/cgeo/geocaching/connector/gc/UncertainProperty.java @@ -4,18 +4,17 @@ package cgeo.geocaching.connector.gc; /** * Property with certainty. When merging properties, the one with higher certainty wins. * - * @param <T> */ public class UncertainProperty<T> { private final T value; private final int certaintyLevel; - public UncertainProperty(T value) { + public UncertainProperty(final T value) { this(value, Tile.ZOOMLEVEL_MAX + 1); } - public UncertainProperty(T value, int certaintyLevel) { + public UncertainProperty(final T value, final int certaintyLevel) { this.value = value; this.certaintyLevel = certaintyLevel; } @@ -42,7 +41,7 @@ public class UncertainProperty<T> { return this; } - public static <T> UncertainProperty<T> getMergedProperty(UncertainProperty<T> property, UncertainProperty<T> otherProperty) { + public static <T> UncertainProperty<T> getMergedProperty(final UncertainProperty<T> property, final UncertainProperty<T> otherProperty) { if (null == property) { return otherProperty; } diff --git a/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java b/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java index acf7b48..5e5151a 100644 --- a/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java +++ b/main/src/cgeo/geocaching/connector/oc/IOCAuthParams.java @@ -7,7 +7,6 @@ public interface IOCAuthParams { /** * The site name: 'www.opencaching...' * - * @return */ @NonNull String getSite(); @@ -15,56 +14,48 @@ public interface IOCAuthParams { /** * ResId of the Consumer key * - * @return */ int getCKResId(); /** * ResId of the Consumer secret * - * @return */ int getCSResId(); /** * ResId of the Authorization title * - * @return */ int getAuthTitleResId(); /** * Preference key of the public token * - * @return */ int getTokenPublicPrefKey(); /** * Preference key of the secret token * - * @return */ int getTokenSecretPrefKey(); /** * Preference key of the temporary public token (OAuth) * - * @return */ int getTempTokenPublicPrefKey(); /** * Preference key of the temporary secret token (OAuth) * - * @return */ int getTempTokenSecretPrefKey(); /** * The URI to use as a callback (OAuth) * - * @return */ @NonNull String getCallbackUri(); diff --git a/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java b/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java index 284234e..a2322e0 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java +++ b/main/src/cgeo/geocaching/connector/oc/OCApiConnector.java @@ -31,7 +31,7 @@ public class OCApiConnector extends OCConnector implements ISearchByGeocode { private final ApiSupport apiSupport; private final String licenseString; - public OCApiConnector(String name, String host, String prefix, String cK, String licenseString, ApiSupport apiSupport) { + public OCApiConnector(final String name, final String host, final String prefix, final String cK, final String licenseString, final ApiSupport apiSupport) { super(name, host, prefix); this.cK = cK; this.apiSupport = apiSupport; @@ -39,10 +39,16 @@ public class OCApiConnector extends OCConnector implements ISearchByGeocode { } public void addAuthentication(final Parameters params) { - params.put(CryptUtils.rot13("pbafhzre_xrl"), CryptUtils.rot13(cK)); + final String rotCK = CryptUtils.rot13(cK); + // check that developers are not using the Ant defined properties without any values + if (StringUtils.startsWith(rotCK, "${")) { + throw new IllegalStateException("invalid OKAPI OAuth token " + rotCK); + } + params.put(CryptUtils.rot13("pbafhzre_xrl"), rotCK); } @Override + @NonNull public String getLicenseText(final @NonNull Geocache cache) { // NOT TO BE TRANSLATED return "© " + cache.getOwnerDisplayName() + ", <a href=\"" + getCacheUrl(cache) + "\">" + getName() + "</a>, " + licenseString; @@ -93,13 +99,13 @@ public class OCApiConnector extends OCConnector implements ISearchByGeocode { /** * Checks if a search based on a user name targets the current user - * + * * @param username * Name of the user the query is searching after * @return True - search target and current is same, False - current user not known or not the same as username */ @SuppressWarnings("static-method") - public boolean isSearchForMyCaches(String username) { + public boolean isSearchForMyCaches(final String username) { return false; } } diff --git a/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java b/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java index dd25c5e..2c783fb 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java +++ b/main/src/cgeo/geocaching/connector/oc/OCApiLiveConnector.java @@ -3,7 +3,6 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import cgeo.geocaching.LogCacheActivity; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.ILoggingManager; @@ -15,9 +14,10 @@ import cgeo.geocaching.connector.capability.ISearchByOwner; import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.connector.gc.MapTokens; import cgeo.geocaching.connector.oc.UserInfo.UserInfoStatus; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.CryptUtils; @@ -50,7 +50,8 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public SearchResult searchByViewport(@NonNull final Viewport viewport, final MapTokens tokens) { + @NonNull + public SearchResult searchByViewport(@NonNull final Viewport viewport, @NonNull final MapTokens tokens) { return new SearchResult(OkapiClient.getCachesBBox(viewport, this)); } @@ -99,7 +100,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean addToWatchlist(final Geocache cache) { + public boolean addToWatchlist(@NonNull final Geocache cache) { final boolean added = OkapiClient.setWatchState(cache, true, this); if (added) { @@ -110,7 +111,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean removeFromWatchlist(final Geocache cache) { + public boolean removeFromWatchlist(@NonNull final Geocache cache) { final boolean removed = OkapiClient.setWatchState(cache, false, this); if (removed) { @@ -126,12 +127,13 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public ILoggingManager getLoggingManager(final LogCacheActivity activity, final Geocache cache) { + @NonNull + public ILoggingManager getLoggingManager(@NonNull final LogCacheActivity activity, @NonNull final Geocache cache) { return new OkapiLoggingManager(activity, this, cache); } @Override - public boolean canLog(final Geocache cache) { + public boolean canLog(@NonNull final Geocache cache) { return true; } @@ -150,7 +152,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return StringUtils.isNotEmpty(getUserName()) && StringUtils.equals(cache.getOwnerDisplayName(), getUserName()); } @@ -176,8 +178,7 @@ public class OCApiLiveConnector extends OCApiConnector implements ISearchByCente @Override public SearchResult searchByKeyword(final @NonNull String name, final @NonNull RecaptchaReceiver recaptchaReceiver) { - final Geopoint currentPos = CgeoApplication.getInstance().currentGeo().getCoords(); - return new SearchResult(OkapiClient.getCachesNamed(currentPos, name, this)); + return new SearchResult(OkapiClient.getCachesNamed(Sensors.getInstance().currentGeo().getCoords(), name, this)); } @Override diff --git a/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java b/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java index a1030f0..21d10df 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java +++ b/main/src/cgeo/geocaching/connector/oc/OCAuthParams.java @@ -3,7 +3,7 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Intents; import cgeo.geocaching.R; -import cgeo.geocaching.network.OAuthAuthorizationActivity.OAuthParameters; +import cgeo.geocaching.activity.OAuthAuthorizationActivity.OAuthParameters; import org.eclipse.jdt.annotation.NonNull; diff --git a/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java b/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java index eb7e7a1..c15ca6c 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java +++ b/main/src/cgeo/geocaching/connector/oc/OCAuthorizationActivity.java @@ -2,8 +2,8 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.Intents; import cgeo.geocaching.R; +import cgeo.geocaching.activity.OAuthAuthorizationActivity; import cgeo.geocaching.connector.oc.OkapiError.OkapiErrors; -import cgeo.geocaching.network.OAuthAuthorizationActivity; import cgeo.geocaching.settings.Settings; import ch.boye.httpclientandroidlib.HttpResponse; @@ -23,9 +23,9 @@ public class OCAuthorizationActivity extends OAuthAuthorizationActivity { private int tempTokenSecretPrefKey; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { - Bundle extras = getIntent().getExtras(); + final Bundle extras = getIntent().getExtras(); if (extras != null) { titleResId = extras.getInt(Intents.EXTRA_OAUTH_TITLE_RES_ID); tokenPublicPrefKey = extras.getInt(Intents.EXTRA_OAUTH_TOKEN_PUBLIC_KEY); @@ -68,8 +68,8 @@ public class OCAuthorizationActivity extends OAuthAuthorizationActivity { * Return an extended error in case of an invalid time stamp */ @Override - protected String getExtendedErrorMsg(HttpResponse response) { - OkapiError error = OkapiClient.decodeErrorResponse(response); + protected String getExtendedErrorMsg(final HttpResponse response) { + final OkapiError error = OkapiClient.decodeErrorResponse(response); if (error.getResult() == OkapiErrors.INVALID_TIMESTAMP) { return res.getString(R.string.init_login_popup_invalid_timestamp); } diff --git a/main/src/cgeo/geocaching/connector/oc/OCCZConnector.java b/main/src/cgeo/geocaching/connector/oc/OCCZConnector.java new file mode 100644 index 0000000..ee4330a --- /dev/null +++ b/main/src/cgeo/geocaching/connector/oc/OCCZConnector.java @@ -0,0 +1,34 @@ +package cgeo.geocaching.connector.oc; + +import cgeo.geocaching.utils.Log; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +public class OCCZConnector extends OCConnector { + + private static final String GEOCODE_PREFIX = "OZ"; + + public OCCZConnector() { + super("OpenCaching.CZ", "www.opencaching.cz", GEOCODE_PREFIX); + } + + @Override + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { + if (!StringUtils.containsIgnoreCase(url, "opencaching.cz")) { + return null; + } + final String id = StringUtils.substringAfter(url, "cacheid="); + try { + final String geocode = GEOCODE_PREFIX + StringUtils.leftPad(Integer.toHexString(Integer.parseInt(id)), 4, '0'); + if (canHandle(geocode)) { + return geocode; + } + } catch (final NumberFormatException e) { + Log.e("Unexpected URL for opencaching.cz " + url); + } + return super.getGeocodeFromUrl(url); + } +} diff --git a/main/src/cgeo/geocaching/connector/oc/OCConnector.java b/main/src/cgeo/geocaching/connector/oc/OCConnector.java index 1ba88d5..6d7b23a 100644 --- a/main/src/cgeo/geocaching/connector/oc/OCConnector.java +++ b/main/src/cgeo/geocaching/connector/oc/OCConnector.java @@ -1,12 +1,13 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import cgeo.geocaching.R; import cgeo.geocaching.connector.AbstractConnector; import cgeo.geocaching.enumerations.LogType; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import java.util.Arrays; import java.util.List; @@ -29,42 +30,46 @@ public class OCConnector extends AbstractConnector { } @Override - public boolean canHandle(@NonNull String geocode) { + public boolean canHandle(@NonNull final String geocode) { return codePattern.matcher(geocode).matches(); } @Override + @NonNull public String getName() { return name; } @Override - public String getCacheUrl(@NonNull Geocache cache) { + @NonNull + public String getCacheUrl(@NonNull final Geocache cache) { return getCacheUrlPrefix() + cache.getGeocode(); } @Override + @NonNull public String getHost() { return host; } @Override - public boolean isZippedGPXFile(String fileName) { + public boolean isZippedGPXFile(@NonNull final String fileName) { return GPX_ZIP_FILE_PATTERN.matcher(fileName).matches(); } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return false; } @Override + @NonNull protected String getCacheUrlPrefix() { return "http://" + host + "/viewcache.php?wp="; } @Override - public int getCacheMapMarkerId(boolean disabled) { + public int getCacheMapMarkerId(final boolean disabled) { if (disabled) { return R.drawable.marker_disabled_oc; } @@ -72,11 +77,32 @@ public class OCConnector extends AbstractConnector { } @Override - public final List<LogType> getPossibleLogTypes(Geocache cache) { + @NonNull + public final List<LogType> getPossibleLogTypes(@NonNull final Geocache cache) { if (cache.isEventCache()) { return EVENT_LOG_TYPES; } return STANDARD_LOG_TYPES; } + + @Override + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { + // different opencaching installations have different supported URLs + + // host.tld/geocode + final String shortHost = StringUtils.remove(getHost(), "www."); + String geocode = StringUtils.substringAfter(url, shortHost + "/"); + if (canHandle(geocode)) { + return geocode; + } + + // host.tld/viewcache.php?wp=geocode + geocode = StringUtils.substringAfter(url, shortHost + "/viewcache.php?wp="); + if (canHandle(geocode)) { + return geocode; + } + return super.getGeocodeFromUrl(url); + } } diff --git a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java index bd87042..e7d4e6f 100644 --- a/main/src/cgeo/geocaching/connector/oc/OkapiClient.java +++ b/main/src/cgeo/geocaching/connector/oc/OkapiClient.java @@ -22,44 +22,57 @@ import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.enumerations.WaypointType; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.OAuth; +import cgeo.geocaching.network.OAuthTokens; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.HtmlUtils; +import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.SynchronizedDateFormat; import ch.boye.httpclientandroidlib.HttpResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import android.net.Uri; +import java.io.IOException; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.EnumSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; - +import java.util.regex.Pattern; + +/** + * Client for the OpenCaching API (Okapi). + * + * @see <a href="http://www.opencaching.de/okapi/introduction.html">Okapi overview</a> + * + */ final class OkapiClient { + private static final String PARAMETER_LOGCOUNT_VALUE = "all"; + private static final String PARAMETER_LOGCOUNT_KEY = "lpc"; private static final char SEPARATOR = '|'; private static final String SEPARATOR_STRING = Character.toString(SEPARATOR); private static final SynchronizedDateFormat LOG_DATE_FORMAT = new SynchronizedDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", TimeZone.getTimeZone("UTC"), Locale.US); @@ -132,6 +145,8 @@ final class OkapiClient { private static final String METHOD_SEARCH_NEAREST = "services/caches/search/nearest"; private static final String METHOD_RETRIEVE_CACHES = "services/caches/geocaches"; + private static final Pattern PATTERN_TIMEZONE = Pattern.compile("([+-][01][0-9]):([03])0"); + public static Geocache getCache(final String geoCode) { final Parameters params = new Parameters("cache_code", geoCode); final IConnector connector = ConnectorFactory.getConnector(geoCode); @@ -143,13 +158,14 @@ final class OkapiClient { params.add("fields", getFullFields(ocapiConn)); params.add("attribution_append", "none"); + params.add(PARAMETER_LOGCOUNT_KEY, PARAMETER_LOGCOUNT_VALUE); final JSONResult result = request(ocapiConn, OkapiService.SERVICE_CACHE, params); return result.isSuccess ? parseCache(result.data) : null; } - public static List<Geocache> getCachesAround(final Geopoint center, final OCApiConnector connector) { + public static List<Geocache> getCachesAround(final Geopoint center, @NonNull final OCApiConnector connector) { final String centerString = GeopointFormatter.format(GeopointFormatter.Format.LAT_DECDEGREE_RAW, center) + SEPARATOR + GeopointFormatter.format(GeopointFormatter.Format.LON_DECDEGREE_RAW, center); final Parameters params = new Parameters("search_method", METHOD_SEARCH_NEAREST); final Map<String, String> valueMap = new LinkedHashMap<>(); @@ -160,15 +176,15 @@ final class OkapiClient { return requestCaches(connector, params, valueMap, false); } - public static List<Geocache> getCachesByOwner(final String username, final OCApiConnector connector) { + public static List<Geocache> getCachesByOwner(final String username, @NonNull final OCApiConnector connector) { return getCachesByUser(username, connector, "owner_uuid"); } - public static List<Geocache> getCachesByFinder(final String username, final OCApiConnector connector) { + public static List<Geocache> getCachesByFinder(final String username, @NonNull final OCApiConnector connector) { return getCachesByUser(username, connector, "found_by"); } - private static List<Geocache> getCachesByUser(final String username, final OCApiConnector connector, final String userRequestParam) { + private static List<Geocache> getCachesByUser(final String username, @NonNull final OCApiConnector connector, final String userRequestParam) { final Parameters params = new Parameters("search_method", METHOD_SEARCH_ALL); final Map<String, String> valueMap = new LinkedHashMap<>(); final @Nullable @@ -181,7 +197,7 @@ final class OkapiClient { return requestCaches(connector, params, valueMap, connector.isSearchForMyCaches(username)); } - public static List<Geocache> getCachesNamed(final Geopoint center, final String namePart, final OCApiConnector connector) { + public static List<Geocache> getCachesNamed(final Geopoint center, final String namePart, @NonNull final OCApiConnector connector) { final Map<String, String> valueMap = new LinkedHashMap<>(); final Parameters params; @@ -202,17 +218,22 @@ final class OkapiClient { return requestCaches(connector, params, valueMap, false); } - private static List<Geocache> requestCaches(final OCApiConnector connector, final Parameters params, final Map<String, String> valueMap, final boolean my) { + private static List<Geocache> requestCaches(@NonNull final OCApiConnector connector, final Parameters params, final Map<String, String> valueMap, final boolean my) { // if a global type filter is set, and OKAPI does not know that type, then return an empty list instead of all caches if (Settings.getCacheType() != CacheType.ALL && StringUtils.isBlank(getFilterFromType())) { return Collections.emptyList(); } addFilterParams(valueMap, connector, my); - params.add("search_params", new JSONObject(valueMap).toString()); + try { + params.add("search_params", JsonUtils.writer.writeValueAsString(valueMap)); + } catch (final JsonProcessingException e) { + Log.e("requestCaches", e); + return Collections.emptyList(); + } addRetrieveParams(params, connector); - final JSONObject data = request(connector, OkapiService.SERVICE_SEARCH_AND_RETRIEVE, params).data; + final ObjectNode data = request(connector, OkapiService.SERVICE_SEARCH_AND_RETRIEVE, params).data; if (data == null) { return Collections.emptyList(); @@ -224,7 +245,7 @@ final class OkapiClient { /** * Assumes level 3 OAuth. */ - public static List<Geocache> getCachesBBox(final Viewport viewport, final OCApiConnector connector) { + public static List<Geocache> getCachesBBox(final Viewport viewport, @NonNull final OCApiConnector connector) { if (viewport.getLatitudeSpan() == 0 || viewport.getLongitudeSpan() == 0) { return Collections.emptyList(); @@ -241,11 +262,11 @@ final class OkapiClient { return requestCaches(connector, params, valueMap, false); } - public static boolean setWatchState(final Geocache cache, final boolean watched, final OCApiConnector connector) { + public static boolean setWatchState(final Geocache cache, final boolean watched, @NonNull final OCApiConnector connector) { final Parameters params = new Parameters("cache_code", cache.getGeocode()); params.add("watched", watched ? "true" : "false"); - final JSONObject data = request(connector, OkapiService.SERVICE_MARK_CACHE, params).data; + final ObjectNode data = request(connector, OkapiService.SERVICE_MARK_CACHE, params).data; if (data == null) { return false; @@ -256,7 +277,7 @@ final class OkapiClient { return true; } - public static LogResult postLog(final Geocache cache, final LogType logType, final Calendar date, final String log, final String logPassword, final OCApiConnector connector) { + public static LogResult postLog(final Geocache cache, final LogType logType, final Calendar date, final String log, final String logPassword, @NonNull final OCApiConnector connector) { final Parameters params = new Parameters("cache_code", cache.getGeocode()); params.add("logtype", logType.oc_type); params.add("comment", log); @@ -269,68 +290,58 @@ final class OkapiClient { params.add("password", logPassword); } - final JSONObject data = request(connector, OkapiService.SERVICE_SUBMIT_LOG, params).data; + final ObjectNode data = request(connector, OkapiService.SERVICE_SUBMIT_LOG, params).data; if (data == null) { return new LogResult(StatusCode.LOG_POST_ERROR, ""); } try { - if (data.getBoolean("success")) { - return new LogResult(StatusCode.NO_ERROR, data.getString("log_uuid")); + if (data.get("success").asBoolean()) { + return new LogResult(StatusCode.NO_ERROR, data.get("log_uuid").asText()); } return new LogResult(StatusCode.LOG_POST_ERROR, ""); - } catch (final JSONException e) { + } catch (final NullPointerException e) { Log.e("OkapiClient.postLog", e); } return new LogResult(StatusCode.LOG_POST_ERROR, ""); } - private static List<Geocache> parseCaches(final JSONObject response) { + private static List<Geocache> parseCaches(final ObjectNode response) { try { // Check for empty result - final String result = response.getString("results"); - if (StringUtils.isBlank(result) || StringUtils.equals(result, "[]")) { + final JsonNode results = response.path("results"); + if (!results.isObject()) { return Collections.emptyList(); } // Get and iterate result list - final JSONObject cachesResponse = response.getJSONObject("results"); - if (cachesResponse != null) { - final List<Geocache> caches = new ArrayList<>(cachesResponse.length()); - final Iterator<?> keys = cachesResponse.keys(); - while (keys.hasNext()) { - final Object next = keys.next(); - if (next instanceof String) { - final String key = (String) next; - final Geocache cache = parseSmallCache(cachesResponse.getJSONObject(key)); - caches.add(cache); - } - } - return caches; + final List<Geocache> caches = new ArrayList<>(results.size()); + for (final JsonNode cache: results) { + caches.add(parseSmallCache((ObjectNode) cache)); } - } catch (final JSONException e) { + return caches; + } catch (ClassCastException | NullPointerException e) { Log.e("OkapiClient.parseCachesResult", e); } return Collections.emptyList(); } - private static Geocache parseSmallCache(final JSONObject response) { + private static Geocache parseSmallCache(final ObjectNode response) { final Geocache cache = new Geocache(); cache.setReliableLatLon(true); try { - parseCoreCache(response, cache); - DataStore.saveCache(cache, EnumSet.of(SaveFlag.CACHE)); - } catch (final JSONException e) { + } catch (final NullPointerException e) { + // FIXME: here we may return a partially filled cache Log.e("OkapiClient.parseSmallCache", e); } return cache; } - private static Geocache parseCache(final JSONObject response) { + private static Geocache parseCache(final ObjectNode response) { final Geocache cache = new Geocache(); cache.setReliableLatLon(true); try { @@ -338,28 +349,27 @@ final class OkapiClient { parseCoreCache(response, cache); // not used: url - final JSONObject ownerObject = response.getJSONObject(CACHE_OWNER); - final String owner = parseUser(ownerObject); + final String owner = parseUser(response.get(CACHE_OWNER)); cache.setOwnerDisplayName(owner); // OpenCaching has no distinction between user id and user display name. Set the ID anyway to simplify c:geo workflows. cache.setOwnerUserId(owner); - cache.getLogCounts().put(LogType.FOUND_IT, response.getInt(CACHE_FOUNDS)); - cache.getLogCounts().put(LogType.DIDNT_FIND_IT, response.getInt(CACHE_NOTFOUNDS)); + cache.getLogCounts().put(LogType.FOUND_IT, response.get(CACHE_FOUNDS).asInt()); + cache.getLogCounts().put(LogType.DIDNT_FIND_IT, response.get(CACHE_NOTFOUNDS).asInt()); // only current Api - cache.getLogCounts().put(LogType.WILL_ATTEND, response.optInt(CACHE_WILLATTENDS)); + cache.getLogCounts().put(LogType.WILL_ATTEND, response.path(CACHE_WILLATTENDS).asInt()); - if (!response.isNull(CACHE_RATING)) { - cache.setRating((float) response.getDouble(CACHE_RATING)); + if (response.has(CACHE_RATING)) { + cache.setRating((float) response.get(CACHE_RATING).asDouble()); } - cache.setVotes(response.getInt(CACHE_VOTES)); + cache.setVotes(response.get(CACHE_VOTES).asInt()); - cache.setFavoritePoints(response.getInt(CACHE_RECOMMENDATIONS)); + cache.setFavoritePoints(response.get(CACHE_RECOMMENDATIONS).asInt()); // not used: req_password // Prepend gc-link to description if available final StringBuilder description = new StringBuilder(500); - if (!response.isNull("gc_code")) { - final String gccode = response.getString("gc_code"); + if (response.hasNonNull("gc_code")) { + final String gccode = response.get("gc_code").asText(); description.append(CgeoApplication.getInstance().getResources() .getString(R.string.cache_listed_on, GCConnector.getInstance().getName())) .append(": <a href=\"http://coord.info/") @@ -368,71 +378,69 @@ final class OkapiClient { .append(gccode) .append("</a><br /><br />"); } - description.append(response.getString(CACHE_DESCRIPTION)); + description.append(response.get(CACHE_DESCRIPTION).asText()); cache.setDescription(description.toString()); // currently the hint is delivered as HTML (contrary to OKAPI documentation), so we can store it directly - cache.setHint(response.getString(CACHE_HINT)); + cache.setHint(response.get(CACHE_HINT).asText()); // not used: hints - final JSONArray images = response.getJSONArray(CACHE_IMAGES); + final ArrayNode images = (ArrayNode) response.get(CACHE_IMAGES); if (images != null) { - for (int i = 0; i < images.length(); i++) { - final JSONObject imageResponse = images.getJSONObject(i); - final String title = imageResponse.getString(CACHE_IMAGE_CAPTION); - final String url = absoluteUrl(imageResponse.getString(CACHE_IMAGE_URL), cache.getGeocode()); + for (final JsonNode imageResponse: images) { + final String title = imageResponse.get(CACHE_IMAGE_CAPTION).asText(); + final String url = absoluteUrl(imageResponse.get(CACHE_IMAGE_URL).asText(), cache.getGeocode()); // all images are added as spoiler images, although OKAPI has spoiler and non spoiler images cache.addSpoiler(new Image(url, title)); } } - cache.setAttributes(parseAttributes(response.getJSONArray(CACHE_ATTRNAMES), response.optJSONArray(CACHE_ATTR_ACODES))); + cache.setAttributes(parseAttributes((ArrayNode) response.path(CACHE_ATTRNAMES), (ArrayNode) response.get(CACHE_ATTR_ACODES))); //TODO: Store license per cache //cache.setLicense(response.getString("attribution_note")); - cache.setWaypoints(parseWaypoints(response.getJSONArray(CACHE_WPTS)), false); + cache.setWaypoints(parseWaypoints((ArrayNode) response.path(CACHE_WPTS)), false); - cache.setInventory(parseTrackables(response.getJSONArray(CACHE_TRACKABLES))); + cache.setInventory(parseTrackables((ArrayNode) response.path(CACHE_TRACKABLES))); - if (!response.isNull(CACHE_IS_WATCHED)) { - cache.setOnWatchlist(response.getBoolean(CACHE_IS_WATCHED)); + if (response.has(CACHE_IS_WATCHED)) { + cache.setOnWatchlist(response.get(CACHE_IS_WATCHED).asBoolean()); } - if (!response.isNull(CACHE_MY_NOTES)) { - cache.setPersonalNote(response.getString(CACHE_MY_NOTES)); - cache.parseWaypointsFromNote(); + if (response.hasNonNull(CACHE_MY_NOTES)) { + cache.setPersonalNote(response.get(CACHE_MY_NOTES).asText()); } - cache.setLogPasswordRequired(response.getBoolean(CACHE_REQ_PASSWORD)); + cache.setLogPasswordRequired(response.get(CACHE_REQ_PASSWORD).asBoolean()); cache.setDetailedUpdatedNow(); // save full detailed caches DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), parseLogs(response.getJSONArray(CACHE_LATEST_LOGS))); - } catch (final JSONException e) { + DataStore.saveLogs(cache.getGeocode(), parseLogs((ArrayNode) response.path(CACHE_LATEST_LOGS))); + } catch (ClassCastException | NullPointerException e) { Log.e("OkapiClient.parseCache", e); } return cache; } - private static void parseCoreCache(final JSONObject response, final Geocache cache) throws JSONException { - cache.setGeocode(response.getString(CACHE_CODE)); - cache.setName(response.getString(CACHE_NAME)); + private static void parseCoreCache(final ObjectNode response, final Geocache cache) { + cache.setGeocode(response.get(CACHE_CODE).asText()); + cache.setName(response.get(CACHE_NAME).asText()); // not used: names - setLocation(cache, response.getString(CACHE_LOCATION)); - cache.setType(getCacheType(response.getString(CACHE_TYPE))); + setLocation(cache, response.get(CACHE_LOCATION).asText()); + cache.setType(getCacheType(response.get(CACHE_TYPE).asText())); - final String status = response.getString(CACHE_STATUS); + final String status = response.get(CACHE_STATUS).asText(); cache.setDisabled(status.equalsIgnoreCase(CACHE_STATUS_DISABLED)); cache.setArchived(status.equalsIgnoreCase(CACHE_STATUS_ARCHIVED)); cache.setSize(getCacheSize(response)); - cache.setDifficulty((float) response.getDouble(CACHE_DIFFICULTY)); - cache.setTerrain((float) response.getDouble(CACHE_TERRAIN)); + cache.setDifficulty((float) response.get(CACHE_DIFFICULTY).asDouble()); + cache.setTerrain((float) response.get(CACHE_TERRAIN).asDouble()); - cache.setInventoryItems(response.getInt(CACHE_TRACKABLES_COUNT)); + cache.setInventoryItems(response.get(CACHE_TRACKABLES_COUNT).asInt()); - if (!response.isNull(CACHE_IS_FOUND)) { - cache.setFound(response.getBoolean(CACHE_IS_FOUND)); + if (response.has(CACHE_IS_FOUND)) { + cache.setFound(response.get(CACHE_IS_FOUND).asBoolean()); } - cache.setHidden(parseDate(response.getString(CACHE_HIDDEN))); + cache.setHidden(parseDate(response.get(CACHE_HIDDEN).asText())); } private static String absoluteUrl(final String url, final String geocode) { @@ -448,38 +456,36 @@ final class OkapiClient { return url; } - private static String parseUser(final JSONObject user) throws JSONException { - return user.getString(USER_USERNAME); + private static String parseUser(final JsonNode user) { + return user.get(USER_USERNAME).asText(); } - private static List<LogEntry> parseLogs(final JSONArray logsJSON) { + private static List<LogEntry> parseLogs(final ArrayNode logsJSON) { final List<LogEntry> result = new LinkedList<>(); - for (int i = 0; i < logsJSON.length(); i++) { + for (final JsonNode logResponse: logsJSON) { try { - final JSONObject logResponse = logsJSON.getJSONObject(i); final LogEntry log = new LogEntry( - parseUser(logResponse.getJSONObject(LOG_USER)), - parseDate(logResponse.getString(LOG_DATE)).getTime(), - parseLogType(logResponse.getString(LOG_TYPE)), - logResponse.getString(LOG_COMMENT).trim()); + parseUser(logResponse.get(LOG_USER)), + parseDate(logResponse.get(LOG_DATE).asText()).getTime(), + parseLogType(logResponse.get(LOG_TYPE).asText()), + HtmlUtils.removeExtraParagraph(logResponse.get(LOG_COMMENT).asText().trim())); result.add(log); - } catch (final JSONException e) { + } catch (final NullPointerException e) { Log.e("OkapiClient.parseLogs", e); } } return result; } - private static List<Waypoint> parseWaypoints(final JSONArray wptsJson) { + private static List<Waypoint> parseWaypoints(final ArrayNode wptsJson) { List<Waypoint> result = null; - for (int i = 0; i < wptsJson.length(); i++) { + for (final JsonNode wptResponse: wptsJson) { try { - final JSONObject wptResponse = wptsJson.getJSONObject(i); - final Waypoint wpt = new Waypoint(wptResponse.getString(WPT_NAME), - parseWptType(wptResponse.getString(WPT_TYPE)), + final Waypoint wpt = new Waypoint(wptResponse.get(WPT_NAME).asText(), + parseWptType(wptResponse.get(WPT_TYPE).asText()), false); - wpt.setNote(wptResponse.getString(WPT_DESCRIPTION)); - final Geopoint pt = parseCoords(wptResponse.getString(WPT_LOCATION)); + wpt.setNote(wptResponse.get(WPT_DESCRIPTION).asText()); + final Geopoint pt = parseCoords(wptResponse.get(WPT_LOCATION).asText()); if (pt != null) { wpt.setCoords(pt); } @@ -488,26 +494,25 @@ final class OkapiClient { } wpt.setPrefix(wpt.getName()); result.add(wpt); - } catch (final JSONException e) { + } catch (final NullPointerException e) { Log.e("OkapiClient.parseWaypoints", e); } } return result; } - private static List<Trackable> parseTrackables(final JSONArray trackablesJson) { - if (trackablesJson.length() == 0) { + private static List<Trackable> parseTrackables(final ArrayNode trackablesJson) { + if (trackablesJson.size() == 0) { return Collections.emptyList(); } final List<Trackable> result = new ArrayList<>(); - for (int i = 0; i < trackablesJson.length(); i++) { + for (final JsonNode trackableResponse: trackablesJson) { try { - final JSONObject trackableResponse = trackablesJson.getJSONObject(i); final Trackable trk = new Trackable(); - trk.setGeocode(trackableResponse.getString(TRK_GEOCODE)); - trk.setName(trackableResponse.getString(TRK_NAME)); + trk.setGeocode(trackableResponse.get(TRK_GEOCODE).asText()); + trk.setName(trackableResponse.get(TRK_NAME).asText()); result.add(trk); - } catch (final JSONException e) { + } catch (final NullPointerException e) { Log.e("OkapiClient.parseWaypoints", e); // Don't overwrite internal state with possibly partial result return null; @@ -576,7 +581,7 @@ final class OkapiClient { } private static Date parseDate(final String date) { - final String strippedDate = date.replaceAll("\\+0([0-9]){1}\\:00", "+0$100"); + final String strippedDate = PATTERN_TIMEZONE.matcher(date).replaceAll("$1$20"); try { return ISO8601DATEFORMAT.parse(strippedDate); } catch (final ParseException e) { @@ -595,14 +600,14 @@ final class OkapiClient { return null; } - private static List<String> parseAttributes(final JSONArray nameList, final JSONArray acodeList) { + private static List<String> parseAttributes(final ArrayNode nameList, final ArrayNode acodeList) { final List<String> result = new ArrayList<>(); - for (int i = 0; i < nameList.length(); i++) { + for (int i = 0; i < nameList.size(); i++) { try { - final String name = nameList.getString(i); - final int acode = acodeList != null ? Integer.parseInt(acodeList.getString(i).substring(1)) : CacheAttribute.NO_ID; + final String name = nameList.get(i).asText(); + final int acode = acodeList != null ? Integer.parseInt(acodeList.get(i).asText().substring(1)) : CacheAttribute.NO_ID; final CacheAttribute attr = CacheAttribute.getByOcACode(acode); if (attr != null) { @@ -610,7 +615,7 @@ final class OkapiClient { } else { result.add(name); } - } catch (final JSONException e) { + } catch (final NullPointerException e) { Log.e("OkapiClient.parseAttributes", e); } } @@ -624,27 +629,29 @@ final class OkapiClient { cache.setCoords(new Geopoint(latitude, longitude)); } - private static CacheSize getCacheSize(final JSONObject response) { - if (response.isNull(CACHE_SIZE2)) { + @NonNull + private static CacheSize getCacheSize(final ObjectNode response) { + if (!response.has(CACHE_SIZE2)) { return getCacheSizeDeprecated(response); } try { - final String size = response.getString(CACHE_SIZE2); + final String size = response.get(CACHE_SIZE2).asText(); return CacheSize.getById(size); - } catch (final JSONException e) { + } catch (final NullPointerException e) { Log.e("OkapiClient.getCacheSize", e); return getCacheSizeDeprecated(response); } } - private static CacheSize getCacheSizeDeprecated(final JSONObject response) { - if (response.isNull(CACHE_SIZE_DEPRECATED)) { + @NonNull + private static CacheSize getCacheSizeDeprecated(final ObjectNode response) { + if (!response.has(CACHE_SIZE_DEPRECATED)) { return CacheSize.NOT_CHOSEN; } double size = 0; try { - size = response.getDouble(CACHE_SIZE_DEPRECATED); - } catch (final JSONException e) { + size = response.get(CACHE_SIZE_DEPRECATED).asDouble(); + } catch (final NullPointerException e) { Log.e("OkapiClient.getCacheSize", e); } switch ((int) Math.round(size)) { @@ -692,12 +699,7 @@ final class OkapiClient { return CacheType.UNKNOWN; } - private static String getCoreFields(final OCApiConnector connector) { - if (connector == null) { - Log.e("OkapiClient.getCoreFields called with invalid connector"); - return StringUtils.EMPTY; - } - + private static String getCoreFields(@NonNull final OCApiConnector connector) { if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { return SERVICE_CACHE_CORE_FIELDS + SEPARATOR + SERVICE_CACHE_CORE_L3_FIELDS; } @@ -705,12 +707,7 @@ final class OkapiClient { return SERVICE_CACHE_CORE_FIELDS; } - private static String getFullFields(final OCApiConnector connector) { - if (connector == null) { - Log.e("OkapiClient.getFullFields called with invalid connector"); - return StringUtils.EMPTY; - } - + private static String getFullFields(@NonNull final OCApiConnector connector) { final StringBuilder res = new StringBuilder(500); res.append(SERVICE_CACHE_CORE_FIELDS); @@ -730,21 +727,20 @@ final class OkapiClient { } @NonNull - private static JSONResult request(final OCApiConnector connector, final OkapiService service, final Parameters params) { - if (connector == null) { - return new JSONResult(null); - } - + private static JSONResult request(@NonNull final OCApiConnector connector, final OkapiService service, final Parameters params) { final String host = connector.getHost(); if (StringUtils.isBlank(host)) { - return new JSONResult(null); + return new JSONResult("unknown OKAPI connector host"); } params.add("langpref", getPreferredLanguage()); if (connector.getSupportedAuthLevel() == OAuthLevel.Level3) { - final ImmutablePair<String, String> tokens = Settings.getTokenPair(connector.getTokenPublicPrefKeyId(), connector.getTokenSecretPrefKeyId()); - OAuth.signOAuth(host, service.methodName, "GET", false, params, tokens.left, tokens.right, connector.getCK(), connector.getCS()); + final OAuthTokens tokens = new OAuthTokens(connector); + if (!tokens.isValid()) { + return new JSONResult("invalid oauth tokens"); + } + OAuth.signOAuth(host, service.methodName, "GET", false, params, tokens, connector.getCK(), connector.getCS()); } else { connector.addAuthentication(params); } @@ -754,14 +750,14 @@ final class OkapiClient { } private static String getPreferredLanguage() { - final String code = Locale.getDefault().getCountry(); + final String code = Locale.getDefault().getLanguage(); if (StringUtils.isNotBlank(code)) { return StringUtils.lowerCase(code) + "|en"; } return "en"; } - private static void addFilterParams(final Map<String, String> valueMap, final OCApiConnector connector, final boolean my) { + private static void addFilterParams(final Map<String, String> valueMap, @NonNull final OCApiConnector connector, final boolean my) { if (!Settings.isExcludeDisabledCaches()) { valueMap.put("status", "Available|Temporarily unavailable"); } @@ -774,7 +770,7 @@ final class OkapiClient { } } - private static void addRetrieveParams(final Parameters params, final OCApiConnector connector) { + private static void addRetrieveParams(final Parameters params, @NonNull final OCApiConnector connector) { params.add("retr_method", METHOD_RETRIEVE_CACHES); params.add("retr_params", "{\"fields\": \"" + getCoreFields(connector) + "\"}"); params.add("wrap", "true"); @@ -800,7 +796,7 @@ final class OkapiClient { } public static @Nullable - String getUserUUID(final OCApiConnector connector, final String userName) { + String getUserUUID(@NonNull final OCApiConnector connector, final String userName) { final Parameters params = new Parameters("fields", USER_UUID, USER_USERNAME, userName); final JSONResult result = request(connector, OkapiService.SERVICE_USER_BY_USERNAME, params); @@ -810,16 +806,7 @@ final class OkapiClient { return null; } - final JSONObject data = result.data; - if (!data.isNull(USER_UUID)) { - try { - return data.getString(USER_UUID); - } catch (final JSONException e) { - Log.e("OkapiClient.getUserUUID - uuid", e); - } - } - - return null; + return result.data.path(USER_UUID).asText(null); } public static UserInfo getUserInfo(final OCApiLiveConnector connector) { @@ -833,31 +820,11 @@ final class OkapiClient { return new UserInfo(StringUtils.EMPTY, 0, UserInfoStatus.getFromOkapiError(error.getResult())); } - final JSONObject data = result.data; - - String name = StringUtils.EMPTY; - boolean successUserName = false; - - if (!data.isNull(USER_USERNAME)) { - try { - name = data.getString(USER_USERNAME); - successUserName = true; - } catch (final JSONException e) { - Log.e("OkapiClient.getUserInfo - name", e); - } - } - - int finds = 0; - boolean successFinds = false; - - if (!data.isNull(USER_CACHES_FOUND)) { - try { - finds = data.getInt(USER_CACHES_FOUND); - successFinds = true; - } catch (final JSONException e) { - Log.e("OkapiClient.getUserInfo - finds", e); - } - } + final ObjectNode data = result.data; + final boolean successUserName = data.has(USER_USERNAME); + final String name = data.path(USER_USERNAME).asText(); + final boolean successFinds = data.has(USER_CACHES_FOUND); + final int finds = data.path(USER_CACHES_FOUND).asInt(); return new UserInfo(name, finds, successUserName && successFinds ? UserInfoStatus.SUCCESSFUL : UserInfoStatus.FAILED); } @@ -874,7 +841,7 @@ final class OkapiClient { if (!result.isSuccess) { return new OkapiError(result.data); } - return new OkapiError(new JSONObject()); + return new OkapiError(new ObjectNode(JsonUtils.factory)); } /** @@ -884,21 +851,27 @@ final class OkapiClient { private static class JSONResult { public final boolean isSuccess; - public final JSONObject data; + public final ObjectNode data; public JSONResult(final @Nullable HttpResponse response) { - final boolean isSuccess = Network.isSuccess(response); + final boolean isRequestSuccessful = Network.isSuccess(response); final String responseData = Network.getResponseDataAlways(response); - JSONObject data = null; + ObjectNode tempData = null; if (responseData != null) { try { - data = new JSONObject(responseData); - } catch (final JSONException e) { + tempData = (ObjectNode) JsonUtils.reader.readTree(responseData); + } catch (IOException | ClassCastException e) { Log.w("JSONResult", e); } } - this.data = data; - this.isSuccess = isSuccess && data != null; + data = tempData; + isSuccess = isRequestSuccessful && tempData != null; + } + + public JSONResult(final @NonNull String errorMessage) { + isSuccess = false; + data = new ObjectNode(JsonUtils.factory); + data.putObject("error").put("developer_message", errorMessage); } } } diff --git a/main/src/cgeo/geocaching/connector/oc/OkapiError.java b/main/src/cgeo/geocaching/connector/oc/OkapiError.java index 7faf2c7..f3234f8 100644 --- a/main/src/cgeo/geocaching/connector/oc/OkapiError.java +++ b/main/src/cgeo/geocaching/connector/oc/OkapiError.java @@ -2,11 +2,11 @@ package cgeo.geocaching.connector.oc; import cgeo.geocaching.utils.Log; +import com.fasterxml.jackson.databind.node.ObjectNode; + import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.json.JSONException; -import org.json.JSONObject; /** * Handles the JSON error response from OKAPI @@ -26,7 +26,7 @@ public class OkapiError { @NonNull private final OkapiErrors state; @NonNull private final String message; - public OkapiError(@Nullable JSONObject data) { + public OkapiError(@Nullable final ObjectNode data) { // A null-response is by definition an error (some exception occurred somewhere in the flow) if (data == null) { @@ -39,10 +39,10 @@ public class OkapiError { String localmessage = null; OkapiErrors localstate = OkapiErrors.UNSPECIFIED; try { - JSONObject error = data.getJSONObject("error"); + final ObjectNode error = (ObjectNode) data.get("error"); // Check reason_stack element to look for the specific oauth problems we want to report back if (error.has("reason_stack")) { - String reason = error.getString("reason_stack"); + final String reason = error.get("reason_stack").asText(); if (StringUtils.contains(reason, "invalid_oauth_request")) { if (StringUtils.contains(reason, "invalid_timestamp")) { localstate = OkapiErrors.INVALID_TIMESTAMP; @@ -53,10 +53,10 @@ public class OkapiError { } // Check if we can extract a message as well if (error.has("developer_message")) { - localmessage = error.getString("developer_message"); + localmessage = error.get("developer_message").asText(); assert localmessage != null; // by virtue of defaultString } - } catch (JSONException ex) { + } catch (ClassCastException | NullPointerException ex) { Log.d("OkapiError: Failed to parse JSON", ex); localstate = OkapiErrors.UNSPECIFIED; } diff --git a/main/src/cgeo/geocaching/connector/oc/OkapiLoggingManager.java b/main/src/cgeo/geocaching/connector/oc/OkapiLoggingManager.java index 51c8a7e..76e597c 100644 --- a/main/src/cgeo/geocaching/connector/oc/OkapiLoggingManager.java +++ b/main/src/cgeo/geocaching/connector/oc/OkapiLoggingManager.java @@ -9,6 +9,9 @@ import cgeo.geocaching.connector.LogResult; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.StatusCode; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import android.net.Uri; import java.util.Calendar; @@ -19,7 +22,7 @@ public class OkapiLoggingManager extends AbstractLoggingManager { private final OCApiLiveConnector connector; private final Geocache cache; - private LogCacheActivity activity; + private final LogCacheActivity activity; private boolean hasLoaderError = true; public OkapiLoggingManager(final LogCacheActivity activity, final OCApiLiveConnector connector, final Geocache cache) { @@ -37,18 +40,21 @@ public class OkapiLoggingManager extends AbstractLoggingManager { } @Override - public final LogResult postLog(final Geocache cache, final LogType logType, final Calendar date, final String log, final String logPassword, final List<TrackableLog> trackableLogs) { + @NonNull + public final LogResult postLog(@NonNull final LogType logType, @NonNull final Calendar date, @NonNull final String log, @Nullable final String logPassword, @NonNull final List<TrackableLog> trackableLogs) { final LogResult result = OkapiClient.postLog(cache, logType, date, log, logPassword, connector); connector.login(null, null); return result; } @Override + @NonNull public final ImageResult postLogImage(final String logId, final String imageCaption, final String imageDescription, final Uri imageUri) { return new ImageResult(StatusCode.LOG_POST_ERROR, ""); } @Override + @NonNull public List<LogType> getPossibleLogTypes() { if (hasLoaderError) { return Collections.emptyList(); diff --git a/main/src/cgeo/geocaching/connector/oc/UserInfo.java b/main/src/cgeo/geocaching/connector/oc/UserInfo.java index c8b37cd..42249b0 100644 --- a/main/src/cgeo/geocaching/connector/oc/UserInfo.java +++ b/main/src/cgeo/geocaching/connector/oc/UserInfo.java @@ -15,11 +15,11 @@ public class UserInfo { public final int resId; - UserInfoStatus(int resId) { + UserInfoStatus(final int resId) { this.resId = resId; } - public static UserInfoStatus getFromOkapiError(OkapiErrors result) { + public static UserInfoStatus getFromOkapiError(final OkapiErrors result) { switch (result) { case NO_ERROR: return SUCCESSFUL; @@ -37,7 +37,7 @@ public class UserInfo { private final int finds; private final UserInfoStatus status; - UserInfo(String name, int finds, UserInfoStatus status) { + UserInfo(final String name, final int finds, final UserInfoStatus status) { this.name = name; this.finds = finds; this.status = status; diff --git a/main/src/cgeo/geocaching/connector/ox/OXConnector.java b/main/src/cgeo/geocaching/connector/ox/OXConnector.java index 7d4cf7f..d1db301 100644 --- a/main/src/cgeo/geocaching/connector/ox/OXConnector.java +++ b/main/src/cgeo/geocaching/connector/ox/OXConnector.java @@ -1,7 +1,6 @@ package cgeo.geocaching.connector.ox; import cgeo.geocaching.Geocache; -import cgeo.geocaching.ICache; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.AbstractConnector; import cgeo.geocaching.connector.capability.ISearchByCenter; @@ -9,12 +8,13 @@ import cgeo.geocaching.connector.capability.ISearchByGeocode; import cgeo.geocaching.connector.capability.ISearchByKeyword; import cgeo.geocaching.connector.capability.ISearchByViewPort; import cgeo.geocaching.connector.gc.MapTokens; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.loaders.RecaptchaReceiver; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.CancellableHandler; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -30,33 +30,37 @@ public class OXConnector extends AbstractConnector implements ISearchByCenter, I private static final Pattern PATTERN_GEOCODE = Pattern.compile("OX[A-Z0-9]+", Pattern.CASE_INSENSITIVE); @Override - public boolean canHandle(@NonNull String geocode) { + public boolean canHandle(@NonNull final String geocode) { return PATTERN_GEOCODE.matcher(geocode).matches(); } @Override - public String getCacheUrl(@NonNull Geocache cache) { + @NonNull + public String getCacheUrl(@NonNull final Geocache cache) { return getCacheUrlPrefix() + cache.getGeocode(); } @Override + @NonNull public String getName() { return "OpenCaching.com"; } @Override + @NonNull public String getHost() { return "www.opencaching.com"; } @Override - public String getLicenseText(@NonNull Geocache cache) { + @NonNull + public String getLicenseText(@NonNull final Geocache cache) { // NOT TO BE TRANSLATED return "<a href=\"" + getCacheUrl(cache) + "\">" + getName() + "</a> data licensed under the Creative Commons CC-BY-SA 3.0 License"; } @Override - public boolean isOwner(final ICache cache) { + public boolean isOwner(@NonNull final Geocache cache) { return false; } @@ -74,17 +78,19 @@ public class OXConnector extends AbstractConnector implements ISearchByCenter, I } @Override - public SearchResult searchByCenter(@NonNull Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { + public SearchResult searchByCenter(@NonNull final Geopoint center, final @NonNull RecaptchaReceiver recaptchaReceiver) { return createSearchResult(OpenCachingApi.searchByCenter(center)); } @Override + @NonNull protected String getCacheUrlPrefix() { return "http://www.opencaching.com/#!geocache/"; } @Override - public SearchResult searchByViewport(@NonNull Viewport viewport, final MapTokens tokens) { + @NonNull + public SearchResult searchByViewport(@NonNull final Viewport viewport, @NonNull final MapTokens tokens) { return createSearchResult(OpenCachingApi.searchByBoundingBox(viewport)); } @@ -98,10 +104,20 @@ public class OXConnector extends AbstractConnector implements ISearchByCenter, I return createSearchResult(OpenCachingApi.searchByKeyword(name)); } - private static SearchResult createSearchResult(Collection<Geocache> caches) { + private static SearchResult createSearchResult(final Collection<Geocache> caches) { if (caches == null) { return null; } return new SearchResult(caches); } + + @Override + @Nullable + public String getGeocodeFromUrl(@NonNull final String url) { + final String geocode = StringUtils.substringAfter(url, "http://www.opencaching.com/de/#!geocache/"); + if (canHandle(geocode)) { + return geocode; + } + return super.getGeocodeFromUrl(url); + } } diff --git a/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java b/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java index 7896826..25f66f4 100644 --- a/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java +++ b/main/src/cgeo/geocaching/connector/ox/OXGPXParser.java @@ -29,7 +29,6 @@ public class OXGPXParser extends GPX10Parser { * The short description of OX caches contains "title by owner, type(T/D/Awesomeness)". That is a lot of * duplication. Additionally a space between type and (T/D/Awesomeness) is introduced. * - * @param cache */ private static void removeTitleFromShortDescription(final @NonNull Geocache cache) { cache.setShortDescription(StringUtils.replace(StringUtils.trim(StringUtils.substringAfterLast(cache.getShortDescription(), ",")), "(", " (")); diff --git a/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java b/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java index b2afff5..a12a7fb 100644 --- a/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java +++ b/main/src/cgeo/geocaching/connector/ox/OpenCachingApi.java @@ -2,10 +2,10 @@ package cgeo.geocaching.connector.ox; import cgeo.geocaching.Geocache; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; @@ -39,19 +39,19 @@ public class OpenCachingApi { return null; } - private static HttpResponse getRequest(String string, Parameters parameters) { + private static HttpResponse getRequest(final String uri, final Parameters parameters) { parameters.add("Authorization", DEV_KEY); - return Network.getRequest(string, parameters); + return Network.getRequest(uri, parameters); } private static Collection<Geocache> importCachesFromResponse(final HttpResponse response, final boolean isDetailed) { if (response == null) { return Collections.emptyList(); } - Collection<Geocache> caches; + final Collection<Geocache> caches; try { - caches = new OXGPXParser(StoredList.TEMPORARY_LIST_ID, isDetailed).parse(response.getEntity().getContent(), null); - } catch (Exception e) { + caches = new OXGPXParser(StoredList.TEMPORARY_LIST.id, isDetailed).parse(response.getEntity().getContent(), null); + } catch (final Exception e) { Log.e("Error importing from OpenCaching.com", e); return Collections.emptyList(); } @@ -81,10 +81,10 @@ public class OpenCachingApi { * Parameters to modify * @return True - query possible, False - no query, all caches excluded */ - private static boolean addTypeFilter(Parameters queryParameters) { + private static boolean addTypeFilter(final Parameters queryParameters) { boolean doQuery = true; if (Settings.getCacheType() != CacheType.ALL) { - String typeFilter; + final String typeFilter; switch (Settings.getCacheType()) { case TRADITIONAL: typeFilter = "Traditional Cache"; diff --git a/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java b/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java index fb554b9..03549d1 100644 --- a/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/AbstractTrackableConnector.java @@ -1,5 +1,6 @@ package cgeo.geocaching.connector.trackable; +import cgeo.geocaching.Trackable; import cgeo.geocaching.connector.AbstractConnector; import cgeo.geocaching.connector.UserAction; @@ -16,14 +17,25 @@ public abstract class AbstractTrackableConnector implements TrackableConnector { } @Override - public @Nullable - String getTrackableCodeFromUrl(@NonNull String url) { + public boolean hasTrackableUrls() { + return true; + } + + @Override + @Nullable + public String getTrackableCodeFromUrl(@NonNull final String url) { return null; } @Override - public @NonNull - List<UserAction> getUserActions() { + @NonNull + public List<UserAction> getUserActions() { return AbstractConnector.getDefaultUserActions(); } + + @Override + @NonNull + public String getUrl(@NonNull final Trackable trackable) { + throw new IllegalStateException("this trackable does not have a corresponding URL"); + } } diff --git a/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java b/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java index 03052f9..6f3749c 100644 --- a/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/GeokretyConnector.java @@ -15,17 +15,19 @@ public class GeokretyConnector extends AbstractTrackableConnector { private static final Pattern PATTERN_GK_CODE = Pattern.compile("GK[0-9A-F]{4,}"); @Override - public boolean canHandleTrackable(String geocode) { + public boolean canHandleTrackable(final String geocode) { return geocode != null && PATTERN_GK_CODE.matcher(geocode).matches(); } @Override - public String getUrl(Trackable trackable) { + @NonNull + public String getUrl(@NonNull final Trackable trackable) { return "http://geokrety.org/konkret.php?id=" + getId(trackable.getGeocode()); } @Override - public Trackable searchTrackable(String geocode, String guid, String id) { + @Nullable + public Trackable searchTrackable(final String geocode, final String guid, final String id) { final String page = Network.getResponseData(Network.getRequest("http://geokrety.org/export2.php?gkid=" + getId(geocode))); if (page == null) { return null; @@ -33,32 +35,35 @@ public class GeokretyConnector extends AbstractTrackableConnector { return GeokretyParser.parse(page); } - protected static int getId(String geocode) { + protected static int getId(final String geocode) { try { final String hex = geocode.substring(2); return Integer.parseInt(hex, 16); } catch (final NumberFormatException e) { - Log.e("Trackable.getUrl", e); + Log.e("Trackable.getId", e); } return -1; } @Override public @Nullable - String getTrackableCodeFromUrl(@NonNull String url) { + String getTrackableCodeFromUrl(@NonNull final String url) { // http://geokrety.org/konkret.php?id=38545 String id = StringUtils.substringAfterLast(url, "konkret.php?id="); if (StringUtils.isNumeric(id)) { return geocode(Integer.parseInt(id)); } + // http://geokretymap.org/38545 + id = StringUtils.substringAfterLast(url, "geokretymap.org/"); + if (StringUtils.isNumeric(id)) { + return geocode(Integer.parseInt(id)); + } return null; } /** * Get geocode from geokrety id * - * @param id - * @return */ public static String geocode(final int id) { return String.format("GK%04X", id); diff --git a/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java b/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java index 0e64abd..a6e63ca 100644 --- a/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java +++ b/main/src/cgeo/geocaching/connector/trackable/GeokretyParser.java @@ -26,7 +26,7 @@ public class GeokretyParser { geokret.setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String name) { + public void end(final String name) { trackable.setName(name); } }); @@ -34,7 +34,7 @@ public class GeokretyParser { geokret.setStartElementListener(new StartElementListener() { @Override - public void start(Attributes attributes) { + public void start(final Attributes attributes) { try { final String kretyId = attributes.getValue("id"); if (StringUtils.isNumeric(kretyId)) { @@ -73,7 +73,7 @@ public class GeokretyParser { return null; } - protected static String getType(int type) { + protected static String getType(final int type) { switch (type) { case 0: return CgeoApplication.getInstance().getString(R.string.geokret_type_traditional); diff --git a/main/src/cgeo/geocaching/connector/trackable/SwaggieConnector.java b/main/src/cgeo/geocaching/connector/trackable/SwaggieConnector.java new file mode 100644 index 0000000..dcd618c --- /dev/null +++ b/main/src/cgeo/geocaching/connector/trackable/SwaggieConnector.java @@ -0,0 +1,51 @@ +package cgeo.geocaching.connector.trackable; + +import cgeo.geocaching.Trackable; +import cgeo.geocaching.network.Network; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.regex.Pattern; + +public final class SwaggieConnector extends AbstractTrackableConnector { + + private static final Pattern PATTERN_SW_CODE = Pattern.compile("SW[0-9]{4}"); + + @Override + public boolean canHandleTrackable(final String geocode) { + return geocode != null && PATTERN_SW_CODE.matcher(geocode).matches(); + } + + @Override + @NonNull + public String getUrl(@NonNull final Trackable trackable) { + return getUrl(trackable.getGeocode()); + } + + @Override + @Nullable + public Trackable searchTrackable(final String geocode, final String guid, final String id) { + final String page = Network.getResponseData(Network.getRequest(getUrl(geocode))); + if (page == null) { + return null; + } + return SwaggieParser.parse(page); + } + + @Override + @Nullable + public String getTrackableCodeFromUrl(@NonNull final String url) { + final String geocode = StringUtils.upperCase(StringUtils.substringAfterLast(url, "swaggie/")); + if (canHandleTrackable(geocode)) { + return geocode; + } + return null; + } + + private static String getUrl(final String geocode) { + return "http://geocaching.com.au/swaggie/" + geocode; + } + +} diff --git a/main/src/cgeo/geocaching/connector/trackable/SwaggieParser.java b/main/src/cgeo/geocaching/connector/trackable/SwaggieParser.java new file mode 100644 index 0000000..1883056 --- /dev/null +++ b/main/src/cgeo/geocaching/connector/trackable/SwaggieParser.java @@ -0,0 +1,55 @@ +package cgeo.geocaching.connector.trackable; + +import cgeo.geocaching.Trackable; +import cgeo.geocaching.utils.TextUtils; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.regex.Pattern; + +final class SwaggieParser { + + private SwaggieParser() { + // utility class + } + + private static final Pattern PATTERN_NAME = Pattern.compile(Pattern.quote("<h1><a") + ".*?>(.*?)<"); + private static final Pattern PATTERN_GEOCODE = Pattern.compile(Pattern.quote("'/swaggie/") + "(.*?)'"); + private static final Pattern PATTERN_DESCRIPTION = Pattern.compile(Pattern.quote("'swaggie_description'>") + "(.*?)</div"); + private static final Pattern PATTERN_OWNER = Pattern.compile(">([^<]*?)</a> released"); + + @Nullable + public static Trackable parse(@NonNull final String page) { + final Trackable trackable = new Trackable(); + final String name = TextUtils.getMatch(page, PATTERN_NAME, null); + if (StringUtils.isEmpty(name)) { + return null; + } + trackable.setName(name); + + final String geocode = TextUtils.getMatch(page, PATTERN_GEOCODE, null); + if (StringUtils.isEmpty(geocode)) { + return null; + } + trackable.setGeocode(geocode); + + final String description = StringUtils.trim(TextUtils.getMatch(page, PATTERN_DESCRIPTION, StringUtils.EMPTY)); + if (StringUtils.isEmpty(description)) { + return null; + } + trackable.setDetails(description); + + final String owner = StringUtils.trim(TextUtils.getMatch(page, PATTERN_OWNER, StringUtils.EMPTY)); + if (StringUtils.isEmpty(owner)) { + return null; + } + trackable.setOwner(owner); + + trackable.setType("Swaggie"); + + return trackable; + } + +} diff --git a/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java b/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java index 6071b5f..aea60e6 100644 --- a/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/TrackableConnector.java @@ -16,16 +16,33 @@ public interface TrackableConnector { public boolean canHandleTrackable(final String geocode); - public String getUrl(final Trackable trackable); + /** + * Check whether the connector has URLs corresponding the the trackable. + * + * @return <tt>true</tt> if the connector handles URLs, <tt>false</tt> otherwise + */ + public boolean hasTrackableUrls(); + + /** + * Return the URL for a trackable. Might throw {@link IllegalStateException} if called + * on a connector which does not have URLs for trackables. This might be checked using + * {@link #hasTrackableUrls()}. + * + * @param trackable the trackable + * @return the URL corresponding to this trackable + */ + @NonNull + public String getUrl(@NonNull final Trackable trackable); public boolean isLoggable(); + @Nullable public Trackable searchTrackable(String geocode, String guid, String id); - public @Nullable - String getTrackableCodeFromUrl(final @NonNull String url); + @Nullable + public String getTrackableCodeFromUrl(final @NonNull String url); - public @NonNull - List<UserAction> getUserActions(); + @NonNull + public List<UserAction> getUserActions(); } diff --git a/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java b/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java index 77848d7..665ebea 100644 --- a/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/TravelBugConnector.java @@ -20,12 +20,13 @@ public class TravelBugConnector extends AbstractTrackableConnector { private final static Pattern PATTERN_TB_CODE = Pattern.compile("(TB[0-9A-Z]+)|([0-9A-Z]{6})", Pattern.CASE_INSENSITIVE); @Override - public boolean canHandleTrackable(String geocode) { + public boolean canHandleTrackable(final String geocode) { return TravelBugConnector.PATTERN_TB_CODE.matcher(geocode).matches() && !StringUtils.startsWithIgnoreCase(geocode, "GC"); } @Override - public String getUrl(Trackable trackable) { + @NonNull + public String getUrl(@NonNull final Trackable trackable) { return "http://www.geocaching.com//track/details.aspx?tracker=" + trackable.getGeocode(); } @@ -35,7 +36,8 @@ public class TravelBugConnector extends AbstractTrackableConnector { } @Override - public Trackable searchTrackable(String geocode, String guid, String id) { + @Nullable + public Trackable searchTrackable(final String geocode, final String guid, final String id) { return GCParser.searchTrackable(geocode, guid, id); } @@ -56,15 +58,15 @@ public class TravelBugConnector extends AbstractTrackableConnector { @Override public @Nullable - String getTrackableCodeFromUrl(@NonNull String url) { + String getTrackableCodeFromUrl(@NonNull final String url) { // coord.info URLs - String code = StringUtils.substringAfterLast(url, "coord.info/"); - if (code != null && canHandleTrackable(code)) { - return code; + final String code1 = StringUtils.substringAfterLast(url, "coord.info/"); + if (canHandleTrackable(code1)) { + return code1; } - code = StringUtils.substringAfterLast(url, "?tracker="); - if (code != null && canHandleTrackable(code)) { - return code; + final String code2 = StringUtils.substringAfterLast(url, "?tracker="); + if (canHandleTrackable(code2)) { + return code2; } return null; } diff --git a/main/src/cgeo/geocaching/connector/trackable/UnknownTrackableConnector.java b/main/src/cgeo/geocaching/connector/trackable/UnknownTrackableConnector.java index 0295927..885df7e 100644 --- a/main/src/cgeo/geocaching/connector/trackable/UnknownTrackableConnector.java +++ b/main/src/cgeo/geocaching/connector/trackable/UnknownTrackableConnector.java @@ -2,22 +2,23 @@ package cgeo.geocaching.connector.trackable; import cgeo.geocaching.Trackable; -import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.Nullable; public class UnknownTrackableConnector extends AbstractTrackableConnector { @Override - public boolean canHandleTrackable(String geocode) { + public boolean canHandleTrackable(final String geocode) { return false; } @Override - public String getUrl(Trackable trackable) { - return StringUtils.EMPTY; + public boolean hasTrackableUrls() { + return false; } @Override - public Trackable searchTrackable(String geocode, String guid, String id) { + @Nullable + public Trackable searchTrackable(final String geocode, final String guid, final String id) { return null; } diff --git a/main/src/cgeo/geocaching/enumerations/CacheAttribute.java b/main/src/cgeo/geocaching/enumerations/CacheAttribute.java index 1fdb0ac..770b63b 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheAttribute.java +++ b/main/src/cgeo/geocaching/enumerations/CacheAttribute.java @@ -4,10 +4,11 @@ import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import android.util.SparseArray; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -129,19 +130,22 @@ public enum CacheAttribute { // THIS LIST IS GENERATED: don't change anything here but read // project/attributes/readme.txt + @NonNull private static final String INTERNAL_YES = "_yes"; + @NonNull private static final String INTERNAL_NO = "_no"; public static final int NO_ID = -1; public final int gcid; public final int ocacode; + @NonNull public final String rawName; public final int drawableId; public final int stringIdYes; public final int stringIdNo; - CacheAttribute(final int gcid, final int ocacode, final String rawName, + CacheAttribute(final int gcid, final int ocacode, @NonNull final String rawName, final int drawableId, final int stringIdYes, final int stringIdNo) { this.gcid = gcid; this.ocacode = ocacode; @@ -158,44 +162,45 @@ public enum CacheAttribute { * true: for positive text, false: for negative text * @return the localized text */ + @NonNull public String getL10n(final boolean enabled) { return CgeoApplication.getInstance().getResources().getString( enabled ? stringIdYes : stringIdNo); } - private final static Map<String, CacheAttribute> FIND_BY_GCRAWNAME; + @NonNull + private final static Map<String, CacheAttribute> FIND_BY_GCRAWNAME = new HashMap<>(); + @NonNull private final static SparseArray<CacheAttribute> FIND_BY_OCACODE = new SparseArray<>(); static { - final HashMap<String, CacheAttribute> mapGcRawNames = new HashMap<>(); - for (CacheAttribute attr : values()) { - mapGcRawNames.put(attr.rawName, attr); + for (final CacheAttribute attr : values()) { + FIND_BY_GCRAWNAME.put(attr.rawName, attr); if (attr.ocacode != NO_ID) { FIND_BY_OCACODE.put(attr.ocacode, attr); } } - FIND_BY_GCRAWNAME = Collections.unmodifiableMap(mapGcRawNames); } - public static CacheAttribute getByRawName(final String rawName) { + @Nullable + public static CacheAttribute getByRawName(@Nullable final String rawName) { return rawName != null ? FIND_BY_GCRAWNAME.get(rawName) : null; } + @Nullable public static CacheAttribute getByOcACode(final int ocAcode) { return FIND_BY_OCACODE.get(ocAcode); } - public static String trimAttributeName(String attributeName) { + @NonNull + public static String trimAttributeName(@Nullable final String attributeName) { if (null == attributeName) { return ""; } return attributeName.replace(INTERNAL_YES, "").replace(INTERNAL_NO, "").trim(); } - public static boolean isEnabled(final String attributeName) { + public static boolean isEnabled(@Nullable final String attributeName) { return !StringUtils.endsWithIgnoreCase(attributeName, INTERNAL_NO); } - public String getAttributeName(final boolean yes) { - return rawName + (yes ? INTERNAL_YES : INTERNAL_NO); - } } diff --git a/main/src/cgeo/geocaching/enumerations/CacheListType.java b/main/src/cgeo/geocaching/enumerations/CacheListType.java index 1fce282..297e1be 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheListType.java +++ b/main/src/cgeo/geocaching/enumerations/CacheListType.java @@ -2,6 +2,8 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.loaders.AbstractSearchLoader.CacheListLoaderType; +import org.eclipse.jdt.annotation.NonNull; + public enum CacheListType { OFFLINE(true, CacheListLoaderType.OFFLINE), POCKET(false, CacheListLoaderType.POCKET), @@ -19,9 +21,9 @@ public enum CacheListType { */ public final boolean canSwitch; - public final CacheListLoaderType loaderType; + @NonNull public final CacheListLoaderType loaderType; - CacheListType(final boolean canSwitch, final CacheListLoaderType loaderType) { + CacheListType(final boolean canSwitch, @NonNull final CacheListLoaderType loaderType) { this.canSwitch = canSwitch; this.loaderType = loaderType; } diff --git a/main/src/cgeo/geocaching/enumerations/CacheSize.java b/main/src/cgeo/geocaching/enumerations/CacheSize.java index c4e2831..10c8c9f 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheSize.java +++ b/main/src/cgeo/geocaching/enumerations/CacheSize.java @@ -3,7 +3,9 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; -import java.util.Collections; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -23,6 +25,7 @@ public enum CacheSize { OTHER("Other", 8, R.string.cache_size_other, "other"), UNKNOWN("Unknown", -1, R.string.cache_size_unknown, ""); // CacheSize not init. yet + @NonNull public final String id; public final int comparable; private final int stringId; @@ -31,26 +34,26 @@ public enum CacheSize { */ private final String ocSize2; - CacheSize(final String id, final int comparable, final int stringId, final String ocSize2) { + CacheSize(@NonNull final String id, final int comparable, final int stringId, final String ocSize2) { this.id = id; this.comparable = comparable; this.stringId = stringId; this.ocSize2 = ocSize2; } - final private static Map<String, CacheSize> FIND_BY_ID; + @NonNull + final private static Map<String, CacheSize> FIND_BY_ID = new HashMap<>(); static { - final HashMap<String, CacheSize> mapping = new HashMap<>(); for (final CacheSize cs : values()) { - mapping.put(cs.id.toLowerCase(Locale.US), cs); - mapping.put(cs.ocSize2.toLowerCase(Locale.US), cs); + FIND_BY_ID.put(cs.id.toLowerCase(Locale.US), cs); + FIND_BY_ID.put(cs.ocSize2.toLowerCase(Locale.US), cs); } // add medium as additional string for Regular - mapping.put("medium", CacheSize.REGULAR); - FIND_BY_ID = Collections.unmodifiableMap(mapping); + FIND_BY_ID.put("medium", CacheSize.REGULAR); } - public static CacheSize getById(final String id) { + @NonNull + public static CacheSize getById(@Nullable final String id) { if (id == null) { return UNKNOWN; } @@ -69,10 +72,8 @@ public enum CacheSize { /** * Bad GPX files can contain the container size encoded as number. - * - * @param id - * @return */ + @NonNull private static CacheSize getByNumber(final String id) { try { final int numerical = Integer.parseInt(id); @@ -83,12 +84,13 @@ public enum CacheSize { } } } - } catch (final NumberFormatException e) { + } catch (final NumberFormatException ignored) { // ignore, as this might be a number or not } return UNKNOWN; } + @NonNull public final String getL10n() { return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); } diff --git a/main/src/cgeo/geocaching/enumerations/CacheType.java b/main/src/cgeo/geocaching/enumerations/CacheType.java index 1d190e4..a6c32d9 100644 --- a/main/src/cgeo/geocaching/enumerations/CacheType.java +++ b/main/src/cgeo/geocaching/enumerations/CacheType.java @@ -1,10 +1,11 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.ICache; +import cgeo.geocaching.Geocache; import cgeo.geocaching.R; -import java.util.Collections; +import org.eclipse.jdt.annotation.NonNull; + import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -14,26 +15,26 @@ import java.util.Map; */ public enum CacheType { - TRADITIONAL("traditional", "Traditional Cache", "32bc9333-5e52-4957-b0f6-5a2c8fc7b257", R.string.traditional, R.drawable.type_traditional), - MULTI("multi", "Multi-cache", "a5f6d0ad-d2f2-4011-8c14-940a9ebf3c74", R.string.multi, R.drawable.type_multi), - MYSTERY("mystery", "Unknown Cache", "40861821-1835-4e11-b666-8d41064d03fe", R.string.mystery, R.drawable.type_mystery), - LETTERBOX("letterbox", "Letterbox hybrid", "4bdd8fb2-d7bc-453f-a9c5-968563b15d24", R.string.letterbox, R.drawable.type_letterbox), - EVENT("event", "Event Cache", "69eb8534-b718-4b35-ae3c-a856a55b0874", R.string.event, R.drawable.type_event), - MEGA_EVENT("mega", "Mega-Event Cache", "69eb8535-b718-4b35-ae3c-a856a55b0874", R.string.mega, R.drawable.type_mega), - GIGA_EVENT("giga", "Giga-Event Cache", "51420629-5739-4945-8bdd-ccfd434c0ead", R.string.giga, R.drawable.type_giga), - EARTH("earth", "Earthcache", "c66f5cf3-9523-4549-b8dd-759cd2f18db8", R.string.earth, R.drawable.type_earth), - CITO("cito", "Cache in Trash out Event", "57150806-bc1a-42d6-9cf0-538d171a2d22", R.string.cito, R.drawable.type_cito), - WEBCAM("webcam", "Webcam Cache", "31d2ae3c-c358-4b5f-8dcd-2185bf472d3d", R.string.webcam, R.drawable.type_webcam), - VIRTUAL("virtual", "Virtual Cache", "294d4360-ac86-4c83-84dd-8113ef678d7e", R.string.virtual, R.drawable.type_virtual), - WHERIGO("wherigo", "Wherigo Cache", "0544fa55-772d-4e5c-96a9-36a51ebcf5c9", R.string.wherigo, R.drawable.type_wherigo), - LOSTANDFOUND("lostfound", "Lost and Found Event Cache", "3ea6533d-bb52-42fe-b2d2-79a3424d4728", R.string.lostfound, R.drawable.type_event), // icon missing - PROJECT_APE("ape", "Project Ape Cache", "2555690d-b2bc-4b55-b5ac-0cb704c0b768", R.string.ape, R.drawable.type_ape), - GCHQ("gchq", "Groundspeak HQ", "416f2494-dc17-4b6a-9bab-1a29dd292d8c", R.string.gchq, R.drawable.type_hq), - GPS_EXHIBIT("gps", "GPS Adventures Exhibit", "72e69af2-7986-4990-afd9-bc16cbbb4ce3", R.string.gps, R.drawable.type_event), // icon missing - BLOCK_PARTY("block", "Groundspeak Block Party", "bc2f3df2-1aab-4601-b2ff-b5091f6c02e3", R.string.block, R.drawable.type_event), // icon missing - UNKNOWN("unknown", "unknown", "", R.string.unknown, R.drawable.type_unknown), + TRADITIONAL("traditional", "Traditional Cache", "32bc9333-5e52-4957-b0f6-5a2c8fc7b257", R.string.traditional, R.drawable.type_traditional, "2"), + MULTI("multi", "Multi-cache", "a5f6d0ad-d2f2-4011-8c14-940a9ebf3c74", R.string.multi, R.drawable.type_multi, "3"), + MYSTERY("mystery", "Unknown Cache", "40861821-1835-4e11-b666-8d41064d03fe", R.string.mystery, R.drawable.type_mystery, "8"), + LETTERBOX("letterbox", "Letterbox hybrid", "4bdd8fb2-d7bc-453f-a9c5-968563b15d24", R.string.letterbox, R.drawable.type_letterbox, "5"), + EVENT("event", "Event Cache", "69eb8534-b718-4b35-ae3c-a856a55b0874", R.string.event, R.drawable.type_event, "6"), + MEGA_EVENT("mega", "Mega-Event Cache", "69eb8535-b718-4b35-ae3c-a856a55b0874", R.string.mega, R.drawable.type_mega, "453"), + GIGA_EVENT("giga", "Giga-Event Cache", "51420629-5739-4945-8bdd-ccfd434c0ead", R.string.giga, R.drawable.type_giga, "7005"), + EARTH("earth", "Earthcache", "c66f5cf3-9523-4549-b8dd-759cd2f18db8", R.string.earth, R.drawable.type_earth, "137"), + CITO("cito", "Cache in Trash out Event", "57150806-bc1a-42d6-9cf0-538d171a2d22", R.string.cito, R.drawable.type_cito, "13"), + WEBCAM("webcam", "Webcam Cache", "31d2ae3c-c358-4b5f-8dcd-2185bf472d3d", R.string.webcam, R.drawable.type_webcam, "11"), + VIRTUAL("virtual", "Virtual Cache", "294d4360-ac86-4c83-84dd-8113ef678d7e", R.string.virtual, R.drawable.type_virtual, "4"), + WHERIGO("wherigo", "Wherigo Cache", "0544fa55-772d-4e5c-96a9-36a51ebcf5c9", R.string.wherigo, R.drawable.type_wherigo, "1858"), + LOSTANDFOUND("lostfound", "Lost and Found Event Cache", "3ea6533d-bb52-42fe-b2d2-79a3424d4728", R.string.lostfound, R.drawable.type_event, "3653"), // icon missing + PROJECT_APE("ape", "Project Ape Cache", "2555690d-b2bc-4b55-b5ac-0cb704c0b768", R.string.ape, R.drawable.type_ape, "2"), + GCHQ("gchq", "Groundspeak HQ", "416f2494-dc17-4b6a-9bab-1a29dd292d8c", R.string.gchq, R.drawable.type_hq, "3773"), + GPS_EXHIBIT("gps", "GPS Adventures Exhibit", "72e69af2-7986-4990-afd9-bc16cbbb4ce3", R.string.gps, R.drawable.type_event, "1304"), // icon missing + BLOCK_PARTY("block", "Groundspeak Block Party", "bc2f3df2-1aab-4601-b2ff-b5091f6c02e3", R.string.block, R.drawable.type_event, "4738"), // icon missing + UNKNOWN("unknown", "unknown", "", R.string.unknown, R.drawable.type_unknown, ""), /** No real cache type -> filter */ - ALL("all", "display all caches", "9a79e6ce-3344-409c-bbe9-496530baf758", R.string.all_types, R.drawable.type_unknown); + ALL("all", "display all caches", "9a79e6ce-3344-409c-bbe9-496530baf758", R.string.all_types, R.drawable.type_unknown, ""); /** * id field is used when for storing caches in the database. @@ -47,37 +48,39 @@ public enum CacheType { public final String guid; private final int stringId; public final int markerId; + @NonNull public final String wptTypeId; - CacheType(String id, String pattern, String guid, int stringId, int markerId) { + CacheType(final String id, final String pattern, final String guid, final int stringId, final int markerId, @NonNull final String wptTypeId) { this.id = id; this.pattern = pattern; this.guid = guid; this.stringId = stringId; this.markerId = markerId; + this.wptTypeId = wptTypeId; } - private final static Map<String, CacheType> FIND_BY_ID; - private final static Map<String, CacheType> FIND_BY_PATTERN; - private final static Map<String, CacheType> FIND_BY_GUID; + @NonNull + private final static Map<String, CacheType> FIND_BY_ID = new HashMap<>(); + @NonNull + private final static Map<String, CacheType> FIND_BY_PATTERN = new HashMap<>(); + @NonNull + private final static Map<String, CacheType> FIND_BY_GUID = new HashMap<>(); + static { - final HashMap<String, CacheType> mappingId = new HashMap<>(); - final HashMap<String, CacheType> mappingPattern = new HashMap<>(); - final HashMap<String, CacheType> mappingGuid = new HashMap<>(); - for (CacheType ct : values()) { - mappingId.put(ct.id, ct); - mappingPattern.put(ct.pattern.toLowerCase(Locale.US), ct); - mappingGuid.put(ct.guid, ct); + for (final CacheType ct : values()) { + FIND_BY_ID.put(ct.id, ct); + FIND_BY_PATTERN.put(ct.pattern.toLowerCase(Locale.US), ct); + FIND_BY_GUID.put(ct.guid, ct); } // Add old mystery type for GPX file compatibility. - mappingPattern.put("Mystery Cache".toLowerCase(Locale.US), MYSTERY); + FIND_BY_PATTERN.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); - FIND_BY_GUID = Collections.unmodifiableMap(mappingGuid); + FIND_BY_PATTERN.put("Traditional Geocache".toLowerCase(Locale.US), TRADITIONAL); + // map lab caches to the virtual type for the time being + FIND_BY_PATTERN.put("Lab Cache".toLowerCase(Locale.US), VIRTUAL); } + @NonNull public static CacheType getById(final String id) { final CacheType result = (id != null) ? CacheType.FIND_BY_ID.get(id.toLowerCase(Locale.US).trim()) : null; if (result == null) { @@ -86,6 +89,7 @@ public enum CacheType { return result; } + @NonNull public static CacheType getByPattern(final String pattern) { final CacheType result = (pattern != null) ? CacheType.FIND_BY_PATTERN.get(pattern.toLowerCase(Locale.US).trim()) : null; if (result == null) { @@ -94,6 +98,7 @@ public enum CacheType { return result; } + @NonNull public static CacheType getByGuid(final String id) { final CacheType result = (id != null) ? CacheType.FIND_BY_GUID.get(id) : null; if (result == null) { @@ -102,6 +107,7 @@ public enum CacheType { return result; } + @NonNull public final String getL10n() { return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); } @@ -118,10 +124,9 @@ public enum CacheType { /** * Whether this type contains the given cache. * - * @param cache * @return true if this is the ALL type or if this type equals the type of the cache. */ - public boolean contains(ICache cache) { + public boolean contains(final Geocache cache) { if (cache == null) { return false; } diff --git a/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java b/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java deleted file mode 100644 index 5bcaf4a..0000000 --- a/main/src/cgeo/geocaching/enumerations/LiveMapStrategy.java +++ /dev/null @@ -1,48 +0,0 @@ -package cgeo.geocaching.enumerations; - -import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.R; - -import java.util.EnumSet; - -/** - * Defines the strategy for the Live Map - */ -public interface LiveMapStrategy { - - public enum StrategyFlag { - LOAD_TILES, // 2x2 tiles filling the complete viewport - PARSE_TILES, // parse PNG images - SEARCH_NEARBY // searchByCoords() - } - - public enum Strategy { - FASTEST(1, EnumSet.of(StrategyFlag.LOAD_TILES), R.string.map_strategy_fastest), - FAST(2, EnumSet.of(StrategyFlag.LOAD_TILES, StrategyFlag.PARSE_TILES), R.string.map_strategy_fast), - AUTO(3, EnumSet.noneOf(StrategyFlag.class), R.string.map_strategy_auto), - DETAILED(4, EnumSet.allOf(StrategyFlag.class), R.string.map_strategy_detailed); - - public final int id; - public final EnumSet<StrategyFlag> flags; - private final int stringId; - - Strategy(int id, EnumSet<StrategyFlag> flags, int stringId) { - this.id = id; - this.flags = flags; - this.stringId = stringId; - } - - public static Strategy getById(final int id) { - for (Strategy strategy : Strategy.values()) { - if (strategy.id == id) { - return strategy; - } - } - return AUTO; - } - - public final String getL10n() { - return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); - } - } -} diff --git a/main/src/cgeo/geocaching/enumerations/LoadFlags.java b/main/src/cgeo/geocaching/enumerations/LoadFlags.java index c781f4b..0f08690 100644 --- a/main/src/cgeo/geocaching/enumerations/LoadFlags.java +++ b/main/src/cgeo/geocaching/enumerations/LoadFlags.java @@ -1,5 +1,7 @@ package cgeo.geocaching.enumerations; +import org.eclipse.jdt.annotation.NonNull; + import java.util.EnumSet; /** @@ -22,9 +24,9 @@ public interface LoadFlags { /** Retrieve cache from CacheCache only. Do not load from DB */ public final static EnumSet<LoadFlag> LOAD_CACHE_ONLY = EnumSet.of(LoadFlag.CACHE_BEFORE); /** Retrieve cache from CacheCache first. If not found load from DB */ - public final static EnumSet<LoadFlag> LOAD_CACHE_OR_DB = EnumSet.of(LoadFlag.CACHE_BEFORE, LoadFlag.DB_MINIMAL, LoadFlag.OFFLINE_LOG); + public final static EnumSet<LoadFlag> LOAD_CACHE_OR_DB = EnumSet.of(LoadFlag.CACHE_BEFORE, LoadFlag.DB_MINIMAL, LoadFlag.OFFLINE_LOG, LoadFlag.SPOILERS); /** Retrieve cache (minimalistic information including waypoints) from DB first. If not found load from CacheCache */ - public final static EnumSet<LoadFlag> LOAD_WAYPOINTS = EnumSet.of(LoadFlag.CACHE_AFTER, LoadFlag.DB_MINIMAL, LoadFlag.WAYPOINTS, LoadFlag.OFFLINE_LOG); + public final static EnumSet<LoadFlag> LOAD_WAYPOINTS = EnumSet.of(LoadFlag.CACHE_AFTER, LoadFlag.DB_MINIMAL, LoadFlag.WAYPOINTS, LoadFlag.OFFLINE_LOG, LoadFlag.SPOILERS); /** Retrieve cache (all stored informations) from DB only. Do not load from CacheCache */ public final static EnumSet<LoadFlag> LOAD_ALL_DB_ONLY = EnumSet.range(LoadFlag.DB_MINIMAL, LoadFlag.OFFLINE_LOG); @@ -33,6 +35,7 @@ public interface LoadFlags { DB // include saving to CacheCache } + @NonNull public final static EnumSet<SaveFlag> SAVE_ALL = EnumSet.allOf(SaveFlag.class); public enum RemoveFlag { @@ -41,6 +44,7 @@ public interface LoadFlags { OWN_WAYPOINTS_ONLY_FOR_TESTING // only to be used in unit testing (as we never delete own waypoints) } + @NonNull public final static EnumSet<RemoveFlag> REMOVE_ALL = EnumSet.of(RemoveFlag.CACHE, RemoveFlag.DB); }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/enumerations/LogType.java b/main/src/cgeo/geocaching/enumerations/LogType.java index 84ab7b9..bf0d66c 100644 --- a/main/src/cgeo/geocaching/enumerations/LogType.java +++ b/main/src/cgeo/geocaching/enumerations/LogType.java @@ -3,7 +3,9 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; -import java.util.Collections; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -46,13 +48,16 @@ public enum LogType { UNKNOWN(0, "unknown", "", "", R.string.err_unknown, R.drawable.mark_red); // LogType not init. yet public final int id; + @Nullable public final String iconName; + @NonNull public final String type; + @NonNull public final String oc_type; 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, @Nullable final String iconName, @NonNull final String type, @NonNull final String oc_type, final int stringId, final int markerId) { this.id = id; this.iconName = iconName; this.type = type; @@ -61,27 +66,24 @@ 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); } - private final static Map<String, LogType> FIND_BY_ICONNAME; - private final static Map<String, LogType> FIND_BY_TYPE; + private final static Map<String, LogType> FIND_BY_ICONNAME = new HashMap<>(); + private final static Map<String, LogType> FIND_BY_TYPE = new HashMap<>(); 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); + FIND_BY_ICONNAME.put(lt.iconName, lt); } - mappingType.put(lt.type, lt); + FIND_BY_TYPE.put(lt.type, lt); } - FIND_BY_ICONNAME = Collections.unmodifiableMap(mappingPattern); - FIND_BY_TYPE = Collections.unmodifiableMap(mappingType); } + @NonNull public static LogType getById(final int id) { - for (LogType logType : values()) { + for (final LogType logType : values()) { if (logType.id == id) { return logType; } @@ -89,6 +91,7 @@ public enum LogType { return UNKNOWN; } + @NonNull public static LogType getByIconName(final String imageType) { // Special case for post reviewer note, which appears sometimes as 18.png (in individual entries) or as 68.png // (in logs counts). @@ -102,6 +105,7 @@ public enum LogType { return result; } + @NonNull public static LogType getByType(final String type) { final LogType result = type != null ? LogType.FIND_BY_TYPE.get(type.toLowerCase(Locale.US).trim()) : null; if (result == null) { @@ -110,7 +114,16 @@ public enum LogType { return result; } + @NonNull 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/enumerations/LogTypeTrackable.java b/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java index e008294..05ce6fd 100644 --- a/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java +++ b/main/src/cgeo/geocaching/enumerations/LogTypeTrackable.java @@ -3,19 +3,22 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + public enum LogTypeTrackable { DO_NOTHING("", R.string.log_tb_nothing), VISITED("_Visited", R.string.log_tb_visit), DROPPED_OFF("_DroppedOff", R.string.log_tb_drop); - final public String action; + @NonNull final public String action; final private int resourceId; - LogTypeTrackable(String action, int resourceId) { + LogTypeTrackable(@NonNull final String action, final int resourceId) { this.action = action; this.resourceId = resourceId; } + @NonNull public String getLabel() { return CgeoApplication.getInstance().getString(resourceId); } diff --git a/main/src/cgeo/geocaching/enumerations/StatusCode.java b/main/src/cgeo/geocaching/enumerations/StatusCode.java index 8bda371..37d027f 100644 --- a/main/src/cgeo/geocaching/enumerations/StatusCode.java +++ b/main/src/cgeo/geocaching/enumerations/StatusCode.java @@ -2,6 +2,8 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import android.content.res.Resources; public enum StatusCode { @@ -21,14 +23,13 @@ public enum StatusCode { PREMIUM_ONLY(R.string.err_premium_only), MAINTENANCE(R.string.err_maintenance), LOG_POST_ERROR(R.string.err_log_post_failed), - LOG_POST_ERROR_EC(R.string.err_log_post_failed_ec), NO_LOG_TEXT(R.string.warn_log_text_fill), NOT_LOGGED_IN(R.string.init_login_popup_failed), LOGIMAGE_POST_ERROR(R.string.err_logimage_post_failed); final private int error_string; - StatusCode(int error_string) { + StatusCode(final int error_string) { this.error_string = error_string; } @@ -36,6 +37,7 @@ public enum StatusCode { return error_string; } + @NonNull public String getErrorString(final Resources res) { return res.getString(error_string); } diff --git a/main/src/cgeo/geocaching/enumerations/WaypointType.java b/main/src/cgeo/geocaching/enumerations/WaypointType.java index 1805635..d2281ef 100644 --- a/main/src/cgeo/geocaching/enumerations/WaypointType.java +++ b/main/src/cgeo/geocaching/enumerations/WaypointType.java @@ -3,6 +3,8 @@ package cgeo.geocaching.enumerations; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -22,11 +24,12 @@ public enum WaypointType { WAYPOINT("waypoint", R.string.wp_waypoint, R.drawable.waypoint_waypoint), ORIGINAL("original", R.string.wp_original, R.drawable.waypoint_waypoint); + @NonNull public final String id; public final int stringId; public final int markerId; - WaypointType(String id, int stringId, int markerId) { + WaypointType(@NonNull final String id, final int stringId, final int markerId) { this.id = id; this.stringId = stringId; this.markerId = markerId; @@ -36,34 +39,36 @@ public enum WaypointType { * inverse lookup of waypoint IDs<br/> * non public so that <code>null</code> handling can be handled centrally in the enum type itself */ - private static final Map<String, WaypointType> FIND_BY_ID; - public static final Set<WaypointType> ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL = new HashSet<>(); + private static final Map<String, WaypointType> FIND_BY_ID = new HashMap<>(); + private static final Set<WaypointType> ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL_TMP = new HashSet<>(); static { - final HashMap<String, WaypointType> mapping = new HashMap<>(); - for (WaypointType wt : values()) { - mapping.put(wt.id, wt); + for (final WaypointType wt : values()) { + FIND_BY_ID.put(wt.id, wt); if (wt != WaypointType.OWN && wt != WaypointType.ORIGINAL) { - ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL.add(wt); + ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL_TMP.add(wt); } } - FIND_BY_ID = Collections.unmodifiableMap(mapping); } + @NonNull + public static final Set<WaypointType> ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL = Collections.unmodifiableSet(ALL_TYPES_EXCEPT_OWN_AND_ORIGINAL_TMP); /** * inverse lookup of waypoint IDs<br/> * here the <code>null</code> handling shall be done */ + @NonNull public static WaypointType findById(final String id) { if (null == id) { return WAYPOINT; } - WaypointType waypointType = FIND_BY_ID.get(id); + final WaypointType waypointType = FIND_BY_ID.get(id); if (null == waypointType) { return WAYPOINT; } return waypointType; } + @NonNull public final String getL10n() { return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); } diff --git a/main/src/cgeo/geocaching/export/AbstractExport.java b/main/src/cgeo/geocaching/export/AbstractExport.java index 5d15ecb..94d0506 100644 --- a/main/src/cgeo/geocaching/export/AbstractExport.java +++ b/main/src/cgeo/geocaching/export/AbstractExport.java @@ -22,7 +22,7 @@ abstract class AbstractExport implements Export { * the resource id of the string * @return localized string */ - protected static String getString(int resourceId) { + protected static String getString(final int resourceId) { return CgeoApplication.getInstance().getString(resourceId); } @@ -35,7 +35,7 @@ abstract class AbstractExport implements Export { * The parameter * @return localized string */ - protected static String getString(int resourceId, Object... params) { + protected static String getString(final int resourceId, final Object... params) { return CgeoApplication.getInstance().getString(resourceId, params); } diff --git a/main/src/cgeo/geocaching/export/FieldNotes.java b/main/src/cgeo/geocaching/export/FieldNotes.java index 11d725a..fdc6210 100644 --- a/main/src/cgeo/geocaching/export/FieldNotes.java +++ b/main/src/cgeo/geocaching/export/FieldNotes.java @@ -41,7 +41,7 @@ class FieldNotes { return buffer.toString(); } - File writeToDirectory(File exportLocation) { + File writeToDirectory(final File exportLocation) { if (!LocalStorage.isExternalStorageAvailable()) { return null; } diff --git a/main/src/cgeo/geocaching/export/FieldnoteExport.java b/main/src/cgeo/geocaching/export/FieldnoteExport.java index c03b848..c4a6adc 100644 --- a/main/src/cgeo/geocaching/export/FieldnoteExport.java +++ b/main/src/cgeo/geocaching/export/FieldnoteExport.java @@ -59,10 +59,11 @@ public class FieldnoteExport extends AbstractExport { final AlertDialog.Builder builder = new AlertDialog.Builder(activity); final Context themedContext; - if (Settings.isLightSkin() && VERSION.SDK_INT < VERSION_CODES.HONEYCOMB) + if (Settings.isLightSkin() && VERSION.SDK_INT < VERSION_CODES.HONEYCOMB) { themedContext = new ContextThemeWrapper(activity, R.style.dark); - else + } else { themedContext = activity; + } final View layout = View.inflate(themedContext, R.layout.fieldnote_export_dialog, null); builder.setView(layout); @@ -135,8 +136,10 @@ public class FieldnoteExport extends AbstractExport { for (final Geocache cache : caches) { if (ConnectorFactory.getConnector(cache).equals(connector) && cache.isLogOffline()) { final LogEntry log = DataStore.loadLogOffline(cache.getGeocode()); - if (!onlyNew || log.date > Settings.getFieldnoteExportDate()) { - fieldNotes.add(cache, log); + if (log != null) { + if (!onlyNew || log.date > Settings.getFieldnoteExportDate()) { + fieldNotes.add(cache, log); + } } } publishProgress(++i); diff --git a/main/src/cgeo/geocaching/export/GpxExport.java b/main/src/cgeo/geocaching/export/GpxExport.java index 61d03bc..af564da 100644 --- a/main/src/cgeo/geocaching/export/GpxExport.java +++ b/main/src/cgeo/geocaching/export/GpxExport.java @@ -98,11 +98,7 @@ public class GpxExport extends AbstractExport { } private static String[] getGeocodes(final List<Geocache> caches) { - final ArrayList<String> allGeocodes = new ArrayList<>(caches.size()); - for (final Geocache geocache : caches) { - allGeocodes.add(geocache.getGeocode()); - } - return allGeocodes.toArray(new String[allGeocodes.size()]); + return Geocache.getGeocodes(caches).toArray(new String[caches.size()]); } protected class ExportTask extends AsyncTaskWithProgress<String, File> { @@ -121,8 +117,7 @@ public class GpxExport extends AbstractExport { private File getExportFile() { final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - final Date now = new Date(); - return FileUtils.getUniqueNamedFile(Settings.getGpxExportDir() + File.separatorChar + "export_" + fileNameDateFormat.format(now) + ".gpx"); + return FileUtils.getUniqueNamedFile(new File(Settings.getGpxExportDir(), "export_" + fileNameDateFormat.format(new Date()) + ".gpx")); } @Override @@ -156,7 +151,7 @@ public class GpxExport extends AbstractExport { if (writer != null) { try { writer.close(); - } catch (final IOException e1) { + } catch (final IOException ignored) { // Ignore double error } } diff --git a/main/src/cgeo/geocaching/export/GpxSerializer.java b/main/src/cgeo/geocaching/export/GpxSerializer.java index b24eb4c..8139c76 100644 --- a/main/src/cgeo/geocaching/export/GpxSerializer.java +++ b/main/src/cgeo/geocaching/export/GpxSerializer.java @@ -7,7 +7,7 @@ import cgeo.geocaching.Trackable; import cgeo.geocaching.Waypoint; import cgeo.geocaching.enumerations.CacheAttribute; import cgeo.geocaching.enumerations.LoadFlags; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.SynchronizedDateFormat; import cgeo.geocaching.utils.TextUtils; @@ -55,7 +55,7 @@ public final class GpxSerializer { } - public void writeGPX(List<String> allGeocodesIn, Writer writer, final ProgressListener progressListener) throws IOException { + public void writeGPX(final List<String> allGeocodesIn, final Writer writer, final ProgressListener progressListener) throws IOException { // create a copy of the geocode list, as we need to modify it, but it might be immutable final ArrayList<String> allGeocodes = new ArrayList<>(allGeocodesIn); @@ -88,7 +88,7 @@ public final class GpxSerializer { gpx.endDocument(); } - private void exportBatch(final XmlSerializer gpx, Collection<String> geocodesOfBatch) throws IOException { + private void exportBatch(final XmlSerializer gpx, final Collection<String> geocodesOfBatch) throws IOException { final Set<Geocache> caches = DataStore.loadCaches(geocodesOfBatch, LoadFlags.LOAD_ALL_DB_ONLY); for (final Geocache cache : caches) { if (cache == null) { @@ -174,11 +174,10 @@ public final class GpxSerializer { } /** - * @param boolFlag * @return XML schema compliant boolean representation of the boolean flag. This must be either true, false, 0 or 1, * but no other value (also not upper case True/False). */ - private static String gpxBoolean(boolean boolFlag) { + private static String gpxBoolean(final boolean boolFlag) { return boolFlag ? "true" : "false"; } @@ -195,7 +194,7 @@ public final class GpxSerializer { try { final int numericPrefix = Integer.parseInt(prefix); maxPrefix = Math.max(numericPrefix, maxPrefix); - } catch (final NumberFormatException ex) { + } catch (final NumberFormatException ignored) { // ignore non numeric prefix, as it should be unique in the list of non-own waypoints already } } @@ -220,12 +219,6 @@ public final class GpxSerializer { /** * Writes one waypoint entry for cache waypoint. - * - * @param cache - * The - * @param wp - * @param prefix - * @throws IOException */ private void writeCacheWaypoint(final Waypoint wp) throws IOException { final Geopoint coords = wp.getCoords(); @@ -261,7 +254,7 @@ public final class GpxSerializer { } private void writeLogs(final Geocache cache) throws IOException { - List<LogEntry> logs = cache.getLogs(); + final List<LogEntry> logs = cache.getLogs(); if (logs.isEmpty()) { return; } @@ -297,7 +290,7 @@ public final class GpxSerializer { } private void writeTravelBugs(final Geocache cache) throws IOException { - List<Trackable> inventory = cache.getInventory(); + final List<Trackable> inventory = cache.getInventory(); if (CollectionUtils.isEmpty(inventory)) { return; } @@ -344,7 +337,7 @@ public final class GpxSerializer { return getLocationPart(cache, 0); } - private static String getLocationPart(final Geocache cache, int partIndex) { + private static String getLocationPart(final Geocache cache, final int partIndex) { final String location = cache.getLocation(); if (StringUtils.contains(location, ", ")) { final String[] parts = StringUtils.split(location, ','); @@ -356,7 +349,7 @@ public final class GpxSerializer { } public static String getCountry(final Geocache cache) { - String country = getLocationPart(cache, 1); + final String country = getLocationPart(cache, 1); if (StringUtils.isNotEmpty(country)) { return country; } diff --git a/main/src/cgeo/geocaching/files/AbstractFileListActivity.java b/main/src/cgeo/geocaching/files/AbstractFileListActivity.java index 2a05cbc..3ec9849 100644 --- a/main/src/cgeo/geocaching/files/AbstractFileListActivity.java +++ b/main/src/cgeo/geocaching/files/AbstractFileListActivity.java @@ -9,6 +9,7 @@ import cgeo.geocaching.utils.Log; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import android.app.ProgressDialog; import android.content.DialogInterface; @@ -38,7 +39,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext private String searchInfo; @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { if (msg.obj != null && waitDialog != null) { if (searchInfo == null) { searchInfo = res.getString(R.string.file_searching_in) + " "; @@ -52,7 +53,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext private String getDefaultFolders() { final ArrayList<String> names = new ArrayList<>(); - for (File dir : getExistingBaseFolders()) { + for (final File dir : getExistingBaseFolders()) { names.add(dir.getPath()); } return StringUtils.join(names, '\n'); @@ -62,7 +63,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext final private Handler loadFilesHandler = new Handler() { @Override - public void handleMessage(Message msg) { + public void handleMessage(final Message msg) { if (waitDialog != null) { waitDialog.dismiss(); } @@ -76,17 +77,17 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext }; @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTheme(); setContentView(R.layout.gpx); - Bundle extras = getIntent().getExtras(); + final Bundle extras = getIntent().getExtras(); if (extras != null) { listId = extras.getInt(Intents.EXTRA_LIST_ID); } - if (listId <= StoredList.TEMPORARY_LIST_ID) { + if (listId <= StoredList.TEMPORARY_LIST.id) { listId = StoredList.STANDARD_LIST_ID; } @@ -100,7 +101,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext true, new DialogInterface.OnCancelListener() { @Override - public void onCancel(DialogInterface arg0) { + public void onCancel(final DialogInterface arg0) { if (searchingThread != null && searchingThread.isAlive()) { searchingThread.notifyEnd(); } @@ -171,7 +172,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext } else { Log.w("No external media mounted."); } - } catch (Exception e) { + } catch (final Exception e) { Log.e("AbstractFileListActivity.loadFiles.run", e); } @@ -181,7 +182,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext Collections.sort(files, new Comparator<File>() { @Override - public int compare(File lhs, File rhs) { + public int compare(final File lhs, final File rhs) { return lhs.getName().compareToIgnoreCase(rhs.getName()); } }); @@ -189,7 +190,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext loadFilesHandler.sendMessage(Message.obtain(loadFilesHandler)); } - private void listDirs(List<File> list, List<File> directories, FileListSelector selector, Handler feedbackHandler) { + private void listDirs(final List<File> list, final List<File> directories, final FileListSelector selector, final Handler feedbackHandler) { for (final File dir : directories) { FileUtils.listDir(list, dir, selector, feedbackHandler); } @@ -200,11 +201,10 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext * Check if a filename belongs to the AbstractFileListActivity. This implementation checks for file extensions. * Subclasses may override this method to filter out specific files. * - * @param filename * @return <code>true</code> if the filename belongs to the list */ - protected boolean filenameBelongsToList(final String filename) { - for (String ext : extensions) { + protected boolean filenameBelongsToList(@NonNull final String filename) { + for (final String ext : extensions) { if (StringUtils.endsWithIgnoreCase(filename, ext)) { return true; } @@ -213,7 +213,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext } protected List<File> getExistingBaseFolders() { - ArrayList<File> result = new ArrayList<>(); + final ArrayList<File> result = new ArrayList<>(); for (final File dir : getBaseFolders()) { if (dir.exists() && dir.isDirectory()) { result.add(dir); @@ -245,7 +245,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext boolean shouldEnd = false; @Override - public boolean isSelected(File file) { + public boolean isSelected(final File file) { return filenameBelongsToList(file.getName()); } @@ -254,7 +254,7 @@ public abstract class AbstractFileListActivity<T extends ArrayAdapter<File>> ext return shouldEnd; } - public synchronized void setShouldEnd(boolean shouldEnd) { + public synchronized void setShouldEnd(final boolean shouldEnd) { this.shouldEnd = shouldEnd; } } diff --git a/main/src/cgeo/geocaching/files/FileParser.java b/main/src/cgeo/geocaching/files/FileParser.java index 973e65f..9c70a0d 100644 --- a/main/src/cgeo/geocaching/files/FileParser.java +++ b/main/src/cgeo/geocaching/files/FileParser.java @@ -4,6 +4,7 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.utils.CancellableHandler; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.CharEncoding; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -15,7 +16,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; -import java.util.Date; import java.util.concurrent.CancellationException; public abstract class FileParser { @@ -23,27 +23,24 @@ public abstract class FileParser { * Parses caches from input stream. * * @param stream + * the input stream * @param progressHandler - * for reporting parsing progress (in bytes read from input stream) + * for reporting parsing progress (in bytes read from input stream) * @return collection of caches * @throws IOException - * if the input stream can't be read + * if the input stream can't be read * @throws ParserException - * if the input stream contains data not matching the file format of the parser + * if the input stream contains data not matching the file format of the parser */ + @NonNull public abstract Collection<Geocache> parse(@NonNull final InputStream stream, @Nullable final CancellableHandler progressHandler) throws IOException, ParserException; /** * Convenience method for parsing a file. - * - * @param file - * @param progressHandler - * @return - * @throws IOException - * @throws ParserException */ + @NonNull public Collection<Geocache> parse(final File file, final CancellableHandler progressHandler) throws IOException, ParserException { - BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file)); + final BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file)); try { return parse(stream, progressHandler); } finally { @@ -51,10 +48,11 @@ public abstract class FileParser { } } + @NonNull protected static StringBuilder readStream(@NonNull final InputStream is, @Nullable final CancellableHandler progressHandler) throws IOException { final StringBuilder buffer = new StringBuilder(); - ProgressInputStream progressInputStream = new ProgressInputStream(is); - final BufferedReader input = new BufferedReader(new InputStreamReader(progressInputStream, "UTF-8")); + final ProgressInputStream progressInputStream = new ProgressInputStream(is); + final BufferedReader input = new BufferedReader(new InputStreamReader(progressInputStream, CharEncoding.UTF_8)); try { String line; @@ -77,13 +75,13 @@ public abstract class FileParser { } } - protected static void fixCache(Geocache cache) { + protected static void fixCache(final Geocache cache) { if (cache.getInventory() != null) { cache.setInventoryItems(cache.getInventory().size()); } else { cache.setInventoryItems(0); } - final long time = new Date().getTime(); + final long time = System.currentTimeMillis(); cache.setUpdated(time); cache.setDetailedUpdate(time); } diff --git a/main/src/cgeo/geocaching/files/FileTypeDetector.java b/main/src/cgeo/geocaching/files/FileTypeDetector.java index 389b83a..d1a1892 100644 --- a/main/src/cgeo/geocaching/files/FileTypeDetector.java +++ b/main/src/cgeo/geocaching/files/FileTypeDetector.java @@ -3,6 +3,7 @@ package cgeo.geocaching.files; import cgeo.geocaching.utils.Log; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; @@ -10,7 +11,6 @@ import android.content.ContentResolver; import android.net.Uri; import java.io.BufferedReader; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -20,7 +20,7 @@ public class FileTypeDetector { private final ContentResolver contentResolver; private final Uri uri; - public FileTypeDetector(Uri uri, ContentResolver contentResolver) { + public FileTypeDetector(final Uri uri, final ContentResolver contentResolver) { this.uri = uri; this.contentResolver = contentResolver; } @@ -34,12 +34,10 @@ public class FileTypeDetector { if (is == null) { return FileType.UNKNOWN; } - reader = new BufferedReader(new InputStreamReader(is)); + reader = new BufferedReader(new InputStreamReader(is, CharEncoding.UTF_8)); type = detectHeader(reader); reader.close(); - } catch (FileNotFoundException e) { - Log.e("FileTypeDetector", e); - } catch (IOException e) { + } catch (final IOException e) { Log.e("FileTypeDetector", e); } finally { IOUtils.closeQuietly(reader); @@ -48,7 +46,7 @@ public class FileTypeDetector { return type; } - private static FileType detectHeader(BufferedReader reader) + private static FileType detectHeader(final BufferedReader reader) throws IOException { String line = reader.readLine(); if (isZip(line)) { @@ -68,7 +66,7 @@ public class FileTypeDetector { return FileType.UNKNOWN; } - private static boolean isZip(String line) { + private static boolean isZip(final String line) { return StringUtils.length(line) >= 4 && StringUtils.startsWith(line, "PK") && line.charAt(2) == 3 && line.charAt(3) == 4; diff --git a/main/src/cgeo/geocaching/files/GPX10Parser.java b/main/src/cgeo/geocaching/files/GPX10Parser.java index 0ca2606..815d577 100644 --- a/main/src/cgeo/geocaching/files/GPX10Parser.java +++ b/main/src/cgeo/geocaching/files/GPX10Parser.java @@ -4,12 +4,12 @@ import android.sax.Element; public class GPX10Parser extends GPXParser { - public GPX10Parser(int listIdIn) { + public GPX10Parser(final int listIdIn) { super(listIdIn, "http://www.topografix.com/GPX/1/0", "1.0"); } @Override - protected Element getCacheParent(Element waypoint) { + protected Element getCacheParent(final Element waypoint) { return waypoint; } diff --git a/main/src/cgeo/geocaching/files/GPX11Parser.java b/main/src/cgeo/geocaching/files/GPX11Parser.java index 52a6ae3..6ac152c 100644 --- a/main/src/cgeo/geocaching/files/GPX11Parser.java +++ b/main/src/cgeo/geocaching/files/GPX11Parser.java @@ -4,12 +4,12 @@ import android.sax.Element; public final class GPX11Parser extends GPXParser { - public GPX11Parser(int listIdIn) { + public GPX11Parser(final int listIdIn) { super(listIdIn, "http://www.topografix.com/GPX/1/1", "1.1"); } @Override - protected Element getCacheParent(Element waypoint) { + protected Element getCacheParent(final Element waypoint) { return waypoint.getChild(namespace, "extensions"); } diff --git a/main/src/cgeo/geocaching/files/GPXImporter.java b/main/src/cgeo/geocaching/files/GPXImporter.java index 52f68e1..5699ebe 100644 --- a/main/src/cgeo/geocaching/files/GPXImporter.java +++ b/main/src/cgeo/geocaching/files/GPXImporter.java @@ -95,8 +95,6 @@ public class GPXImporter { * * @param uri * URI of the file to import - * @param mimeType - * @param pathName */ public void importGPX(final Uri uri, final @Nullable String mimeType, final @Nullable String pathName) { final ContentResolver contentResolver = fromActivity.getContentResolver(); @@ -113,7 +111,7 @@ public class GPXImporter { fileType = getFileTypeFromMimeType(mimeType); } - ImportThread importer = getImporterFromFileType(uri, contentResolver, + final ImportThread importer = getImporterFromFileType(uri, contentResolver, fileType); if (importer != null) { @@ -139,14 +137,15 @@ public class GPXImporter { final String mimeType) { if (GPX_MIME_TYPES.contains(mimeType)) { return FileType.GPX; - } else if (ZIP_MIME_TYPES.contains(mimeType)) { + } + if (ZIP_MIME_TYPES.contains(mimeType)) { return FileType.ZIP; } - return FileType.UNKNOWN; + 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, @@ -178,7 +177,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; @@ -210,7 +209,7 @@ public class GPXImporter { } catch (final ParserException e) { Log.i("Importing caches failed - data format error", e); importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_FINISHED_WITH_ERROR, R.string.gpx_import_error_parser, 0, e.getLocalizedMessage())); - } catch (final CancellationException e) { + } catch (final CancellationException ignored) { Log.i("Importing caches canceled"); importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_CANCELED)); } catch (final Exception e) { @@ -244,7 +243,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; } @@ -262,7 +261,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; @@ -284,7 +283,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); } @@ -293,7 +292,7 @@ public class GPXImporter { try { // try to parse cache file as GPX 10 return doImport(new GPX10Parser(listId)); - } catch (final ParserException pe) { + } catch (final ParserException ignored) { // didn't work -> lets try GPX11 return doImport(new GPX11Parser(listId)); } @@ -305,13 +304,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); @@ -333,17 +332,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 { @@ -354,12 +357,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 @@ -403,7 +406,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); @@ -419,7 +422,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; @@ -434,14 +437,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 89ee887..aed82e6 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; @@ -16,8 +18,8 @@ import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; import cgeo.geocaching.enumerations.LogType; import cgeo.geocaching.enumerations.WaypointType; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; @@ -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); } } @@ -223,10 +225,10 @@ public abstract class GPXParser extends FileParser { return null; // id not found } // get text for string - String stringName; + final String stringName; try { stringName = CgeoApplication.getInstance().getResources().getResourceName(stringId); - } catch (final NullPointerException e) { + } catch (final NullPointerException ignored) { return null; } if (stringName == null) { @@ -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); @@ -272,6 +274,7 @@ public abstract class GPXParser extends FileParser { } @Override + @NonNull public Collection<Geocache> parse(@NonNull final InputStream stream, @Nullable final CancellableHandler progressHandler) throws IOException, ParserException { resetCache(); final RootElement root = new RootElement(namespace, "gpx"); @@ -280,7 +283,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 +292,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"); @@ -301,7 +304,7 @@ public abstract class GPXParser extends FileParser { } } } catch (final NumberFormatException e) { - Log.w("Failed to parse waypoint's latitude and/or longitude."); + Log.w("Failed to parse waypoint's latitude and/or longitude", e); } } }); @@ -341,7 +344,7 @@ public abstract class GPXParser extends FileParser { // finally store the cache in the database result.add(geocode); DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB)); - DataStore.saveLogsWithoutTransaction(cache.getGeocode(), logs); + DataStore.saveLogs(cache.getGeocode(), logs); // avoid the cachecache using lots of memory for caches which the user did not actually look at DataStore.removeCache(geocode, EnumSet.of(RemoveFlag.CACHE)); @@ -399,7 +402,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 +415,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 +434,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 +445,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,8 +456,8 @@ public abstract class GPXParser extends FileParser { waypoint.getChild(namespace, "type").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { - final String[] content = body.split("\\|"); + 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 +480,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 +500,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 +523,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")); @@ -532,7 +535,7 @@ public abstract class GPXParser extends FileParser { cache.setDisabled(!attrs.getValue("available").equalsIgnoreCase("true")); } } catch (final RuntimeException e) { - Log.w("Failed to parse cache attributes."); + Log.w("Failed to parse cache attributes", e); } } }); @@ -541,7 +544,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 +553,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 +562,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,8 +571,13 @@ public abstract class GPXParser extends FileParser { gcCache.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { @Override - public void end(String body) { - cache.setType(CacheType.getByPattern(validate(body))); + public void end(final String bodyIn) { + String body = validate(bodyIn); + // lab caches wrongly contain a prefix in the type + if (body.startsWith("Geocache|")) { + body = StringUtils.substringAfter(body, "Geocache|").trim(); + } + cache.setType(CacheType.getByPattern(body)); } }); @@ -577,7 +585,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 +605,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")); @@ -607,7 +615,7 @@ public abstract class GPXParser extends FileParser { cache.getAttributes().add(internalId); } } - } catch (final NumberFormatException e) { + } catch (final NumberFormatException ignored) { // nothing } } @@ -617,7 +625,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 +638,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 +651,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 +664,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 +680,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 +688,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 +696,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,14 +711,14 @@ 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 { if (attrs.getIndex("ref") > -1) { trackable.setGeocode(attrs.getValue("ref")); } - } catch (final RuntimeException e) { + } catch (final RuntimeException ignored) { // nothing } } @@ -733,7 +741,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,14 +755,14 @@ 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 { if (attrs.getIndex("id") > -1) { log.id = Integer.parseInt(attrs.getValue("id")); } - } catch (final NumberFormatException e) { + } catch (final NumberFormatException ignored) { // nothing } } @@ -765,6 +773,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 +789,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 +802,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 +812,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 +821,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 +829,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) { @@ -825,7 +840,6 @@ public abstract class GPXParser extends FileParser { /** * Add listeners for GSAK extensions * - * @param cacheParent */ private void registerGsakExtensions(final Element cacheParent) { for (final String gsakNamespace : GSAK_NS) { @@ -833,7 +847,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 +861,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 +869,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)); } @@ -886,7 +900,6 @@ public abstract class GPXParser extends FileParser { /** * Add listeners for c:geo extensions * - * @param cacheParent */ private void registerCgeoExtensions(final Element cacheParent) { final Element cgeoVisited = cacheParent.getChild(CGEO_NS, "visited"); @@ -894,7 +907,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 +917,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 +930,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 } @@ -925,12 +938,10 @@ public abstract class GPXParser extends FileParser { * GPX 1.0 and 1.1 use different XML elements to put the cache into, therefore needs to be overwritten in the * version specific subclasses * - * @param waypoint - * @return */ 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/files/IFileSelectionView.java b/main/src/cgeo/geocaching/files/IFileSelectionView.java index 5bbc1b2..0407ee4 100644 --- a/main/src/cgeo/geocaching/files/IFileSelectionView.java +++ b/main/src/cgeo/geocaching/files/IFileSelectionView.java @@ -8,7 +8,7 @@ public interface IFileSelectionView { String getCurrentFile(); - void setCurrentFile(String string); + void setCurrentFile(final String name); void close(); diff --git a/main/src/cgeo/geocaching/files/InvalidXMLCharacterFilterReader.java b/main/src/cgeo/geocaching/files/InvalidXMLCharacterFilterReader.java index a7a3e1b..eea14c3 100644 --- a/main/src/cgeo/geocaching/files/InvalidXMLCharacterFilterReader.java +++ b/main/src/cgeo/geocaching/files/InvalidXMLCharacterFilterReader.java @@ -1,98 +1,89 @@ -package cgeo.geocaching.files;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.FilterReader;
-import java.io.IOException;
-import java.io.Reader;
-
-/**
- * Filter reader which can filter out invalid XML characters and character references.
- *
- */
-public class InvalidXMLCharacterFilterReader extends FilterReader
-{
-
- public InvalidXMLCharacterFilterReader(Reader in) {
- super(in);
- }
-
- /**
- * Every overload of {@link Reader#read()} method delegates to this one so
- * it is enough to override only this one. <br />
- * To skip invalid characters this method shifts only valid chars to left
- * and returns decreased value of the original read method. So after last
- * valid character there will be some unused chars in the buffer.
- *
- * @return Number of read valid characters or <code>-1</code> if end of the
- * underling reader was reached.
- */
- @Override
- public int read(char[] cbuf, int off, int len) throws IOException {
- int read = super.read(cbuf, off, len);
- // check for end
- if (read == -1) {
- return -1;
- }
- // target position
- int pos = off - 1;
-
- int entityStart = -1;
- for (int readPos = off; readPos < off + read; readPos++) {
- boolean useChar = true;
- switch (cbuf[readPos]) {
- case '&':
- pos++;
- entityStart = readPos;
- break;
- case ';':
- pos++;
- if (entityStart >= 0) {
- int entityLength = readPos - entityStart + 1;
- if (entityLength <= 5) {
- String entity = new String(cbuf, entityStart, entityLength);
- if (StringUtils.startsWith(entity, "&#")) {
- String numberString = StringUtils.substringBetween(entity, "&#", ";");
- final int value;
- if (StringUtils.startsWith(numberString, "x")) {
- value = Integer.parseInt(numberString.substring(1), 16);
- }
- else {
- value = Integer.parseInt(numberString);
- }
- if (!isValidXMLChar((char) value)) {
- pos -= entityLength;
- useChar = false;
- }
- }
- }
- }
- break;
- default:
- if (isValidXMLChar(cbuf[readPos])) {
- pos++;
- } else {
- continue;
- }
- }
- // copy, and skip unwanted characters
- if (pos < readPos && useChar) {
- cbuf[pos] = cbuf[readPos];
- }
- }
- return pos - off + 1;
- }
-
- private static boolean isValidXMLChar(char c) {
- if ((c == 0x9) ||
- (c == 0xA) ||
- (c == 0xD) ||
- ((c >= 0x20) && (c <= 0xD7FF)) ||
- ((c >= 0xE000) && (c <= 0xFFFD)) ||
- ((c >= 0x10000) && (c <= 0x10FFFF)))
- {
- return true;
- }
- return false;
- }
+package cgeo.geocaching.files; + +import org.apache.commons.lang3.StringUtils; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; + +/** + * Filter reader which can filter out invalid XML characters and character references. + * + */ +public class InvalidXMLCharacterFilterReader extends FilterReader +{ + + public InvalidXMLCharacterFilterReader(final Reader in) { + super(in); + } + + /** + * Every overload of {@link Reader#read()} method delegates to this one so + * it is enough to override only this one. <br /> + * To skip invalid characters this method shifts only valid chars to left + * and returns decreased value of the original read method. So after last + * valid character there will be some unused chars in the buffer. + * + * @return Number of read valid characters or <code>-1</code> if end of the + * underling reader was reached. + */ + @Override + public int read(final char[] cbuf, final int off, final int len) throws IOException { + final int read = super.read(cbuf, off, len); + // check for end + if (read == -1) { + return -1; + } + // target position + int pos = off - 1; + + int entityStart = -1; + for (int readPos = off; readPos < off + read; readPos++) { + boolean useChar = true; + switch (cbuf[readPos]) { + case '&': + pos++; + entityStart = readPos; + break; + case ';': + pos++; + if (entityStart >= 0) { + final int entityLength = readPos - entityStart + 1; + if (entityLength <= 5) { + final String entity = new String(cbuf, entityStart, entityLength); + if (StringUtils.startsWith(entity, "&#")) { + final String numberString = StringUtils.substringBetween(entity, "&#", ";"); + final int value; + if (StringUtils.startsWith(numberString, "x")) { + value = Integer.parseInt(numberString.substring(1), 16); + } + else { + value = Integer.parseInt(numberString); + } + if (!isValidXMLChar((char) value)) { + pos -= entityLength; + useChar = false; + } + } + } + } + break; + default: + if (isValidXMLChar(cbuf[readPos])) { + pos++; + } else { + continue; + } + } + // copy, and skip unwanted characters + if (pos < readPos && useChar) { + cbuf[pos] = cbuf[readPos]; + } + } + return pos - off + 1; + } + + private static boolean isValidXMLChar(final char c) { + return c == 0x9 || c == 0xA || c == 0xD || (c >= 0x20 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD); + } }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/files/LocParser.java b/main/src/cgeo/geocaching/files/LocParser.java index 2871d77..a3d9b8a 100644 --- a/main/src/cgeo/geocaching/files/LocParser.java +++ b/main/src/cgeo/geocaching/files/LocParser.java @@ -1,43 +1,40 @@ package cgeo.geocaching.files; -import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; import cgeo.geocaching.SearchResult; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.enumerations.LoadFlags; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.utils.CancellableHandler; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.MatcherWrapper; +import org.apache.commons.io.Charsets; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.regex.Pattern; public final class LocParser extends FileParser { + @NonNull private static final String NAME_OWNER_SEPARATOR = " by "; - private static final Pattern patternGeocode = Pattern - .compile("name id=\"([^\"]+)\""); - private static final Pattern patternLat = Pattern - .compile("lat=\"([^\"]+)\""); - private static final Pattern patternLon = Pattern - .compile("lon=\"([^\"]+)\""); - private static final Pattern patternName = Pattern.compile("CDATA\\[([^\\]]+)\\]"); + @NonNull private static final CacheSize[] SIZES = { CacheSize.NOT_CHOSEN, // 1 CacheSize.MICRO, // 2 @@ -49,22 +46,95 @@ public final class LocParser extends FileParser { CacheSize.SMALL, // 8 }; - private int listId; + // Used so that the initial value of the geocache is not null. Never filled. + @NonNull + private static final Geocache DUMMY_GEOCACHE = new Geocache(); - public static void parseLoc(final SearchResult searchResult, final String fileContent) { - final Map<String, Geocache> cidCoords = parseCoordinates(fileContent); + private final int listId; + + public static void parseLoc(final SearchResult searchResult, final String fileContent, final Set<Geocache> caches) { + final Map<String, Geocache> cidCoords = parseLoc(fileContent); // save found cache coordinates final HashSet<String> contained = new HashSet<>(); - for (String geocode : searchResult.getGeocodes()) { + for (final String geocode : searchResult.getGeocodes()) { if (cidCoords.containsKey(geocode)) { contained.add(geocode); } } - Set<Geocache> caches = DataStore.loadCaches(contained, LoadFlags.LOAD_CACHE_OR_DB); - for (Geocache cache : caches) { - Geocache coord = cidCoords.get(cache.getGeocode()); - copyCoordToCache(coord, cache); + for (final Geocache cache : caches) { + if (!cache.isReliableLatLon()) { + final Geocache coord = cidCoords.get(cache.getGeocode()); + // Archived caches will not have any coordinates + if (coord != null) { + copyCoordToCache(coord, cache); + } + } + } + } + + @NonNull + private static Map<String, Geocache> parseLoc(final String content) { + return parseLoc(new ByteArrayInputStream(content.getBytes(Charsets.UTF_8))); + } + + @NonNull + private static Map<String, Geocache> parseLoc(final InputStream content) { + try { + final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + final XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(content, Charsets.UTF_8.name()); + final Map<String, Geocache> caches = new HashMap<>(); + int eventType = xpp.getEventType(); + Geocache currentCache = DUMMY_GEOCACHE; + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + switch (xpp.getName()) { + case "waypoint": + currentCache = new Geocache(); + currentCache.setType(CacheType.UNKNOWN); // Type not present in .loc file + break; + case "name": + currentCache.setGeocode(xpp.getAttributeValue(null, "id")); + if (xpp.next() == XmlPullParser.TEXT) { + final String nameOwner = xpp.getText(); + currentCache.setName(StringUtils.trim(StringUtils.substringBeforeLast(nameOwner, NAME_OWNER_SEPARATOR))); + currentCache.setOwnerUserId(StringUtils.trim(StringUtils.substringAfterLast(nameOwner, NAME_OWNER_SEPARATOR))); + } + break; + case "coord": + currentCache.setCoords(new Geopoint(Double.valueOf(xpp.getAttributeValue(null, "lat")), + Double.valueOf(xpp.getAttributeValue(null, "lon")))); + currentCache.setReliableLatLon(true); + break; + case "container": + if (xpp.next() == XmlPullParser.TEXT) { + currentCache.setSize(SIZES[Integer.parseInt(xpp.getText()) - 1]); + } + break; + case "difficulty": + if (xpp.next() == XmlPullParser.TEXT) { + currentCache.setDifficulty(Float.valueOf(xpp.getText())); + } + break; + case "terrain": + if (xpp.next() == XmlPullParser.TEXT) { + currentCache.setTerrain(Float.valueOf(xpp.getText())); + } + break; + default: + // Ignore + } + } else if (eventType == XmlPullParser.END_TAG && xpp.getName().equals("waypoint") && StringUtils.isNotBlank(currentCache.getGeocode())) { + caches.put(currentCache.getGeocode(), currentCache); + } + eventType = xpp.next(); + } + Log.d("Coordinates found in .loc content: " + caches.size()); + return caches; + } catch (XmlPullParserException | IOException e) { + Log.e("unable to parse .loc content", e); + return Collections.emptyMap(); } } @@ -81,56 +151,33 @@ public final class LocParser extends FileParser { cache.setOwnerUserId(coord.getOwnerUserId()); } - static Map<String, Geocache> parseCoordinates(final String fileContent) { - final Map<String, Geocache> coords = new HashMap<>(); - if (StringUtils.isBlank(fileContent)) { - return coords; - } - // >> premium only - - final String[] points = fileContent.split("<waypoint>"); - - // parse coordinates - for (String pointString : points) { - final Geocache pointCoord = parseCache(pointString); - if (StringUtils.isNotBlank(pointCoord.getGeocode())) { - coords.put(pointCoord.getGeocode(), pointCoord); - } - } - - Log.i("Coordinates found in .loc file: " + coords.size()); - return coords; - } - + @NonNull public static Geopoint parsePoint(final String latitude, final String longitude) { // the loc file contains the coordinates as plain floating point values, therefore avoid using the GeopointParser try { return new Geopoint(Double.valueOf(latitude), Double.valueOf(longitude)); - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { Log.e("LOC format has changed", e); } // fall back to parser, just in case the format changes return new Geopoint(latitude, longitude); } - public LocParser(int listId) { + public LocParser(final int listId) { this.listId = listId; } @Override + @NonNull public Collection<Geocache> parse(@NonNull final InputStream stream, @Nullable final CancellableHandler progressHandler) throws IOException, ParserException { - final String streamContent = readStream(stream, null).toString(); - final int maxSize = streamContent.length(); - final Map<String, Geocache> coords = parseCoordinates(streamContent); + final int maxSize = stream.available(); + final Map<String, Geocache> coords = parseLoc(stream); final List<Geocache> caches = new ArrayList<>(); - for (Entry<String, Geocache> entry : coords.entrySet()) { - Geocache coord = entry.getValue(); - if (StringUtils.isBlank(coord.getGeocode()) || StringUtils.isBlank(coord.getName())) { + for (final Entry<String, Geocache> entry : coords.entrySet()) { + final Geocache cache = entry.getValue(); + if (StringUtils.isBlank(cache.getGeocode()) || StringUtils.isBlank(cache.getName())) { continue; } - Geocache cache = new Geocache(); - cache.setReliableLatLon(true); - copyCoordToCache(coord, cache); caches.add(cache); fixCache(cache); @@ -146,53 +193,4 @@ public final class LocParser extends FileParser { return caches; } - public static Geocache parseCache(final String pointString) { - final Geocache cache = new Geocache(); - final MatcherWrapper matcherGeocode = new MatcherWrapper(patternGeocode, pointString); - if (matcherGeocode.find()) { - cache.setGeocode(matcherGeocode.group(1).trim()); - } - - final MatcherWrapper matcherName = new MatcherWrapper(patternName, pointString); - if (matcherName.find()) { - final String name = matcherName.group(1).trim(); - String ownerName = StringUtils.trim(StringUtils.substringAfterLast(name, NAME_OWNER_SEPARATOR)); - if (StringUtils.isEmpty(cache.getOwnerUserId()) && StringUtils.isNotEmpty(ownerName)) { - cache.setOwnerUserId(ownerName); - } - cache.setName(StringUtils.substringBeforeLast(name, NAME_OWNER_SEPARATOR).trim()); - } else { - cache.setName(cache.getGeocode()); - } - - final MatcherWrapper matcherLat = new MatcherWrapper(patternLat, pointString); - final MatcherWrapper matcherLon = new MatcherWrapper(patternLon, pointString); - if (matcherLat.find() && matcherLon.find()) { - cache.setCoords(parsePoint(matcherLat.group(1).trim(), matcherLon.group(1).trim())); - } - - final String difficulty = StringUtils.substringBetween(pointString, "<difficulty>", "</difficulty>"); - final String terrain = StringUtils.substringBetween(pointString, "<terrain>", "</terrain>"); - final String container = StringUtils.substringBetween(pointString, "<container>", "</container"); - try { - if (StringUtils.isNotBlank(difficulty)) { - cache.setDifficulty(Float.parseFloat(difficulty.trim())); - } - - if (StringUtils.isNotBlank(terrain)) { - cache.setTerrain(Float.parseFloat(terrain.trim())); - } - - if (StringUtils.isNotBlank(container)) { - final int size = Integer.parseInt(container.trim()); - if (size >= 1 && size <= 8) { - cache.setSize(SIZES[size - 1]); - } - } - } catch (NumberFormatException e) { - Log.e("LocParser.parseCache", e); - } - - return cache; - } } diff --git a/main/src/cgeo/geocaching/files/LocalStorage.java b/main/src/cgeo/geocaching/files/LocalStorage.java index 63a1844..cfeac9a 100644 --- a/main/src/cgeo/geocaching/files/LocalStorage.java +++ b/main/src/cgeo/geocaching/files/LocalStorage.java @@ -7,7 +7,9 @@ import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.HttpResponse; + import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -43,7 +45,7 @@ public final class LocalStorage { public static final String HEADER_ETAG = "etag"; /** Name of the local private directory used to hold cached information */ - public final static String cache = ".cgeo"; + public final static String CACHE_DIRNAME = ".cgeo"; private static File internalStorageBase; @@ -69,10 +71,10 @@ public final class LocalStorage { return getStorageSpecific(true); } - private static File getStorageSpecific(boolean secondary) { + private static File getStorageSpecific(final boolean secondary) { return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ^ secondary ? getExternalStorageBase() : - new File(getInternalStorageBase(), LocalStorage.cache); + new File(getInternalStorageBase(), LocalStorage.CACHE_DIRNAME); } public static File getExternalDbDirectory() { @@ -84,7 +86,7 @@ public final class LocalStorage { } private static File getExternalStorageBase() { - return new File(Environment.getExternalStorageDirectory(), LocalStorage.cache); + return new File(Environment.getExternalStorageDirectory(), LocalStorage.CACHE_DIRNAME); } private static File getInternalStorageBase() { @@ -103,7 +105,7 @@ public final class LocalStorage { * @return the file extension, including the leading dot, or the empty string if none could be determined */ static String getExtension(final String url) { - String urlExt; + final String urlExt; if (url.startsWith("data:")) { // "data:image/png;base64,i53…" -> ".png" urlExt = StringUtils.substringAfter(StringUtils.substringBefore(url, ";"), "/"); @@ -122,7 +124,7 @@ public final class LocalStorage { * the geocode * @return the cache directory */ - public static File getStorageDir(@Nullable final String geocode) { + public static File getStorageDir(@NonNull final String geocode) { return storageDir(getStorage(), geocode); } @@ -134,12 +136,12 @@ public final class LocalStorage { * the geocode * @return the cache directory */ - private static File getStorageSecDir(@Nullable final String geocode) { + private static File getStorageSecDir(@NonNull final String geocode) { return storageDir(getStorageSec(), geocode); } - private static File storageDir(final File base, @Nullable final String geocode) { - return new File(base, StringUtils.defaultIfEmpty(geocode, "_others")); + private static File storageDir(final File base, @NonNull final String geocode) { + return new File(base, geocode); } /** @@ -155,7 +157,7 @@ public final class LocalStorage { * true if an url was given, false if a file name was given * @return the file */ - public static File getStorageFile(@Nullable final String geocode, final String fileNameOrUrl, final boolean isUrl, final boolean createDirs) { + public static File getStorageFile(@NonNull final String geocode, final String fileNameOrUrl, final boolean isUrl, final boolean createDirs) { return buildFile(getStorageDir(geocode), fileNameOrUrl, isUrl, createDirs); } @@ -202,7 +204,7 @@ public final class LocalStorage { saveHeader(HEADER_ETAG, saved ? response : null, targetFile); saveHeader(HEADER_LAST_MODIFIED, saved ? response : null, targetFile); return saved; - } catch (IOException e) { + } catch (final IOException e) { Log.e("LocalStorage.saveEntityToFile", e); } @@ -242,16 +244,16 @@ public final class LocalStorage { public static String getSavedHeader(final File baseFile, final String name) { try { final File file = filenameForHeader(baseFile, name); - final Reader f = new InputStreamReader(new FileInputStream(file), "UTF-8"); + final Reader reader = new InputStreamReader(new FileInputStream(file), CharEncoding.UTF_8); try { // No header will be more than 256 bytes final char[] value = new char[256]; - final int count = f.read(value); + final int count = reader.read(value); return new String(value, 0, count); } finally { - f.close(); + reader.close(); } - } catch (final FileNotFoundException e) { + } catch (final FileNotFoundException ignored) { // Do nothing, the file does not exist } catch (final Exception e) { Log.w("could not read saved header " + name + " for " + baseFile, e); @@ -291,7 +293,7 @@ public final class LocalStorage { } finally { IOUtils.closeQuietly(inputStream); } - } catch (IOException e) { + } catch (final IOException e) { Log.e("LocalStorage.saveToFile", e); FileUtils.deleteIgnoringFailure(targetFile); } @@ -321,10 +323,10 @@ public final class LocalStorage { // close here already to catch any issue with closing input.close(); output.close(); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { Log.e("LocalStorage.copy: could not copy file", e); return false; - } catch (IOException e) { + } catch (final IOException e) { Log.e("LocalStorage.copy: could not copy file", e); return false; } finally { @@ -345,7 +347,7 @@ public final class LocalStorage { } // Flushing is only necessary if the stream is not immediately closed afterwards. // We rely on all callers to do that correctly outside of this method - } catch (IOException e) { + } catch (final IOException e) { Log.e("LocalStorage.copy: error when copying data", e); return false; } @@ -362,24 +364,6 @@ public final class LocalStorage { return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); } - public static boolean deleteDirectory(@NonNull final File dir) { - final File[] files = dir.listFiles(); - - // Although we are called on an existing directory, it might have been removed concurrently - // in the meantime, for example by the user or by another cleanup task. - if (files != null) { - for (final File file : files) { - if (file.isDirectory()) { - deleteDirectory(file); - } else { - FileUtils.delete(file); - } - } - } - - return FileUtils.delete(dir); - } - /** * Deletes all files from directory geocode with the given prefix. * @@ -398,7 +382,7 @@ public final class LocalStorage { if (!FileUtils.delete(file)) { Log.w("LocalStorage.deleteFilesPrefix: Can't delete file " + file.getName()); } - } catch (Exception e) { + } catch (final Exception e) { Log.e("LocalStorage.deleteFilesPrefix", e); } } @@ -417,7 +401,7 @@ public final class LocalStorage { public static File[] getFilesWithPrefix(final String geocode, final String filenamePrefix) { final FilenameFilter filter = new FilenameFilter() { @Override - public boolean accept(File dir, String filename) { + public boolean accept(final File dir, final String filename) { return filename.startsWith(filenamePrefix); } }; @@ -430,35 +414,35 @@ public final class LocalStorage { */ public static List<File> getStorages() { - String extStorage = Environment.getExternalStorageDirectory().getAbsolutePath(); - List<File> storages = new ArrayList<>(); + final String extStorage = Environment.getExternalStorageDirectory().getAbsolutePath(); + final List<File> storages = new ArrayList<>(); storages.add(new File(extStorage)); - File file = new File(FILE_SYSTEM_TABLE_PATH); + final File file = new File(FILE_SYSTEM_TABLE_PATH); if (file.canRead()) { Reader fr = null; BufferedReader br = null; try { - fr = new InputStreamReader(new FileInputStream(file), "UTF-8"); + fr = new InputStreamReader(new FileInputStream(file), CharEncoding.UTF_8); br = new BufferedReader(fr); - String s = br.readLine(); - while (s != null) { - if (s.startsWith("dev_mount")) { - String[] tokens = StringUtils.split(s); + String str = br.readLine(); + while (str != null) { + if (str.startsWith("dev_mount")) { + final String[] tokens = StringUtils.split(str); if (tokens.length >= 3) { - String path = tokens[2]; // mountpoint + final String path = tokens[2]; // mountpoint if (!extStorage.equals(path)) { - File directory = new File(path); + final File directory = new File(path); if (directory.exists() && directory.isDirectory()) { storages.add(directory); } } } } - s = br.readLine(); + str = br.readLine(); } - } catch (IOException e) { + } catch (final IOException e) { Log.e("Could not get additional mount points for user content. " + - "Proceeding with external storage only (" + extStorage + ")"); + "Proceeding with external storage only (" + extStorage + ")", e); } finally { IOUtils.closeQuietly(fr); IOUtils.closeQuietly(br); diff --git a/main/src/cgeo/geocaching/files/NoCloseInputStream.java b/main/src/cgeo/geocaching/files/NoCloseInputStream.java index 5a72607..493f6ca 100644 --- a/main/src/cgeo/geocaching/files/NoCloseInputStream.java +++ b/main/src/cgeo/geocaching/files/NoCloseInputStream.java @@ -9,9 +9,9 @@ import java.io.InputStream; * one input stream (e.g. ZipInputStream) because SAX parser closes stream. */ public class NoCloseInputStream extends FilterInputStream { - private static ClosedInputStream closedInputStream = new ClosedInputStream(); + private static final ClosedInputStream closedInputStream = new ClosedInputStream(); - public NoCloseInputStream(InputStream in) { + public NoCloseInputStream(final InputStream in) { super(in); } diff --git a/main/src/cgeo/geocaching/files/ParserException.java b/main/src/cgeo/geocaching/files/ParserException.java index c0076cc..6c8cfda 100644 --- a/main/src/cgeo/geocaching/files/ParserException.java +++ b/main/src/cgeo/geocaching/files/ParserException.java @@ -9,11 +9,11 @@ public class ParserException extends Exception { public ParserException() { } - public ParserException(String detailMessage) { + public ParserException(final String detailMessage) { super(detailMessage); } - public ParserException(String detailMessage, Throwable throwable) { + public ParserException(final String detailMessage, final Throwable throwable) { super(detailMessage, throwable); } diff --git a/main/src/cgeo/geocaching/files/ProgressInputStream.java b/main/src/cgeo/geocaching/files/ProgressInputStream.java index 552aee0..3b249a1 100644 --- a/main/src/cgeo/geocaching/files/ProgressInputStream.java +++ b/main/src/cgeo/geocaching/files/ProgressInputStream.java @@ -16,12 +16,12 @@ public class ProgressInputStream extends FilterInputStream { private int progress = 0; - protected ProgressInputStream(InputStream in) { + protected ProgressInputStream(final InputStream in) { super(in); } @Override - public int read() throws IOException { + public int read() throws IOException { // NO_UCD This method is called from the framework final int read = super.read(); if (read >= 0) { progress++; @@ -30,7 +30,7 @@ public class ProgressInputStream extends FilterInputStream { } @Override - public int read(byte[] buffer, int offset, int count) throws IOException { + public int read(final byte[] buffer, final int offset, final int count) throws IOException { final int read = super.read(buffer, offset, count); progress += read; return read; diff --git a/main/src/cgeo/geocaching/files/SimpleDirChooser.java b/main/src/cgeo/geocaching/files/SimpleDirChooser.java index 2aadf16..687aaa0 100644 --- a/main/src/cgeo/geocaching/files/SimpleDirChooser.java +++ b/main/src/cgeo/geocaching/files/SimpleDirChooser.java @@ -142,7 +142,7 @@ public class SimpleDirChooser extends AbstractListActivity { for (final File currentDir : dirs) { listDirs.add(new Option(currentDir.getName(), currentDir.getAbsolutePath(), currentDir.canWrite())); } - } catch (final RuntimeException e) { + } catch (final RuntimeException ignored) { } Collections.sort(listDirs, Option.NAME_COMPARATOR); if (dir.getParent() != null) { @@ -263,7 +263,7 @@ public class SimpleDirChooser extends AbstractListActivity { private boolean checked = false; private boolean writeable = false; - private static Comparator<Option> NAME_COMPARATOR = new Comparator<SimpleDirChooser.Option>() { + private final static Comparator<Option> NAME_COMPARATOR = new Comparator<SimpleDirChooser.Option>() { @Override public int compare(final Option lhs, final Option rhs) { diff --git a/main/src/cgeo/geocaching/filter/AbstractFilter.java b/main/src/cgeo/geocaching/filter/AbstractFilter.java index e602b0f..4d521c5 100644 --- a/main/src/cgeo/geocaching/filter/AbstractFilter.java +++ b/main/src/cgeo/geocaching/filter/AbstractFilter.java @@ -1,21 +1,30 @@ package cgeo.geocaching.filter; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; +import org.eclipse.jdt.annotation.NonNull; + import java.util.ArrayList; import java.util.List; abstract class AbstractFilter implements IFilter { + @NonNull private final String name; - protected AbstractFilter(final String name) { + protected AbstractFilter(final int nameResourceId) { + this(CgeoApplication.getInstance().getString(nameResourceId)); + } + + protected AbstractFilter(@NonNull final String name) { this.name = name; } + @Override - public void filter(final List<Geocache> list) { + public void filter(@NonNull final List<Geocache> list) { final List<Geocache> itemsToRemove = new ArrayList<>(); - for (Geocache item : list) { + for (final Geocache item : list) { if (!accepts(item)) { itemsToRemove.add(item); } @@ -24,6 +33,7 @@ abstract class AbstractFilter implements IFilter { } @Override + @NonNull public String getName() { return name; } @@ -37,4 +47,9 @@ abstract class AbstractFilter implements IFilter { public String toString() { return getName(); } + + @Override + public int getImageId() { + return 0; + } } diff --git a/main/src/cgeo/geocaching/filter/AttributeFilter.java b/main/src/cgeo/geocaching/filter/AttributeFilter.java index b59ab29..6ed4a8f 100644 --- a/main/src/cgeo/geocaching/filter/AttributeFilter.java +++ b/main/src/cgeo/geocaching/filter/AttributeFilter.java @@ -4,6 +4,8 @@ import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import android.content.res.Resources; import java.util.LinkedList; @@ -13,7 +15,7 @@ class AttributeFilter extends AbstractFilter { private final String attribute; - public AttributeFilter(final String name, final String attribute) { + public AttributeFilter(@NonNull final String name, final String attribute) { super(name); this.attribute = attribute; } @@ -25,13 +27,14 @@ class AttributeFilter extends AbstractFilter { } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { return cache.getAttributes().contains(attribute); } public static class Factory implements IFilterFactory { @Override + @NonNull public List<IFilter> getFilters() { final String packageName = CgeoApplication.getInstance().getBaseContext().getPackageName(); final Resources res = CgeoApplication.getInstance().getResources(); diff --git a/main/src/cgeo/geocaching/filter/DifficultyFilter.java b/main/src/cgeo/geocaching/filter/DifficultyFilter.java index 175ad75..7989560 100644 --- a/main/src/cgeo/geocaching/filter/DifficultyFilter.java +++ b/main/src/cgeo/geocaching/filter/DifficultyFilter.java @@ -3,6 +3,8 @@ package cgeo.geocaching.filter; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import java.util.ArrayList; import java.util.List; @@ -13,7 +15,7 @@ class DifficultyFilter extends AbstractRangeFilter { } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { final float difficulty = cache.getDifficulty(); return rangeMin <= difficulty && difficulty < rangeMax; } @@ -24,6 +26,7 @@ class DifficultyFilter extends AbstractRangeFilter { private static final int DIFFICULTY_MAX = 5; @Override + @NonNull public List<IFilter> getFilters() { final ArrayList<IFilter> filters = new ArrayList<>(DIFFICULTY_MAX); for (int difficulty = DIFFICULTY_MIN; difficulty <= DIFFICULTY_MAX; difficulty++) { diff --git a/main/src/cgeo/geocaching/filter/DistanceFilter.java b/main/src/cgeo/geocaching/filter/DistanceFilter.java index 3328c72..b352f5d 100644 --- a/main/src/cgeo/geocaching/filter/DistanceFilter.java +++ b/main/src/cgeo/geocaching/filter/DistanceFilter.java @@ -2,28 +2,31 @@ package cgeo.geocaching.filter; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; -import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.sensors.GeoData; +import cgeo.geocaching.sensors.Sensors; + +import org.eclipse.jdt.annotation.NonNull; import java.util.ArrayList; import java.util.List; class DistanceFilter extends AbstractFilter { - private final IGeoData geo; + private final GeoData geo; private final int minDistance; private final int maxDistance; - public DistanceFilter(String name, final int minDistance, final int maxDistance) { + public DistanceFilter(@NonNull final String name, final int minDistance, final int maxDistance) { super(name); this.minDistance = minDistance; this.maxDistance = maxDistance; - geo = CgeoApplication.getInstance().currentGeo(); + geo = Sensors.getInstance().currentGeo(); } @Override - public boolean accepts(final Geocache cache) { - final Geopoint currentPos = new Geopoint(geo.getLocation()); + public boolean accepts(@NonNull final Geocache cache) { + final Geopoint currentPos = new Geopoint(geo); final Geopoint coords = cache.getCoords(); if (coords == null) { // If a cache has no coordinates, consider it to be out of range. It will @@ -39,6 +42,7 @@ class DistanceFilter extends AbstractFilter { private static final int[] KILOMETERS = { 0, 2, 5, 10, 20, 50 }; @Override + @NonNull public List<IFilter> getFilters() { final List<IFilter> filters = new ArrayList<>(KILOMETERS.length); for (int i = 0; i < KILOMETERS.length; i++) { diff --git a/main/src/cgeo/geocaching/filter/FilterActivity.java b/main/src/cgeo/geocaching/filter/FilterActivity.java new file mode 100644 index 0000000..13a2263 --- /dev/null +++ b/main/src/cgeo/geocaching/filter/FilterActivity.java @@ -0,0 +1,154 @@ +package cgeo.geocaching.filter; + +import butterknife.ButterKnife; +import butterknife.InjectView; + +import cgeo.geocaching.R; +import cgeo.geocaching.activity.AbstractActionBarActivity; +import cgeo.geocaching.filter.FilterRegistry.FactoryEntry; +import cgeo.geocaching.utils.Log; + +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.OptionsItem; +import org.androidannotations.annotations.OptionsMenu; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.OnChildClickListener; +import android.widget.LinearLayout; +import android.widget.SimpleExpandableListAdapter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Show a filter selection using an {@code ExpandableListView}. + */ +@OptionsMenu(R.menu.filter_options) +@EActivity +public class FilterActivity extends AbstractActionBarActivity { + + public static final String EXTRA_FILTER_RESULT = null; + public static final int REQUEST_SELECT_FILTER = 1234; + + private static final String KEY_FILTER_NAME = "filterName"; + private static final String KEY_FILTER_GROUP_NAME = "filterGroupName"; + + @InjectView(R.id.filterList) protected ExpandableListView filterList; + @InjectView(R.id.filters) protected LinearLayout filtersContainer; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState, R.layout.filter_activity); + ButterKnife.inject(this); + + createListAdapter(); + } + + private void createListAdapter() { + final SimpleExpandableListAdapter adapter = + new SimpleExpandableListAdapter( + this, + // top level entries in the next 4 lines + createFilterTopLevelGroups(), + android.R.layout.simple_expandable_list_item_1, + new String[] { KEY_FILTER_GROUP_NAME }, + new int[] { android.R.id.text1 }, + + // child level entries in the next 4 lines + createFilterChildren(), + android.R.layout.simple_expandable_list_item_2, + new String[] { KEY_FILTER_NAME, "CHILD_NAME" }, + new int[] { android.R.id.text1 } + ); + filterList.setAdapter(adapter); + filterList.setOnChildClickListener(new OnChildClickListener() { + + @Override + public boolean onChildClick(final ExpandableListView parent, final View v, final int groupPosition, final int childPosition, final long id) { + setFilterResult(groupPosition, childPosition); + return true; + } + + }); + } + + public static @Nullable IFilter getFilterFromPosition(final int groupPosition, final int childPosition) { + if (groupPosition < 0 || childPosition < 0) { + return null; + } + final FactoryEntry factoryEntry = FilterRegistry.getInstance().getFactories().get(groupPosition); + return createFilterFactory(factoryEntry.getFactory()).getFilters().get(childPosition); + } + + /** + * Creates the group list with the mapped properties. + */ + private static List<Map<String, String>> createFilterTopLevelGroups() { + final ArrayList<Map<String, String>> groups = new ArrayList<>(); + for (final FactoryEntry factoryEntry : FilterRegistry.getInstance().getFactories()) { + final Map<String, String> map = new HashMap<>(); + map.put(KEY_FILTER_GROUP_NAME, factoryEntry.getName()); + groups.add(map); + } + return groups; + } + + private static List<List<Map<String, String>>> createFilterChildren() { + final List<List<Map<String, String>>> listOfChildGroups = new ArrayList<>(); + + for (final FactoryEntry factoryEntry : FilterRegistry.getInstance().getFactories()) { + final IFilterFactory factory = createFilterFactory(factoryEntry.getFactory()); + final List<? extends IFilter> filters = factory.getFilters(); + + final List<Map<String, String>> childGroups = new ArrayList<>(filters.size()); + + for (final IFilter filter : filters) { + final HashMap<String, String> hashMap = new HashMap<>(1); + hashMap.put(KEY_FILTER_NAME, filter.getName()); + hashMap.put("CHILD_NAME", filter.getName()); + childGroups.add(hashMap); + } + listOfChildGroups.add(childGroups); + } + return listOfChildGroups; + } + + private static IFilterFactory createFilterFactory(final Class<? extends IFilterFactory> class1) { + try { + return class1.newInstance(); + } catch (final InstantiationException e) { + Log.e("createFilterFactory", e); + } catch (final IllegalAccessException e) { + Log.e("createFilterFactory", e); + } + return null; + } + + /** + * After calling this method, the calling activity must implement onActivityResult, and check the + * {@link #EXTRA_FILTER_RESULT}. + */ + public static void selectFilter(@NonNull final Activity context) { + context.startActivityForResult(new Intent(context, FilterActivity_.class), REQUEST_SELECT_FILTER); + } + + @OptionsItem(R.id.menu_reset_filter) + void resetFilter() { + setFilterResult(-1, -1); + } + + private void setFilterResult(final int groupPosition, final int childPosition) { + final Intent resultIntent = new Intent(); + resultIntent.putExtra(EXTRA_FILTER_RESULT, new int[] { groupPosition, childPosition }); + setResult(Activity.RESULT_OK, resultIntent); + finish(); + } +} diff --git a/main/src/cgeo/geocaching/filter/FilterRegistry.java b/main/src/cgeo/geocaching/filter/FilterRegistry.java new file mode 100644 index 0000000..d6d9db9 --- /dev/null +++ b/main/src/cgeo/geocaching/filter/FilterRegistry.java @@ -0,0 +1,86 @@ +package cgeo.geocaching.filter; + +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.R; +import cgeo.geocaching.filter.SizeFilter.Factory; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; + +import android.content.res.Resources; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * singleton registry of all available filter components + * + */ +public class FilterRegistry { + private final List<FactoryEntry> registry = new ArrayList<>(); + private static Resources res; + + static class FactoryEntry { + private final String name; + private final @NonNull Class<? extends IFilterFactory> filterFactory; + + public FactoryEntry(final String name, final @NonNull Class<? extends IFilterFactory> filterFactory) { + this.name = name; + this.filterFactory = filterFactory; + } + + @Override + public String toString() { + return name; + } + + public String getName() { + return name; + } + + public Class<? extends IFilterFactory> getFactory() { + return filterFactory; + } + } + + private static class SingletonHolder { + private static final FilterRegistry INSTANCE = new FilterRegistry(); + } + + public static FilterRegistry getInstance() { + return SingletonHolder.INSTANCE; + } + + private FilterRegistry() { + res = CgeoApplication.getInstance().getResources(); + register(R.string.caches_filter_type, TypeFilter.Factory.class); + register(R.string.caches_filter_size, SizeFilter.Factory.class); + register(R.string.cache_terrain, TerrainFilter.Factory.class); + register(R.string.cache_difficulty, DifficultyFilter.Factory.class); + register(R.string.cache_attributes, AttributeFilter.Factory.class); + register(R.string.cache_status, StateFilterFactory.class); + register(R.string.caches_filter_origin, OriginFilter.Factory.class); + register(R.string.caches_filter_distance, DistanceFilter.Factory.class); + register(R.string.caches_filter_popularity, PopularityFilter.Factory.class); + register(R.string.caches_filter_popularity_ratio, PopularityRatioFilter.Factory.class); + register(R.string.caches_filter_personal_data, PersonalDataFilterFactory.class); + } + + private void register(final int resourceId, final @NonNull Class<? extends IFilterFactory> factoryClass) { + registry.add(new FactoryEntry(res.getString(resourceId), factoryClass)); + } + + public String getFactoryName(final Class<Factory> factoryClass) { + for (final FactoryEntry entry : registry) { + if (entry.filterFactory == factoryClass) { + return entry.name; + } + } + return StringUtils.EMPTY; + } + + public List<FactoryEntry> getFactories() { + return Collections.unmodifiableList(registry); + } +} diff --git a/main/src/cgeo/geocaching/filter/FilterUserInterface.java b/main/src/cgeo/geocaching/filter/FilterUserInterface.java deleted file mode 100644 index 9f1d563..0000000 --- a/main/src/cgeo/geocaching/filter/FilterUserInterface.java +++ /dev/null @@ -1,130 +0,0 @@ -package cgeo.geocaching.filter; - -import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.R; -import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.settings.Settings; -import cgeo.geocaching.utils.Log; - -import rx.functions.Action1; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.widget.ArrayAdapter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public final class FilterUserInterface { - - private static class FactoryEntry { - private final String name; - private final Class<? extends IFilterFactory> filterFactory; - - public FactoryEntry(final String name, final Class<? extends IFilterFactory> filterFactory) { - this.name = name; - this.filterFactory = filterFactory; - } - - @Override - public String toString() { - return name; - } - } - - private final Activity activity; - private final ArrayList<FactoryEntry> registry; - private final Resources res; - - public FilterUserInterface(final Activity activity) { - this.activity = activity; - this.res = CgeoApplication.getInstance().getResources(); - - registry = new ArrayList<>(); - if (Settings.getCacheType() == CacheType.ALL) { - register(R.string.caches_filter_type, TypeFilter.Factory.class); - } - register(R.string.caches_filter_size, SizeFilter.Factory.class); - register(R.string.cache_terrain, TerrainFilter.Factory.class); - register(R.string.cache_difficulty, DifficultyFilter.Factory.class); - register(R.string.cache_attributes, AttributeFilter.Factory.class); - register(R.string.cache_status, StateFilter.Factory.class); - register(R.string.caches_filter_track, TrackablesFilter.class); - register(R.string.caches_filter_modified, ModifiedFilter.class); - register(R.string.caches_filter_origin, OriginFilter.Factory.class); - register(R.string.caches_filter_distance, DistanceFilter.Factory.class); - register(R.string.caches_filter_personal_note, PersonalNoteFilter.class); - register(R.string.caches_filter_popularity, PopularityFilter.Factory.class); - register(R.string.caches_filter_popularity_ratio, PopularityRatioFilter.Factory.class); - - // sort by localized names - Collections.sort(registry, new Comparator<FactoryEntry>() { - - @Override - public int compare(final FactoryEntry lhs, final FactoryEntry rhs) { - return lhs.name.compareToIgnoreCase(rhs.name); - } - }); - - // reset shall be last - register(R.string.caches_filter_clear, null); - } - - private void register(final int resourceId, final Class<? extends IFilterFactory> factoryClass) { - registry.add(new FactoryEntry(res.getString(resourceId), factoryClass)); - } - - public void selectFilter(final Action1<IFilter> runAfterwards) { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.caches_filter_title); - - final ArrayAdapter<FactoryEntry> adapter = new ArrayAdapter<>(activity, android.R.layout.select_dialog_item, registry); - - builder.setAdapter(adapter, new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int itemIndex) { - FactoryEntry entry = adapter.getItem(itemIndex); - // reset? - if (entry.filterFactory == null) { - runAfterwards.call(null); - } - else { - try { - IFilterFactory factoryInstance = entry.filterFactory.newInstance(); - selectFromFactory(factoryInstance, entry.name, runAfterwards); - } catch (Exception e) { - Log.e("selectFilter", e); - } - } - } - }); - - builder.create().show(); - } - - private void selectFromFactory(final IFilterFactory factory, final String menuTitle, final Action1<IFilter> runAfterwards) { - final List<IFilter> filters = Collections.unmodifiableList(factory.getFilters()); - if (filters.size() == 1) { - runAfterwards.call(filters.get(0)); - return; - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(menuTitle); - - final ArrayAdapter<IFilter> adapter = new ArrayAdapter<>(activity, android.R.layout.select_dialog_item, filters); - builder.setAdapter(adapter, new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int item) { - runAfterwards.call(filters.get(item)); - } - }); - - builder.create().show(); - } - -} diff --git a/main/src/cgeo/geocaching/filter/IFilter.java b/main/src/cgeo/geocaching/filter/IFilter.java index 4fafe6f..f590f79 100644 --- a/main/src/cgeo/geocaching/filter/IFilter.java +++ b/main/src/cgeo/geocaching/filter/IFilter.java @@ -2,17 +2,21 @@ package cgeo.geocaching.filter; import cgeo.geocaching.Geocache; +import org.eclipse.jdt.annotation.NonNull; + import java.util.List; public interface IFilter { + @NonNull String getName(); /** - * @param cache - * @return true if the filter accepts the cache, false otherwise + * @return {@code true} if the filter accepts the cache, false otherwise */ - boolean accepts(final Geocache cache); + boolean accepts(@NonNull final Geocache cache); + + void filter(@NonNull final List<Geocache> list); - void filter(final List<Geocache> list); + int getImageId(); }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/filter/IFilterFactory.java b/main/src/cgeo/geocaching/filter/IFilterFactory.java index afc99af..82e69da 100644 --- a/main/src/cgeo/geocaching/filter/IFilterFactory.java +++ b/main/src/cgeo/geocaching/filter/IFilterFactory.java @@ -1,7 +1,10 @@ package cgeo.geocaching.filter; +import org.eclipse.jdt.annotation.NonNull; + import java.util.List; interface IFilterFactory { + @NonNull List<? extends IFilter> getFilters(); } diff --git a/main/src/cgeo/geocaching/filter/ModifiedFilter.java b/main/src/cgeo/geocaching/filter/ModifiedFilter.java index 2ac088a..9b5c856 100644 --- a/main/src/cgeo/geocaching/filter/ModifiedFilter.java +++ b/main/src/cgeo/geocaching/filter/ModifiedFilter.java @@ -1,25 +1,27 @@ package cgeo.geocaching.filter; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import java.util.Collections; import java.util.List; class ModifiedFilter extends AbstractFilter implements IFilterFactory { public ModifiedFilter() { - super(CgeoApplication.getInstance().getString(R.string.caches_filter_modified)); + super(R.string.caches_filter_modified); } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { // modified on GC return cache.hasUserModifiedCoords() || cache.hasFinalDefined(); } @Override + @NonNull public List<ModifiedFilter> getFilters() { return Collections.singletonList(this); } diff --git a/main/src/cgeo/geocaching/filter/OfflineLogFilter.java b/main/src/cgeo/geocaching/filter/OfflineLogFilter.java new file mode 100644 index 0000000..0ed9618 --- /dev/null +++ b/main/src/cgeo/geocaching/filter/OfflineLogFilter.java @@ -0,0 +1,19 @@ +package cgeo.geocaching.filter; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; + +import org.eclipse.jdt.annotation.NonNull; + +public class OfflineLogFilter extends AbstractFilter { + + protected OfflineLogFilter() { + super(R.string.caches_filter_offline_log); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isLogOffline(); + } + +} diff --git a/main/src/cgeo/geocaching/filter/OriginFilter.java b/main/src/cgeo/geocaching/filter/OriginFilter.java index 99d1c05..d51b02c 100644 --- a/main/src/cgeo/geocaching/filter/OriginFilter.java +++ b/main/src/cgeo/geocaching/filter/OriginFilter.java @@ -4,6 +4,8 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.IConnector; +import org.eclipse.jdt.annotation.NonNull; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -13,22 +15,23 @@ public class OriginFilter extends AbstractFilter { private final IConnector connector; - public OriginFilter(final IConnector connector) { + public OriginFilter(@NonNull final IConnector connector) { super(connector.getName()); this.connector = connector; } @Override - public final boolean accepts(final Geocache cache) { + public final boolean accepts(@NonNull final Geocache cache) { return ConnectorFactory.getConnector(cache) == connector; } public static final class Factory implements IFilterFactory { @Override + @NonNull public List<OriginFilter> getFilters() { final ArrayList<OriginFilter> filters = new ArrayList<>(); - for (IConnector connector : ConnectorFactory.getConnectors()) { + for (final IConnector connector : ConnectorFactory.getConnectors()) { filters.add(new OriginFilter(connector)); } diff --git a/main/src/cgeo/geocaching/filter/OwnRatingFilter.java b/main/src/cgeo/geocaching/filter/OwnRatingFilter.java new file mode 100644 index 0000000..1b86bab --- /dev/null +++ b/main/src/cgeo/geocaching/filter/OwnRatingFilter.java @@ -0,0 +1,34 @@ +package cgeo.geocaching.filter; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; +import cgeo.geocaching.gcvote.GCVote; + +import org.eclipse.jdt.annotation.NonNull; + +import java.util.Collections; +import java.util.List; + +/** + * Filter {@link Geocache}s if they have a locally stored <b>own</b> {@link GCVote} rating. This filter will not do any + * network request to find potentially missing local votes. + * + */ +public class OwnRatingFilter extends AbstractFilter implements IFilterFactory { + + protected OwnRatingFilter() { + super(R.string.caches_filter_own_rating); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.getMyVote() > 0; + } + + @Override + @NonNull + public List<OwnRatingFilter> getFilters() { + return Collections.singletonList(this); + } + +} diff --git a/main/src/cgeo/geocaching/filter/PersonalDataFilterFactory.java b/main/src/cgeo/geocaching/filter/PersonalDataFilterFactory.java new file mode 100644 index 0000000..e9780da --- /dev/null +++ b/main/src/cgeo/geocaching/filter/PersonalDataFilterFactory.java @@ -0,0 +1,16 @@ +package cgeo.geocaching.filter; + +import org.eclipse.jdt.annotation.NonNull; + +import java.util.Arrays; +import java.util.List; + +public class PersonalDataFilterFactory implements IFilterFactory { + + @Override + @NonNull + public List<? extends IFilter> getFilters() { + return Arrays.asList(new OwnRatingFilter(), new PersonalNoteFilter(), new ModifiedFilter(), new OfflineLogFilter()); + } + +} diff --git a/main/src/cgeo/geocaching/filter/PersonalNoteFilter.java b/main/src/cgeo/geocaching/filter/PersonalNoteFilter.java index 15d262f..11c623e 100644 --- a/main/src/cgeo/geocaching/filter/PersonalNoteFilter.java +++ b/main/src/cgeo/geocaching/filter/PersonalNoteFilter.java @@ -1,26 +1,30 @@ package cgeo.geocaching.filter; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import java.util.Collections; import java.util.List; +/** + * Filter that accepts {@link Geocache}s with a non empty personal note stored locally. + */ public class PersonalNoteFilter extends AbstractFilter implements IFilterFactory { protected PersonalNoteFilter() { - super(CgeoApplication.getInstance().getString(R.string.caches_filter_personal_note)); + super(R.string.caches_filter_personal_note); } @Override - public boolean accepts(Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { return StringUtils.isNotBlank(cache.getPersonalNote()); } @Override + @NonNull public List<PersonalNoteFilter> getFilters() { return Collections.singletonList(this); } diff --git a/main/src/cgeo/geocaching/filter/PopularityFilter.java b/main/src/cgeo/geocaching/filter/PopularityFilter.java index a0244b9..eabc533 100644 --- a/main/src/cgeo/geocaching/filter/PopularityFilter.java +++ b/main/src/cgeo/geocaching/filter/PopularityFilter.java @@ -4,6 +4,8 @@ import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import java.util.ArrayList; import java.util.List; @@ -11,14 +13,14 @@ class PopularityFilter extends AbstractFilter { private final int minFavorites; private final int maxFavorites; - public PopularityFilter(String name, final int minFavorites, final int maxFavorites) { + public PopularityFilter(@NonNull final String name, final int minFavorites, final int maxFavorites) { super(name); this.minFavorites = minFavorites; this.maxFavorites = maxFavorites; } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { return (cache.getFavoritePoints() > minFavorites) && (cache.getFavoritePoints() <= maxFavorites); } @@ -27,10 +29,10 @@ class PopularityFilter extends AbstractFilter { private static final int[] FAVORITES = { 10, 20, 50, 100, 200, 500 }; @Override + @NonNull public List<IFilter> getFilters() { final List<IFilter> filters = new ArrayList<>(FAVORITES.length); - for (int i = 0; i < FAVORITES.length; i++) { - final int minRange = FAVORITES[i]; + for (final int minRange : FAVORITES) { final int maxRange = Integer.MAX_VALUE; final String range = "> " + minRange; final String name = CgeoApplication.getInstance().getResources().getQuantityString(R.plurals.favorite_points, minRange, range); diff --git a/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java b/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java index a04f219..0548345 100644 --- a/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java +++ b/main/src/cgeo/geocaching/filter/PopularityRatioFilter.java @@ -6,6 +6,8 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.enumerations.LogType; +import org.eclipse.jdt.annotation.NonNull; + import java.util.ArrayList; import java.util.List; @@ -16,14 +18,14 @@ class PopularityRatioFilter extends AbstractFilter { private final int minRatio; private final int maxRatio; - public PopularityRatioFilter(String name, final int minRatio, final int maxRatio) { + public PopularityRatioFilter(@NonNull final String name, final int minRatio, final int maxRatio) { super(name); this.minRatio = minRatio; this.maxRatio = maxRatio; } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { final int finds = getFindsCount(cache); if (finds == 0) { // Prevent division by zero @@ -35,11 +37,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; } @@ -51,12 +53,12 @@ class PopularityRatioFilter extends AbstractFilter { private static final int[] RATIOS = { 10, 20, 30, 40, 50, 75 }; @Override + @NonNull public List<IFilter> getFilters() { final List<IFilter> filters = new ArrayList<>(RATIOS.length); - for (int i = 0; i < RATIOS.length; i++) { - final int minRange = RATIOS[i]; + 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/filter/RatingFilter.java b/main/src/cgeo/geocaching/filter/RatingFilter.java new file mode 100644 index 0000000..16ce16c --- /dev/null +++ b/main/src/cgeo/geocaching/filter/RatingFilter.java @@ -0,0 +1,25 @@ +package cgeo.geocaching.filter; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; +import cgeo.geocaching.gcvote.GCVote; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * Filter {@link Geocache}s if they have a locally stored {@link GCVote} rating. This filter will not do any network + * request to find potentially missing local votes. + * + */ +public class RatingFilter extends AbstractFilter { + + protected RatingFilter() { + super(R.string.caches_filter_rating); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.getRating() > 0; + } + +} diff --git a/main/src/cgeo/geocaching/filter/SizeFilter.java b/main/src/cgeo/geocaching/filter/SizeFilter.java index f02874c..a19d95b 100644 --- a/main/src/cgeo/geocaching/filter/SizeFilter.java +++ b/main/src/cgeo/geocaching/filter/SizeFilter.java @@ -3,23 +3,26 @@ package cgeo.geocaching.filter; import cgeo.geocaching.Geocache; import cgeo.geocaching.enumerations.CacheSize; +import org.eclipse.jdt.annotation.NonNull; + import java.util.LinkedList; import java.util.List; class SizeFilter extends AbstractFilter { private final CacheSize cacheSize; - public SizeFilter(final CacheSize cacheSize) { + public SizeFilter(@NonNull final CacheSize cacheSize) { super(cacheSize.id); this.cacheSize = cacheSize; } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { return cacheSize == cache.getSize(); } @Override + @NonNull public String getName() { return cacheSize.getL10n(); } @@ -27,16 +30,17 @@ class SizeFilter extends AbstractFilter { public static class Factory implements IFilterFactory { @Override + @NonNull public List<IFilter> getFilters() { final CacheSize[] cacheSizes = CacheSize.values(); final List<IFilter> filters = new LinkedList<>(); - for (CacheSize cacheSize : cacheSizes) { + for (final CacheSize cacheSize : cacheSizes) { if (cacheSize != CacheSize.UNKNOWN) { filters.add(new SizeFilter(cacheSize)); } } return filters; } - } + } diff --git a/main/src/cgeo/geocaching/filter/StateFilter.java b/main/src/cgeo/geocaching/filter/StateFilter.java deleted file mode 100644 index ebe133c..0000000 --- a/main/src/cgeo/geocaching/filter/StateFilter.java +++ /dev/null @@ -1,153 +0,0 @@ -package cgeo.geocaching.filter; - -import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.Geocache; -import cgeo.geocaching.R; - -import android.content.res.Resources; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -abstract class StateFilter extends AbstractFilter { - - static final Resources res = CgeoApplication.getInstance().getResources(); - - protected StateFilter(final String name) { - super(name); - } - - static class StateFoundFilter extends StateFilter { - - public StateFoundFilter() { - super(res.getString(R.string.cache_status_found)); - } - - @Override - public boolean accepts(final Geocache cache) { - return cache.isFound(); - } - - } - - static class StateNotFoundFilter extends StateFilter { - - public StateNotFoundFilter() { - super(res.getString(R.string.cache_not_status_found)); - } - - @Override - public boolean accepts(final Geocache cache) { - return !cache.isFound(); - } - - } - - static class StateArchivedFilter extends StateFilter { - public StateArchivedFilter() { - super(res.getString(R.string.cache_status_archived)); - } - - @Override - public boolean accepts(final Geocache cache) { - return cache.isArchived(); - } - } - - static class StateDisabledFilter extends StateFilter { - public StateDisabledFilter() { - super(res.getString(R.string.cache_status_disabled)); - } - - @Override - public boolean accepts(final Geocache cache) { - return cache.isDisabled(); - } - } - - static class StatePremiumFilter extends StateFilter { - public StatePremiumFilter() { - super(res.getString(R.string.cache_status_premium)); - } - - @Override - public boolean accepts(final Geocache cache) { - return cache.isPremiumMembersOnly(); - } - } - - static class StateNonPremiumFilter extends StateFilter { - public StateNonPremiumFilter() { - super(res.getString(R.string.cache_status_not_premium)); - } - - @Override - public boolean accepts(final Geocache cache) { - return !cache.isPremiumMembersOnly(); - } - } - - private static class StateOfflineLogFilter extends StateFilter { - public StateOfflineLogFilter() { - super(res.getString(R.string.cache_status_offline_log)); - } - - @Override - public boolean accepts(final Geocache cache) { - return cache.isLogOffline(); - } - } - - static class StateStoredFilter extends StateFilter { - public StateStoredFilter() { - super(res.getString(R.string.cache_status_stored)); - } - - @Override - public boolean accepts(final Geocache cache) { - return cache.isOffline(); - } - } - - static class StateNotStoredFilter extends StateFilter { - public StateNotStoredFilter() { - super(res.getString(R.string.cache_status_not_stored)); - } - - @Override - public boolean accepts(final Geocache cache) { - return !cache.isOffline(); - } - } - - public static class Factory implements IFilterFactory { - - @Override - public List<StateFilter> getFilters() { - final List<StateFilter> filters = new ArrayList<>(6); - filters.add(new StateFoundFilter()); - filters.add(new StateNotFoundFilter()); - filters.add(new StateArchivedFilter()); - filters.add(new StateDisabledFilter()); - filters.add(new StatePremiumFilter()); - filters.add(new StateNonPremiumFilter()); - filters.add(new StateOfflineLogFilter()); - filters.add(new StateStoredFilter()); - filters.add(new StateNotStoredFilter()); - - Collections.sort(filters, new Comparator<StateFilter>() { - - @Override - public int compare(final StateFilter filter1, final StateFilter filter2) { - return String.CASE_INSENSITIVE_ORDER.compare(filter1.getName(), filter2.getName()); - } - }); - - return filters; - } - - } - -} diff --git a/main/src/cgeo/geocaching/filter/StateFilterFactory.java b/main/src/cgeo/geocaching/filter/StateFilterFactory.java new file mode 100644 index 0000000..42de764 --- /dev/null +++ b/main/src/cgeo/geocaching/filter/StateFilterFactory.java @@ -0,0 +1,145 @@ +package cgeo.geocaching.filter; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; + +import org.eclipse.jdt.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class StateFilterFactory implements IFilterFactory { + + @Override + @NonNull + public List<? extends IFilter> getFilters() { + final List<AbstractFilter> filters = new ArrayList<>(6); + filters.add(new StateFoundFilter()); + filters.add(new StateNotFoundFilter()); + filters.add(new StateArchivedFilter()); + filters.add(new StateDisabledFilter()); + filters.add(new StatePremiumFilter()); + filters.add(new StateNonPremiumFilter()); + filters.add(new StateOfflineLogFilter()); + filters.add(new StateStoredFilter()); + filters.add(new StateNotStoredFilter()); + filters.add(new RatingFilter()); + filters.add(new TrackablesFilter()); + + Collections.sort(filters, new Comparator<AbstractFilter>() { + + @Override + public int compare(final AbstractFilter filter1, final AbstractFilter filter2) { + return String.CASE_INSENSITIVE_ORDER.compare(filter1.getName(), filter2.getName()); + } + }); + + return filters; + } + + static class StateFoundFilter extends AbstractFilter { + + public StateFoundFilter() { + super(R.string.cache_status_found); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isFound(); + } + + } + + static class StateNotFoundFilter extends AbstractFilter { + + public StateNotFoundFilter() { + super(R.string.cache_not_status_found); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return !cache.isFound(); + } + + } + + static class StateArchivedFilter extends AbstractFilter { + public StateArchivedFilter() { + super(R.string.cache_status_archived); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isArchived(); + } + } + + static class StateDisabledFilter extends AbstractFilter { + public StateDisabledFilter() { + super(R.string.cache_status_disabled); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isDisabled(); + } + } + + static class StatePremiumFilter extends AbstractFilter { + public StatePremiumFilter() { + super(R.string.cache_status_premium); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isPremiumMembersOnly(); + } + } + + static class StateNonPremiumFilter extends AbstractFilter { + public StateNonPremiumFilter() { + super(R.string.cache_status_not_premium); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return !cache.isPremiumMembersOnly(); + } + } + + private static class StateOfflineLogFilter extends AbstractFilter { + public StateOfflineLogFilter() { + super(R.string.cache_status_offline_log); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isLogOffline(); + } + } + + static class StateStoredFilter extends AbstractFilter { + public StateStoredFilter() { + super(R.string.cache_status_stored); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return cache.isOffline(); + } + } + + static class StateNotStoredFilter extends AbstractFilter { + public StateNotStoredFilter() { + super(R.string.cache_status_not_stored); + } + + @Override + public boolean accepts(@NonNull final Geocache cache) { + return !cache.isOffline(); + } + } + +} diff --git a/main/src/cgeo/geocaching/filter/TerrainFilter.java b/main/src/cgeo/geocaching/filter/TerrainFilter.java index 7da6a19..977e4a5 100644 --- a/main/src/cgeo/geocaching/filter/TerrainFilter.java +++ b/main/src/cgeo/geocaching/filter/TerrainFilter.java @@ -3,6 +3,8 @@ package cgeo.geocaching.filter; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; +import org.eclipse.jdt.annotation.NonNull; + import java.util.ArrayList; import java.util.List; @@ -13,7 +15,7 @@ class TerrainFilter extends AbstractRangeFilter { } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { final float terrain = cache.getTerrain(); return rangeMin <= terrain && terrain < rangeMax; } @@ -23,6 +25,7 @@ class TerrainFilter extends AbstractRangeFilter { private static final int TERRAIN_MAX = 7; @Override + @NonNull public List<IFilter> getFilters() { final ArrayList<IFilter> filters = new ArrayList<>(TERRAIN_MAX); for (int terrain = TERRAIN_MIN; terrain <= TERRAIN_MAX; terrain++) { diff --git a/main/src/cgeo/geocaching/filter/TrackablesFilter.java b/main/src/cgeo/geocaching/filter/TrackablesFilter.java index d836a0f..debe11f 100644 --- a/main/src/cgeo/geocaching/filter/TrackablesFilter.java +++ b/main/src/cgeo/geocaching/filter/TrackablesFilter.java @@ -1,25 +1,18 @@ package cgeo.geocaching.filter; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; -import java.util.Collections; -import java.util.List; +import org.eclipse.jdt.annotation.NonNull; -class TrackablesFilter extends AbstractFilter implements IFilterFactory { +class TrackablesFilter extends AbstractFilter { public TrackablesFilter() { - super(CgeoApplication.getInstance().getString(R.string.caches_filter_track)); + super(R.string.caches_filter_track); } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { return cache.hasTrackables(); } - @Override - public List<TrackablesFilter> getFilters() { - return Collections.singletonList(this); - } - } diff --git a/main/src/cgeo/geocaching/filter/TypeFilter.java b/main/src/cgeo/geocaching/filter/TypeFilter.java index d363d39..b8c879f 100644 --- a/main/src/cgeo/geocaching/filter/TypeFilter.java +++ b/main/src/cgeo/geocaching/filter/TypeFilter.java @@ -3,23 +3,26 @@ package cgeo.geocaching.filter; import cgeo.geocaching.Geocache; import cgeo.geocaching.enumerations.CacheType; +import org.eclipse.jdt.annotation.NonNull; + import java.util.LinkedList; import java.util.List; class TypeFilter extends AbstractFilter { private final CacheType cacheType; - public TypeFilter(final CacheType cacheType) { + public TypeFilter(@NonNull final CacheType cacheType) { super(cacheType.id); this.cacheType = cacheType; } @Override - public boolean accepts(final Geocache cache) { + public boolean accepts(@NonNull final Geocache cache) { return cacheType == cache.getType(); } @Override + @NonNull public String getName() { return cacheType.getL10n(); } @@ -27,10 +30,11 @@ class TypeFilter extends AbstractFilter { public static class Factory implements IFilterFactory { @Override + @NonNull public List<IFilter> getFilters() { final CacheType[] types = CacheType.values(); final List<IFilter> filters = new LinkedList<>(); - for (CacheType cacheType : types) { + for (final CacheType cacheType : types) { if (cacheType != CacheType.ALL) { filters.add(new TypeFilter(cacheType)); } @@ -39,4 +43,9 @@ class TypeFilter extends AbstractFilter { } } + + @Override + public int getImageId() { + return cacheType.markerId; + } } diff --git a/main/src/cgeo/geocaching/gcvote/GCVote.java b/main/src/cgeo/geocaching/gcvote/GCVote.java index 8de3edc..2985e89 100644 --- a/main/src/cgeo/geocaching/gcvote/GCVote.java +++ b/main/src/cgeo/geocaching/gcvote/GCVote.java @@ -8,13 +8,18 @@ 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; @@ -22,17 +27,9 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; 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); @@ -46,10 +43,6 @@ public final class GCVote { /** * Get user rating for a given guid or geocode. For a guid first the ratings cache is checked * before a request to gcvote.com is made. - * - * @param guid - * @param geocode - * @return */ public static GCVoteRating getRating(final String guid, final String geocode) { if (StringUtils.isNotBlank(guid) && RATINGS_CACHE.containsKey(guid)) { @@ -66,161 +59,100 @@ public final class GCVote { /** * Get user ratings from gcvote.com - * - * @param guids - * @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 id = xpp.getAttributeValue(null, requestByGuids ? "cacheId" : "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; + } } /** * Transmit user vote to gcvote.com * - * @param cache - * @param vote + * @param cache the geocache (supported by GCVote) + * @param rating the rating * @return {@code true} if the rating was submitted successfully */ - public static boolean setRating(final Geocache cache, final float vote) { + public static boolean setRating(final Geocache cache, final float rating) { if (!isVotingPossible(cache)) { - return false; + throw new IllegalArgumentException("voting is not possible for " + cache); } - if (!isValidRating(vote)) { - return false; + if (!isValidRating(rating)) { + throw new IllegalArgumentException("invalid rating " + rating); } final ImmutablePair<String, String> login = Settings.getGCvoteLogin(); - if (login == null) { - return false; - } - final Parameters params = new Parameters( "userName", login.left, "password", login.right, "cacheId", cache.getGuid(), - "voteUser", String.format("%.1f", vote).replace(',', '.'), + "waypoint", cache.getGeocode(), + "voteUser", String.format(Locale.US, "%.1f", rating), "version", "cgeo"); - final String result = Network.getResponseData(Network.getRequest("http://gcvote.com/setVote.php", params)); - - return result != null && result.trim().equalsIgnoreCase("ok"); + final String result = StringUtils.trim(Network.getResponseData(Network.getRequest("http://gcvote.com/setVote.php", params))); + if (!StringUtils.equalsIgnoreCase(result, "ok")) { + Log.e("GCVote.setRating: could not post rating, answer was " + result); + return false; + } + return true; } public static void loadRatings(final @NonNull ArrayList<Geocache> caches) { @@ -236,34 +168,29 @@ 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 (final Geocache cache : caches) { + if (ratings.containsKey(cache.getGeocode())) { + final 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) { + } catch (final Exception e) { Log.e("GCvote.loadRatings", e); } } /** * Get geocodes of all the caches, which can be used with GCVote. Non-GC caches will be filtered out. - * - * @param caches - * @return */ private static @NonNull ArrayList<String> getVotableGeocodes(final @NonNull Collection<Geocache> caches) { final ArrayList<String> geocodes = new ArrayList<>(caches.size()); for (final Geocache cache : caches) { - String geocode = cache.getGeocode(); + final String geocode = cache.getGeocode(); if (StringUtils.isNotBlank(geocode) && cache.supportsGCVote()) { geocodes.add(geocode); } @@ -275,11 +202,7 @@ public final class GCVote { return rating >= MIN_RATING && rating <= MAX_RATING; } - public static String getRatingText(final float rating) { - return String.format(Locale.getDefault(), "%.1f", rating); - } - - public static boolean isVotingPossible(final Geocache cache) { + public static boolean isVotingPossible(@NonNull final Geocache cache) { return Settings.isGCvoteLogin() && StringUtils.isNotBlank(cache.getGuid()) && cache.supportsGCVote(); } @@ -308,7 +231,7 @@ public final class GCVote { } } - private static String getString(int resId) { + private static String getString(final int resId) { return CgeoApplication.getInstance().getString(resId); } diff --git a/main/src/cgeo/geocaching/gcvote/GCVoteDialog.java b/main/src/cgeo/geocaching/gcvote/GCVoteDialog.java new file mode 100644 index 0000000..0738274 --- /dev/null +++ b/main/src/cgeo/geocaching/gcvote/GCVoteDialog.java @@ -0,0 +1,112 @@ +package cgeo.geocaching.gcvote; + +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.DataStore; +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; +import cgeo.geocaching.gcvote.GCVoteRatingBarUtil.OnRatingChangeListener; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; + +import org.eclipse.jdt.annotation.Nullable; + +import rx.functions.Action1; +import rx.functions.Func0; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +/** + * Small dialog showing only a rating bar to vote on GCVote.com. Confirming the dialog will send the vote over the + * network (in the background). + */ +public class GCVoteDialog { + + public static void show(final Activity context, final Geocache cache, final @Nullable Runnable afterVoteSent) { + final Context themedContext; + + if (Settings.isLightSkin() && VERSION.SDK_INT < VERSION_CODES.HONEYCOMB) { + themedContext = new ContextThemeWrapper(context, R.style.dark); + } else { + themedContext = context; + } + + final View votingLayout = View.inflate(themedContext, R.layout.gcvote_dialog, null); + + final AlertDialog.Builder builder = new AlertDialog.Builder(themedContext); + builder.setView(votingLayout); + builder.setPositiveButton(R.string.cache_menu_vote, new OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + vote(cache, GCVoteRatingBarUtil.getRating(votingLayout), afterVoteSent); + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int whichButton) { + dialog.dismiss(); + } + }); + final AlertDialog dialog = builder.create(); + + GCVoteRatingBarUtil.initializeRatingBar(cache, votingLayout, new OnRatingChangeListener() { + + @Override + public void onRatingChanged(final float stars) { + final Button button = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + // this listener might be fired already while the dialog is not yet shown + if (button != null) { + button.setEnabled(GCVote.isValidRating(stars)); + } + } + }); + dialog.show(); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(GCVote.isValidRating(cache.getMyVote())); + } + + private static void vote(final Geocache cache, final float rating, final @Nullable Runnable afterVoteSent) { + RxUtils.andThenOnUi(RxUtils.networkScheduler, new Func0<Boolean>() { + @Override + public Boolean call() { + try { + if (GCVote.isValidRating(rating) && GCVote.isVotingPossible(cache)) { + // send over network + if (GCVote.setRating(cache, rating)) { + // store locally + cache.setMyVote(rating); + DataStore.saveChangedCache(cache); + return true; + } + Log.w("GCVoteDialog.vote: could not send vote"); + } + } catch (final RuntimeException e) { + Log.e("GCVoteDialog.vote: could not send vote", e); + } + + return false; + } + }, new Action1<Boolean>() { + @Override + public void call(final Boolean status) { + final CgeoApplication context = CgeoApplication.getInstance(); + final String text = context.getString(status ? R.string.gcvote_sent : R.string.err_gcvote_send_rating); + Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); + if (afterVoteSent != null) { + afterVoteSent.run(); + } + } + }); + } + +} diff --git a/main/src/cgeo/geocaching/gcvote/GCVoteRatingBarUtil.java b/main/src/cgeo/geocaching/gcvote/GCVoteRatingBarUtil.java new file mode 100644 index 0000000..2d485bd --- /dev/null +++ b/main/src/cgeo/geocaching/gcvote/GCVoteRatingBarUtil.java @@ -0,0 +1,58 @@ +package cgeo.geocaching.gcvote; + +import butterknife.ButterKnife; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.R; + +import org.eclipse.jdt.annotation.Nullable; + +import android.view.View; +import android.widget.RatingBar; +import android.widget.RatingBar.OnRatingBarChangeListener; +import android.widget.TextView; + +/** + * TODO: convert to fragment + * + */ +public final class GCVoteRatingBarUtil { + public interface OnRatingChangeListener { + public void onRatingChanged(final float stars); + } + + private GCVoteRatingBarUtil() { + // utility class + } + + public static void initializeRatingBar(final Geocache cache, final View parentView, @Nullable final OnRatingChangeListener changeListener) { + if (GCVote.isVotingPossible(cache)) { + final RatingBar ratingBar = ButterKnife.findById(parentView, R.id.gcvoteRating); + final TextView label = ButterKnife.findById(parentView, R.id.gcvoteLabel); + ratingBar.setVisibility(View.VISIBLE); + label.setVisibility(View.VISIBLE); + ratingBar.setOnRatingBarChangeListener(new OnRatingBarChangeListener() { + + @Override + public void onRatingChanged(final RatingBar ratingBar, final float stars, final boolean fromUser) { + // 0.5 is not a valid rating, therefore we must limit + final float rating = GCVote.isValidRating(stars) ? stars : 0; + if (rating < stars) { + ratingBar.setRating(rating); + } + label.setText(GCVote.getDescription(rating)); + if (changeListener != null) { + changeListener.onRatingChanged(rating); + } + } + }); + ratingBar.setRating(cache.getMyVote()); + } + } + + public static float getRating(final View parentView) { + final RatingBar ratingBar = ButterKnife.findById(parentView, R.id.gcvoteRating); + return ratingBar.getRating(); + } + +} diff --git a/main/src/cgeo/geocaching/list/AbstractList.java b/main/src/cgeo/geocaching/list/AbstractList.java index 9b57b3a..5836e6c 100644 --- a/main/src/cgeo/geocaching/list/AbstractList.java +++ b/main/src/cgeo/geocaching/list/AbstractList.java @@ -8,7 +8,7 @@ public abstract class AbstractList { public final int id; public final String title; - private static SparseArray<AbstractList> LISTS = new SparseArray<>(); + private final static SparseArray<AbstractList> LISTS = new SparseArray<>(); public AbstractList(final int id, final String title) { this.id = id; @@ -25,7 +25,7 @@ public abstract class AbstractList { public abstract int getNumberOfCaches(); @Nullable - public static AbstractList getListById(int listId) { + public static AbstractList getListById(final int listId) { return LISTS.get(listId); } diff --git a/main/src/cgeo/geocaching/list/ListNameMemento.java b/main/src/cgeo/geocaching/list/ListNameMemento.java new file mode 100644 index 0000000..6d5d481 --- /dev/null +++ b/main/src/cgeo/geocaching/list/ListNameMemento.java @@ -0,0 +1,21 @@ +package cgeo.geocaching.list; + +import org.apache.commons.lang3.StringUtils; + +/** + * Memento to remember list name suggestions from search terms. + */ +public class ListNameMemento { + public static final ListNameMemento EMPTY = new ListNameMemento(); + private String newListName = StringUtils.EMPTY; + + public String rememberTerm(final String term) { + newListName = term; + return term; + } + + public String getTerm() { + return newListName; + } + +} diff --git a/main/src/cgeo/geocaching/list/StoredList.java b/main/src/cgeo/geocaching/list/StoredList.java index 53632a0..0ffd58a 100644 --- a/main/src/cgeo/geocaching/list/StoredList.java +++ b/main/src/cgeo/geocaching/list/StoredList.java @@ -24,12 +24,12 @@ import java.util.Comparator; import java.util.List; public final class StoredList extends AbstractList { - public static final int TEMPORARY_LIST_ID = 0; - public static final StoredList TEMPORARY_LIST = new StoredList(TEMPORARY_LIST_ID, "<temporary>", 0); // Never displayed + private static final int TEMPORARY_LIST_ID = 0; + public static final StoredList TEMPORARY_LIST = new StoredList(TEMPORARY_LIST_ID, "<temporary>", 0); // Never displayed public static final int STANDARD_LIST_ID = 1; private final int count; // this value is only valid as long as the list is not changed by other database operations - public StoredList(int id, String title, int count) { + public StoredList(final int id, final String title, final int count) { super(id, title); this.count = count; } @@ -48,7 +48,7 @@ public final class StoredList extends AbstractList { } @Override - public boolean equals(Object obj) { + public boolean equals(final Object obj) { if (this == obj) { return true; } @@ -69,34 +69,30 @@ public final class StoredList extends AbstractList { res = app.getResources(); } - public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards) { - promptForListSelection(titleId, runAfterwards, false, -1); - } - public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId) { - promptForListSelection(titleId, runAfterwards, onlyConcreteLists, exceptListId, StringUtils.EMPTY); + promptForListSelection(titleId, runAfterwards, onlyConcreteLists, exceptListId, ListNameMemento.EMPTY); } - public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId, final String newListName) { + public void promptForListSelection(final int titleId, @NonNull final Action1<Integer> runAfterwards, final boolean onlyConcreteLists, final int exceptListId, final @NonNull ListNameMemento listNameMemento) { final List<AbstractList> lists = getMenuLists(onlyConcreteLists, exceptListId); final List<CharSequence> listsTitle = new ArrayList<>(); - for (AbstractList list : lists) { + for (final AbstractList list : lists) { listsTitle.add(list.getTitleAndCount()); } final CharSequence[] items = new CharSequence[listsTitle.size()]; final Activity activity = activityRef.get(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(res.getString(titleId)); builder.setItems(listsTitle.toArray(items), new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialogInterface, int itemId) { + public void onClick(final DialogInterface dialogInterface, final int itemId) { final AbstractList list = lists.get(itemId); if (list == PseudoList.NEW_LIST) { // create new list on the fly - promptForListCreation(runAfterwards, newListName); + promptForListCreation(runAfterwards, listNameMemento.getTerm()); } else { runAfterwards.call(lists.get(itemId).id); @@ -106,15 +102,12 @@ public final class StoredList extends AbstractList { builder.create().show(); } - public static List<AbstractList> getMenuLists(boolean onlyConcreteLists, int exceptListId) { + public static List<AbstractList> getMenuLists(final boolean onlyConcreteLists, final int exceptListId) { final List<AbstractList> lists = new ArrayList<>(); lists.addAll(getSortedLists()); - if (exceptListId > StoredList.TEMPORARY_LIST_ID) { - StoredList exceptList = DataStore.getList(exceptListId); - if (exceptList != null) { - lists.remove(exceptList); - } + if (exceptListId == StoredList.STANDARD_LIST_ID || exceptListId >= DataStore.customListIdOffset) { + lists.remove(DataStore.getList(exceptListId)); } if (!onlyConcreteLists) { @@ -138,7 +131,7 @@ public final class StoredList extends AbstractList { Collections.sort(lists, new Comparator<StoredList>() { @Override - public int compare(StoredList lhs, StoredList rhs) { + public int compare(final StoredList lhs, final StoredList rhs) { // have the standard list at the top if (lhs.id == STANDARD_LIST_ID) { return -1; @@ -153,7 +146,7 @@ public final class StoredList extends AbstractList { return lists; } - public void promptForListCreation(@NonNull final Action1<Integer> runAfterwards, String newListName) { + public void promptForListCreation(@NonNull final Action1<Integer> runAfterwards, final String newListName) { handleListNameInput(newListName, R.string.list_dialog_create_title, R.string.list_dialog_create, new Action1<String>() { // We need to update the list cache by creating a new StoredList object here. @@ -177,7 +170,7 @@ public final class StoredList extends AbstractList { }); } - private void handleListNameInput(final String defaultValue, int dialogTitle, int buttonTitle, final Action1<String> runnable) { + private void handleListNameInput(final String defaultValue, final int dialogTitle, final int buttonTitle, final Action1<String> runnable) { final Activity activity = activityRef.get(); if (activity == null) { return; @@ -187,7 +180,7 @@ public final class StoredList extends AbstractList { @Override public void call(final String input) { // remove whitespaces added by autocompletion of Android keyboard - String listName = StringUtils.trim(input); + final String listName = StringUtils.trim(input); if (StringUtils.isNotBlank(listName)) { runnable.call(listName); } @@ -225,8 +218,8 @@ public final class StoredList extends AbstractList { /** * Return the given list, if it is a concrete list. Return the default list otherwise. */ - public static int getConcreteList(int listId) { - if (listId == PseudoList.ALL_LIST.id || listId == TEMPORARY_LIST_ID || listId == PseudoList.HISTORY_LIST.id) { + public static int getConcreteList(final int listId) { + if (listId == PseudoList.ALL_LIST.id || listId == TEMPORARY_LIST.id || listId == PseudoList.HISTORY_LIST.id) { return STANDARD_LIST_ID; } return listId; diff --git a/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java b/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java index b2cb0b2..8e4b114 100644 --- a/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java +++ b/main/src/cgeo/geocaching/loaders/AbstractSearchLoader.java @@ -29,7 +29,6 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> FINDER, OWNER, MAP, - REMOVE_FROM_HISTORY, NEXT_PAGE; public int getLoaderId() { @@ -43,9 +42,9 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> private String recaptchaText = null; private SearchResult search; private boolean loading; - private CountDownLatch latch = new CountDownLatch(1); + private final CountDownLatch latch = new CountDownLatch(1); - public AbstractSearchLoader(Context context) { + public AbstractSearchLoader(final Context context) { super(context); } @@ -65,7 +64,7 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> // Unless we make a new Search the Loader framework won't deliver results. It does't do equals only identity search = new SearchResult(search); } - } catch (Exception e) { + } catch (final Exception e) { Log.e("Error in Loader ", e); } loading = false; @@ -95,13 +94,13 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> public void waitForUser() { try { latch.await(); - } catch (InterruptedException e) { + } catch (final InterruptedException ignored) { Log.w("searchThread is not waiting for user…"); } } @Override - public void setKey(String key) { + public void setKey(final String key) { recaptchaKey = key; } @@ -125,7 +124,7 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> } @Override - public void setText(String text) { + public void setText(final String text) { recaptchaText = text; latch.countDown(); } diff --git a/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java deleted file mode 100644 index e1573c9..0000000 --- a/main/src/cgeo/geocaching/loaders/AddressGeocacheListLoader.java +++ /dev/null @@ -1,23 +0,0 @@ -package cgeo.geocaching.loaders; - -import cgeo.geocaching.SearchResult; -import cgeo.geocaching.connector.gc.GCParser; -import cgeo.geocaching.settings.Settings; - -import android.content.Context; - -public class AddressGeocacheListLoader extends AbstractSearchLoader { - - private final String address; - - public AddressGeocacheListLoader(Context context, String address) { - super(context); - this.address = address; - } - - @Override - public SearchResult runSearch() { - return GCParser.searchByAddress(address, Settings.getCacheType(), Settings.isShowCaptcha(), this); - } - -} diff --git a/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java index c2d92bf..2549b7b 100644 --- a/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/CoordsGeocacheListLoader.java @@ -3,9 +3,10 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.SearchResult; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ISearchByCenter; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import org.eclipse.jdt.annotation.NonNull; + import rx.functions.Func1; import android.content.Context; diff --git a/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java index fdb35f2..53e5de1 100644 --- a/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/HistoryGeocacheListLoader.java @@ -3,7 +3,7 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.DataStore; import cgeo.geocaching.SearchResult; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.settings.Settings; import android.content.Context; @@ -11,7 +11,7 @@ import android.content.Context; public class HistoryGeocacheListLoader extends AbstractSearchLoader { private final Geopoint coords; - public HistoryGeocacheListLoader(Context context, Geopoint coords) { + public HistoryGeocacheListLoader(final Context context, final Geopoint coords) { super(context); this.coords = coords; } diff --git a/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java index 45b264f..4c3baf5 100644 --- a/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/KeywordGeocacheListLoader.java @@ -13,7 +13,7 @@ public class KeywordGeocacheListLoader extends AbstractSearchLoader { private final @NonNull String keyword; - public KeywordGeocacheListLoader(Context context, final @NonNull String keyword) { + public KeywordGeocacheListLoader(final Context context, final @NonNull String keyword) { super(context); this.keyword = keyword; } diff --git a/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java index 05eac18..cd81619 100644 --- a/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/NextPageGeocacheListLoader.java @@ -9,7 +9,7 @@ import android.content.Context; public class NextPageGeocacheListLoader extends AbstractSearchLoader { private final SearchResult search; - public NextPageGeocacheListLoader(Context context, SearchResult search) { + public NextPageGeocacheListLoader(final Context context, final SearchResult search) { super(context); this.search = search; } diff --git a/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java index 0d5af6a..b9fbc76 100644 --- a/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/OfflineGeocacheListLoader.java @@ -3,7 +3,7 @@ package cgeo.geocaching.loaders; import cgeo.geocaching.DataStore; import cgeo.geocaching.Intents; import cgeo.geocaching.SearchResult; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.settings.Settings; import android.content.Context; @@ -26,7 +26,6 @@ public class OfflineGeocacheListLoader extends AbstractSearchLoader { } /** - * @param listId * @return the bundle needed for querying the LoaderManager for the offline list with the given id */ public static Bundle getBundleForList(final int listId) { diff --git a/main/src/cgeo/geocaching/loaders/PocketGeocacheListLoader.java b/main/src/cgeo/geocaching/loaders/PocketGeocacheListLoader.java index 32fb020..47472be 100644 --- a/main/src/cgeo/geocaching/loaders/PocketGeocacheListLoader.java +++ b/main/src/cgeo/geocaching/loaders/PocketGeocacheListLoader.java @@ -9,7 +9,7 @@ import android.content.Context; public class PocketGeocacheListLoader extends AbstractSearchLoader { private final String guid; - public PocketGeocacheListLoader(Context context, String guid) { + public PocketGeocacheListLoader(final Context context, final String guid) { super(context); this.guid = guid; } diff --git a/main/src/cgeo/geocaching/loaders/RemoveFromHistoryLoader.java b/main/src/cgeo/geocaching/loaders/RemoveFromHistoryLoader.java deleted file mode 100644 index dc1a5df..0000000 --- a/main/src/cgeo/geocaching/loaders/RemoveFromHistoryLoader.java +++ /dev/null @@ -1,28 +0,0 @@ -package cgeo.geocaching.loaders; - -import cgeo.geocaching.DataStore; -import cgeo.geocaching.SearchResult; -import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.settings.Settings; - -import android.content.Context; - -public class RemoveFromHistoryLoader extends AbstractSearchLoader { - - private final String[] selected; - private final Geopoint coords; - - public RemoveFromHistoryLoader(Context context, String[] selected, Geopoint coords) { - super(context); - this.selected = selected.clone(); - this.coords = coords; - } - - @Override - public SearchResult runSearch() { - DataStore.clearVisitDate(selected); - return DataStore.getHistoryOfCaches(true, coords != null ? Settings.getCacheType() : CacheType.ALL); - } - -} diff --git a/main/src/cgeo/geocaching/location/AndroidGeocoder.java b/main/src/cgeo/geocaching/location/AndroidGeocoder.java new file mode 100644 index 0000000..d0d07e8 --- /dev/null +++ b/main/src/cgeo/geocaching/location/AndroidGeocoder.java @@ -0,0 +1,82 @@ +package cgeo.geocaching.location; + +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; + +import org.apache.commons.collections4.CollectionUtils; +import org.eclipse.jdt.annotation.NonNull; + +import rx.Observable; +import rx.functions.Func0; + +import android.content.Context; +import android.location.Address; +import android.location.Geocoder; + +import java.util.List; +import java.util.Locale; + +public class AndroidGeocoder { + private final Geocoder geocoder; + + public AndroidGeocoder(final Context context) { + geocoder = new Geocoder(context, Locale.getDefault()); + } + + /** + * Retrieve addresses from a textual location using Android geocoding API. The work happens on the network + * scheduler. + * + * @param keyword + * the location + * @return an observable containing zero or more locations + * + * @see Geocoder#getFromLocationName(String, int) + */ + public Observable<Address> getFromLocationName(@NonNull final String keyword) { + if (!Geocoder.isPresent()) { + return Observable.error(new RuntimeException("no Android geocoder")); + } + return Observable.defer(new Func0<Observable<Address>>() { + @Override + public Observable<Address> call() { + try { + return addressesToObservable(geocoder.getFromLocationName(keyword, 20)); + } catch (final Exception e) { + Log.i("Unable to use Android reverse geocoder: " + e.getMessage()); + return Observable.error(e); + } + } + }).subscribeOn(RxUtils.networkScheduler); + } + + /** + * Retrieve the physical address for coordinates. The work happens on the network scheduler. + * + * @param coords the coordinates + * @return an observable containing one location or an error + */ + public Observable<Address> getFromLocation(@NonNull final Geopoint coords) { + if (!Geocoder.isPresent()) { + return Observable.error(new RuntimeException("no Android reverse geocoder")); + } + return Observable.defer(new Func0<Observable<Address>>() { + @Override + public Observable<Address> call() { + try { + return addressesToObservable(geocoder.getFromLocation(coords.getLatitude(), coords.getLongitude(), 1)); + } catch (final Exception e) { + Log.i("Unable to use Android reverse geocoder: " + e.getMessage()); + return Observable.error(e); + } + } + }).subscribeOn(RxUtils.networkScheduler).first(); + } + + private static Observable<Address> addressesToObservable(final List<Address> addresses) { + return CollectionUtils.isEmpty(addresses) ? + Observable.<Address>error(new RuntimeException("no result from Android geocoder")) : + Observable.from(addresses); + } + +} diff --git a/main/src/cgeo/geocaching/geopoint/DistanceParser.java b/main/src/cgeo/geocaching/location/DistanceParser.java index e3a7482..7cbc19f 100644 --- a/main/src/cgeo/geocaching/geopoint/DistanceParser.java +++ b/main/src/cgeo/geocaching/location/DistanceParser.java @@ -1,4 +1,4 @@ -package cgeo.geocaching.geopoint; +package cgeo.geocaching.location; import cgeo.geocaching.utils.MatcherWrapper; @@ -22,7 +22,7 @@ public final class DistanceParser { * @throws NumberFormatException * if the given number is invalid */ - public static float parseDistance(String distanceText, final boolean metricUnit) + public static float parseDistance(final String distanceText, final boolean metricUnit) throws NumberFormatException { final MatcherWrapper matcher = new MatcherWrapper(pattern, distanceText); diff --git a/main/src/cgeo/geocaching/location/GCGeocoder.java b/main/src/cgeo/geocaching/location/GCGeocoder.java new file mode 100644 index 0000000..549044f --- /dev/null +++ b/main/src/cgeo/geocaching/location/GCGeocoder.java @@ -0,0 +1,65 @@ +package cgeo.geocaching.location; + +import cgeo.geocaching.network.Network; +import cgeo.geocaching.network.Parameters; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; + +import rx.Observable; +import rx.functions.Func0; + +import android.location.Address; + +import java.util.Locale; + +public class GCGeocoder { + + private GCGeocoder() { + // Do not instantiate + } + + /** + * Retrieve addresses from a textual location using geocaching.com geocoding API. The work happens on the network + * scheduler. + * + * @param address + * the location + * @return an observable containing zero or more locations + * + * @see android.location.Geocoder#getFromLocationName(String, int) + */ + public static Observable<Address> getFromLocationName(@NonNull final String address) { + return Observable.defer(new Func0<Observable<Address>>() { + @Override + public Observable<Address> call() { + if (!Settings.isGCConnectorActive()) { + return Observable.error(new RuntimeException("geocaching.com connector is not active")); + } + final ObjectNode response = Network.requestJSON("https://www.geocaching.com/api/geocode", new Parameters("q", address)); + if (response == null || !StringUtils.equalsIgnoreCase(response.path("status").asText(), "success")) { + return Observable.error(new RuntimeException("unable to use geocaching.com geocoder")); + } + + final JsonNode data = response.path("data"); + final Address geocodedAddress = new Address(Locale.getDefault()); + try { + geocodedAddress.setLatitude(data.get("lat").asDouble()); + geocodedAddress.setLongitude(data.get("lng").asDouble()); + geocodedAddress.setAddressLine(0, address); + return Observable.just(geocodedAddress); + } catch (final Exception e) { + Log.e("unable to decode answer from geocaching.com geocoder", e); + return Observable.error(e); + } + } + }).subscribeOn(RxUtils.networkScheduler); + } + +} diff --git a/main/src/cgeo/geocaching/geopoint/Geopoint.java b/main/src/cgeo/geocaching/location/Geopoint.java index bb34114..a746a1e 100644 --- a/main/src/cgeo/geocaching/geopoint/Geopoint.java +++ b/main/src/cgeo/geocaching/location/Geopoint.java @@ -1,4 +1,4 @@ -package cgeo.geocaching.geopoint; +package cgeo.geocaching.location; import cgeo.geocaching.ICoordinates; import cgeo.geocaching.R; @@ -53,7 +53,7 @@ public final class Geopoint implements ICoordinates, Parcelable { * if the string cannot be parsed * @see GeopointParser#parse(String) */ - public Geopoint(final String text) { + public Geopoint(@NonNull final String text) { final Geopoint parsed = GeopointParser.parse(text); latitude = parsed.latitude; longitude = parsed.longitude; @@ -97,12 +97,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Create new Geopoint from individual textual components. * - * @param latDir - * @param latDeg - * @param latDegFrac - * @param lonDir - * @param lonDeg - * @param lonDegFrac */ public Geopoint(final String latDir, final String latDeg, final String latDegFrac, final String lonDir, final String lonDeg, final String lonDegFrac) { @@ -113,14 +107,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Create new Geopoint from individual textual components. * - * @param latDir - * @param latDeg - * @param latMin - * @param latMinFrac - * @param lonDir - * @param lonDeg - * @param lonMin - * @param lonMinFrac */ public Geopoint(final String latDir, final String latDeg, final String latMin, final String latMinFrac, final String lonDir, final String lonDeg, final String lonMin, final String lonMinFrac) { @@ -131,16 +117,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Create new Geopoint from individual textual components. * - * @param latDir - * @param latDeg - * @param latMin - * @param latSec - * @param latSecFrac - * @param lonDir - * @param lonDeg - * @param lonMin - * @param lonSec - * @param lonSecFrac */ public Geopoint(final String latDir, final String latDeg, final String latMin, final String latSec, final String latSecFrac, final String lonDir, final String lonDeg, final String lonMin, final String lonSec, final String lonSecFrac) { @@ -202,8 +178,8 @@ public final class Geopoint implements ICoordinates, Parcelable { * @return An array of floats: the distance in meters, then the bearing in degrees */ private float[] pathTo(final Geopoint target) { - float[] results = new float[2]; - android.location.Location.distanceBetween(latitude, longitude, target.latitude, target.longitude, results); + final float[] results = new float[2]; + Location.distanceBetween(latitude, longitude, target.latitude, target.longitude, results); return results; } @@ -283,7 +259,7 @@ public final class Geopoint implements ICoordinates, Parcelable { * @see GeopointFormatter * @return formatted coordinates */ - public String format(GeopointFormatter.Format format) { + public String format(final GeopointFormatter.Format format) { return GeopointFormatter.format(format, this); } @@ -301,7 +277,7 @@ public final class Geopoint implements ICoordinates, Parcelable { abstract public static class GeopointException extends NumberFormatException { private static final long serialVersionUID = 1L; - protected GeopointException(String msg) { + protected GeopointException(final String msg) { super(msg); } } @@ -347,7 +323,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get latitude character (N or S). * - * @return */ public char getLatDir() { return latitude >= 0 ? 'N' : 'S'; @@ -356,7 +331,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get longitude character (E or W). * - * @return */ public char getLonDir() { return longitude >= 0 ? 'E' : 'W'; @@ -365,7 +339,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the integral non-negative latitude degrees. * - * @return */ public int getLatDeg() { return getDeg(latitude); @@ -374,7 +347,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the integral non-negative longitude degrees. * - * @return */ public int getLonDeg() { return getDeg(longitude); @@ -387,7 +359,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the fractional part of the latitude degrees scaled up by 10^5. * - * @return */ public int getLatDegFrac() { return getDegFrac(latitude); @@ -396,7 +367,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the fractional part of the longitude degrees scaled up by 10^5. * - * @return */ public int getLonDegFrac() { return getDegFrac(longitude); @@ -409,7 +379,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the integral latitude minutes. * - * @return */ public int getLatMin() { return getMin(latitude); @@ -418,7 +387,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the integral longitude minutes. * - * @return */ public int getLonMin() { return getMin(longitude); @@ -431,7 +399,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the fractional part of the latitude minutes scaled up by 1000. * - * @return */ public int getLatMinFrac() { return getMinFrac(latitude); @@ -440,7 +407,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the fractional part of the longitude minutes scaled up by 1000. * - * @return */ public int getLonMinFrac() { return getMinFrac(longitude); @@ -453,7 +419,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the latitude minutes. * - * @return */ public double getLatMinRaw() { return getMinRaw(latitude); @@ -462,7 +427,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the longitude minutes. * - * @return */ public double getLonMinRaw() { return getMinRaw(longitude); @@ -475,7 +439,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the integral part of the latitude seconds. * - * @return */ public int getLatSec() { return getSec(latitude); @@ -484,7 +447,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the integral part of the longitude seconds. * - * @return */ public int getLonSec() { return getSec(longitude); @@ -497,7 +459,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the fractional part of the latitude seconds scaled up by 1000. * - * @return */ public int getLatSecFrac() { @@ -507,7 +468,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the fractional part of the longitude seconds scaled up by 1000. * - * @return */ public int getLonSecFrac() { @@ -521,7 +481,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the latitude seconds. * - * @return */ public double getLatSecRaw() { return getSecRaw(latitude); @@ -530,7 +489,6 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Get the longitude seconds. * - * @return */ public double getLonSecRaw() { return getSecRaw(longitude); @@ -555,19 +513,19 @@ public final class Geopoint implements ICoordinates, Parcelable { /** * Gets distance in meters (workaround for 4.2.1 JIT bug). */ - public static double getDistance(double lat1, double lon1, double lat2, double lon2) { + public static double getDistance(final double lat1, final double lon1, final double lat2, final double lon2) { // for haversine use R = 6372.8 km instead of 6371 km - double earthRadius = 6372.8; - double dLat = toRadians(lat2 - lat1); - double dLon = toRadians(lon2 - lon1); - double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + final double earthRadius = 6372.8; + final double dLat = toRadians(lat2 - lat1); + final double dLon = toRadians(lon2 - lon1); + final double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); // simplify haversine return (2 * earthRadius * 1000 * Math.asin(Math.sqrt(a))); } - private static double toRadians(double angdeg) { + private static double toRadians(final double angdeg) { return angdeg * DEG_TO_RAD; } diff --git a/main/src/cgeo/geocaching/geopoint/GeopointFormatter.java b/main/src/cgeo/geocaching/location/GeopointFormatter.java index a6965f9..599b6c7 100644 --- a/main/src/cgeo/geocaching/geopoint/GeopointFormatter.java +++ b/main/src/cgeo/geocaching/location/GeopointFormatter.java @@ -1,4 +1,4 @@ -package cgeo.geocaching.geopoint; +package cgeo.geocaching.location; import java.util.Locale; diff --git a/main/src/cgeo/geocaching/geopoint/GeopointParser.java b/main/src/cgeo/geocaching/location/GeopointParser.java index b486f01..273c5b8 100644 --- a/main/src/cgeo/geocaching/geopoint/GeopointParser.java +++ b/main/src/cgeo/geocaching/location/GeopointParser.java @@ -1,9 +1,9 @@ -package cgeo.geocaching.geopoint; - +package cgeo.geocaching.location; import cgeo.geocaching.utils.MatcherWrapper; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import java.util.regex.Pattern; @@ -17,16 +17,16 @@ class GeopointParser { final int matcherPos; final int matcherLength; - public ResultWrapper(final double result, int matcherPos, int stringLength) { + public ResultWrapper(final double result, final int matcherPos, final int stringLength) { this.result = result; this.matcherPos = matcherPos; this.matcherLength = stringLength; } } - // ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) - private static final Pattern PATTERN_LAT = Pattern.compile("\\b([NS]|)\\s*(\\d+)°?(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE); - private static final Pattern PATTERN_LON = Pattern.compile("\\b([WE]|)\\s*(\\d+)°?(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE); + // ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) + private static final Pattern PATTERN_LAT = Pattern.compile("\\b([NS]|)\\s*(\\d+°?|°)(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE); + private static final Pattern PATTERN_LON = Pattern.compile("\\b([WE]|)\\s*(\\d+°?|°)(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE); private static final Pattern PATTERN_BAD_BLANK = Pattern.compile("(\\d)[,.] (\\d{2,})"); @@ -55,7 +55,7 @@ class GeopointParser { * @throws Geopoint.ParseException * if lat or lon could not be parsed */ - public static Geopoint parse(final String text) { + public static Geopoint parse(@NonNull final String text) { final ResultWrapper latitudeWrapper = parseHelper(text, LatLon.LAT); // cut away the latitude part when parsing the longitude final ResultWrapper longitudeWrapper = parseHelper(text.substring(latitudeWrapper.matcherPos + latitudeWrapper.matcherLength), LatLon.LON); @@ -78,55 +78,58 @@ class GeopointParser { /** * Helper for coordinates-parsing * - * @param text - * @param latlon - * @return + * @param text the text to parse + * @param latlon the kind of coordinate to parse + * @return a wrapper with the result and the corresponding matching part + * @throws Geopoint.ParseException if the text cannot be parsed */ - private static ResultWrapper parseHelper(final String text, final LatLon latlon) { - + private static ResultWrapper parseHelper(@NonNull final String text, final LatLon latlon) { MatcherWrapper matcher = new MatcherWrapper(PATTERN_BAD_BLANK, text); - String replaceSpaceAfterComma = matcher.replaceAll("$1.$2"); + final String replaceSpaceAfterComma = matcher.replaceAll("$1.$2"); final Pattern pattern = LatLon.LAT == latlon ? PATTERN_LAT : PATTERN_LON; matcher = new MatcherWrapper(pattern, replaceSpaceAfterComma); try { return new ResultWrapper(Double.valueOf(replaceSpaceAfterComma), 0, text.length()); - } catch (NumberFormatException e1) { + } catch (final NumberFormatException ignored) { // fall through to advanced parsing } - if (matcher.find()) { - final double sign = matcher.group(1).equalsIgnoreCase("S") || matcher.group(1).equalsIgnoreCase("W") ? -1.0 : 1.0; - final double degree = Integer.valueOf(matcher.group(2)).doubleValue(); + try { + if (matcher.find()) { + final double sign = matcher.group(1).equalsIgnoreCase("S") || matcher.group(1).equalsIgnoreCase("W") ? -1.0 : 1.0; + final double degree = Integer.valueOf(StringUtils.defaultIfEmpty(StringUtils.stripEnd(matcher.group(2), "°"), "0")).doubleValue(); - double minutes = 0.0; - double seconds = 0.0; + double minutes = 0.0; + double seconds = 0.0; - if (null != matcher.group(3)) { - minutes = Integer.valueOf(matcher.group(3)).doubleValue(); + if (null != matcher.group(3)) { + minutes = Integer.valueOf(matcher.group(3)).doubleValue(); - if (null != matcher.group(4)) { - seconds = Double.parseDouble("0." + matcher.group(4)) * 60.0; - } else if (null != matcher.group(5)) { - seconds = Double.parseDouble(matcher.group(5).replace(",", ".")); + if (null != matcher.group(4)) { + seconds = Double.parseDouble("0." + matcher.group(4)) * 60.0; + } else if (null != matcher.group(5)) { + seconds = Double.parseDouble(matcher.group(5).replace(",", ".")); + } } - } - - return new ResultWrapper(sign * (degree + minutes / 60.0 + seconds / 3600.0), matcher.start(), matcher.group().length()); + return new ResultWrapper(sign * (degree + minutes / 60.0 + seconds / 3600.0), matcher.start(), matcher.group().length()); + } + } catch (final NumberFormatException ignored) { + // We might have encountered too large a number. This was not the right way to do it, try another. } // Nothing found with "N 52...", try to match string as decimal degree parts (i.e. multiple doubles) try { - final String[] items = StringUtils.split(text.trim()); + final String[] items = StringUtils.split(StringUtils.trimToEmpty(text)); if (items.length > 0 && items.length <= 2) { final int index = (latlon == LatLon.LON ? items.length - 1 : 0); final String textPart = items[index]; final int pos = (latlon == LatLon.LON ? text.lastIndexOf(textPart) : text.indexOf(textPart)); return new ResultWrapper(Double.parseDouble(textPart), pos, textPart.length()); } - } catch (NumberFormatException e) { + } catch (final NumberFormatException ignored) { // The right exception will be raised below. } diff --git a/main/src/cgeo/geocaching/geopoint/IConversion.java b/main/src/cgeo/geocaching/location/IConversion.java index b1cbffd..f7f0e36 100644 --- a/main/src/cgeo/geocaching/geopoint/IConversion.java +++ b/main/src/cgeo/geocaching/location/IConversion.java @@ -1,4 +1,4 @@ -package cgeo.geocaching.geopoint; +package cgeo.geocaching.location; public interface IConversion { public static final float MILES_TO_KILOMETER = 1.609344f; diff --git a/main/src/cgeo/geocaching/location/MapQuestGeocoder.java b/main/src/cgeo/geocaching/location/MapQuestGeocoder.java new file mode 100644 index 0000000..ae82546 --- /dev/null +++ b/main/src/cgeo/geocaching/location/MapQuestGeocoder.java @@ -0,0 +1,135 @@ +package cgeo.geocaching.location; + +import cgeo.geocaching.network.Network; +import cgeo.geocaching.network.Parameters; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Func0; + +import android.location.Address; + +import java.util.Locale; + +public class MapQuestGeocoder { + + private static final String MAPQUEST_KEY = "Fmjtd|luurn1u2n9,bs=o5-9wynua"; + + private MapQuestGeocoder() { + // Do not instantiate + } + + /** + * Retrieve addresses from a textual location using MapQuest geocoding API. The work happens on the network + * scheduler. + * + * @param address + * the location + * @return an observable containing zero or more locations + * + * @see android.location.Geocoder#getFromLocationName(String, int) + */ + public static Observable<Address> getFromLocationName(@NonNull final String address) { + return get("address", new Parameters("location", address, "maxResults", "20", "thumbMaps", "false")); + } + + /** + * Retrieve the physical address for coordinates. The work happens on the network scheduler. + * + * @param coords the coordinates + * @return an observable containing one location or an error + */ + public static Observable<Address> getFromLocation(@NonNull final Geopoint coords) { + return get("reverse", new Parameters("location", String.format(Locale.US, "%f,%f", coords.getLatitude(), coords.getLongitude()))).first(); + } + + private static Observable<Address> get(@NonNull final String method, @NonNull final Parameters parameters) { + return Observable.defer(new Func0<Observable<Address>>() { + @Override + public Observable<Address> call() { + final ObjectNode response = Network.requestJSON("https://open.mapquestapi.com/geocoding/v1/" + method, + parameters.put("key", MAPQUEST_KEY)); + if (response == null) { + Log.w("MapQuest decoder error: no response"); + return Observable.error(new RuntimeException("no answer from MapQuest geocoder")); + } + final int statusCode = response.path("info").path("statuscode").asInt(-1); + if (statusCode != 0) { + Log.w("MapQuest decoder error: statuscode is not 0"); + return Observable.error(new RuntimeException("no correct answer from MapQuest geocoder")); + } + return Observable.create(new OnSubscribe<Address>() { + @Override + public void call(final Subscriber<? super Address> subscriber) { + try { + for (final JsonNode address: response.get("results").get(0).get("locations")) { + subscriber.onNext(mapquestToAddress(address)); + } + subscriber.onCompleted(); + } catch (final Exception e) { + Log.e("Error decoding MapQuest address", e); + subscriber.onError(e); + } + } + }); + } + }).subscribeOn(RxUtils.networkScheduler); + } + + private static Address mapquestToAddress(final JsonNode mapquestAddress) { + final Address address = new Address(Locale.getDefault()); + for (int i = 1; i <= 6; i++) { + final String adminAreaName = "adminArea" + i; + setComponent(address, mapquestAddress, adminAreaName, mapquestAddress.path(adminAreaName + "Type").asText()); + } + setComponent(address, mapquestAddress, "postalCode", "PostalCode"); + int index = 0; + for (final String addressComponent: new String[]{ mapquestAddress.path("street").asText(), address.getSubLocality(), address.getLocality(), + address.getPostalCode(), address.getSubAdminArea(), address.getAdminArea(), address.getCountryCode() }) { + if (StringUtils.isNotBlank(addressComponent)) { + address.setAddressLine(index++, addressComponent); + } + } + address.setLatitude(mapquestAddress.get("latLng").get("lat").asDouble()); + address.setLongitude(mapquestAddress.get("latLng").get("lng").asDouble()); + return address; + } + + private static void setComponent(final Address address, final JsonNode mapquestAddress, final String adminArea, final String adminAreaType) { + final String content = StringUtils.trimToNull(mapquestAddress.path(adminArea).asText()); + switch (adminAreaType) { + case "City": + address.setLocality(content); + break; + case "Neighborhood": + address.setSubLocality(content); + break; + case "PostalCode": + address.setPostalCode(content); + break; + case "State": + address.setAdminArea(content); + break; + case "County": + address.setSubAdminArea(content); + break; + case "Country": + address.setCountryCode(content); + address.setCountryName(new Locale("", content).getDisplayCountry()); + break; + // Make checkers happy + default: + break; + } + } + +} diff --git a/main/src/cgeo/geocaching/geopoint/Units.java b/main/src/cgeo/geocaching/location/Units.java index 018216d..842bd83 100644 --- a/main/src/cgeo/geocaching/geopoint/Units.java +++ b/main/src/cgeo/geocaching/location/Units.java @@ -1,4 +1,4 @@ -package cgeo.geocaching.geopoint; +package cgeo.geocaching.location; import cgeo.geocaching.settings.Settings; @@ -10,8 +10,8 @@ public class Units { public static ImmutablePair<Double, String> scaleDistance(final double distanceKilometers) { double distance; - String units; - if (Settings.isUseImperialUnits()) { + final String units; + if (Settings.useImperialUnits()) { distance = distanceKilometers / IConversion.MILES_TO_KILOMETER; if (distance >= 0.1) { units = "mi"; @@ -37,24 +37,24 @@ public class Units { } final ImmutablePair<Double, String> scaled = scaleDistance(distanceKilometers); - String formatString; + final String formatString; if (scaled.left >= 100) { - formatString = "%.0f"; + formatString = "%.0f %s"; } else if (scaled.left >= 10) { - formatString = "%.1f"; + formatString = "%.1f %s"; } else { - formatString = "%.2f"; + formatString = "%.2f %s"; } - return String.format(formatString + " %s", scaled.left, scaled.right); + return String.format(formatString, scaled.left, scaled.right); } - public static String getDistanceFromMeters(float meters) { + public static String getDistanceFromMeters(final float meters) { return getDistanceFromKilometers(meters / 1000f); } public static String getSpeed(final float kilometersPerHour) { - if (Settings.isUseImperialUnits()) { + if (Settings.useImperialUnits()) { return String.format(Locale.US, "%.0f mph", kilometersPerHour / IConversion.MILES_TO_KILOMETER); } return String.format(Locale.US, "%.0f km/h", kilometersPerHour); diff --git a/main/src/cgeo/geocaching/geopoint/Viewport.java b/main/src/cgeo/geocaching/location/Viewport.java index ba0e040..b885336 100644 --- a/main/src/cgeo/geocaching/geopoint/Viewport.java +++ b/main/src/cgeo/geocaching/location/Viewport.java @@ -1,4 +1,4 @@ -package cgeo.geocaching.geopoint; +package cgeo.geocaching.location; import cgeo.geocaching.ICoordinates; @@ -79,6 +79,22 @@ public final class Viewport { && coords.getLatitudeE6() <= topRight.getLatitudeE6(); } + /** + * Count the number of points present in the viewport. + * + * @param points a collection of (possibly null) points + * @return the number of non-null points in the viewport + */ + public int count(final @NonNull Collection<? extends ICoordinates> points) { + int total = 0; + for (final ICoordinates point: points) { + if (point != null && contains(point)) { + total += 1; + } + } + return total; + } + @Override public String toString() { return "(" + bottomLeft.toString() + "," + topRight.toString() + ")"; @@ -86,7 +102,7 @@ public final class Viewport { /** * Check whether another viewport is fully included into the current one. - * + * * @param vp * the other viewport * @return true if the viewport is fully included into this one, false otherwise @@ -102,6 +118,7 @@ public final class Viewport { * the database table to use as prefix, or null if no prefix is required * @return the string without the "where" keyword */ + @NonNull public StringBuilder sqlWhere(@Nullable final String dbTable) { final String prefix = dbTable == null ? "" : (dbTable + "."); return new StringBuilder(prefix).append("latitude >= ").append(getLatitudeMin()).append(" and ") diff --git a/main/src/cgeo/geocaching/maps/AbstractItemizedOverlay.java b/main/src/cgeo/geocaching/maps/AbstractItemizedOverlay.java index 747618b..9436b2a 100644 --- a/main/src/cgeo/geocaching/maps/AbstractItemizedOverlay.java +++ b/main/src/cgeo/geocaching/maps/AbstractItemizedOverlay.java @@ -17,9 +17,9 @@ import android.graphics.drawable.Drawable; */ public abstract class AbstractItemizedOverlay implements GeneralOverlay { - private ItemizedOverlayImpl ovlImpl; + private final ItemizedOverlayImpl ovlImpl; - protected AbstractItemizedOverlay(ItemizedOverlayImpl ovlImplIn) { + protected AbstractItemizedOverlay(final ItemizedOverlayImpl ovlImplIn) { ovlImpl = ovlImplIn; } @@ -27,26 +27,26 @@ public abstract class AbstractItemizedOverlay implements GeneralOverlay { ovlImpl.superPopulate(); } - public boolean onTap(int index) { + public boolean onTap(final int index) { return ovlImpl.superOnTap(index); } - Drawable boundCenterBottom(Drawable markerIn) { + Drawable boundCenterBottom(final Drawable markerIn) { return ovlImpl.superBoundCenterBottom(markerIn); } - void setLastFocusedItemIndex(int index) { + void setLastFocusedItemIndex(final int index) { ovlImpl.superSetLastFocusedItemIndex(index); } @Override - public void draw(Canvas canvas, MapViewImpl mapView, boolean shadow) { + public void draw(final Canvas canvas, final MapViewImpl mapView, final boolean shadow) { ovlImpl.superDraw(canvas, mapView, shadow); } @Override - public void drawOverlayBitmap(Canvas canvas, Point drawPosition, - MapProjectionImpl projection, byte drawZoomLevel) { + public void drawOverlayBitmap(final Canvas canvas, final Point drawPosition, + final MapProjectionImpl projection, final byte drawZoomLevel) { ovlImpl.superDrawOverlayBitmap(canvas, drawPosition, projection, drawZoomLevel); } diff --git a/main/src/cgeo/geocaching/maps/AbstractMap.java b/main/src/cgeo/geocaching/maps/AbstractMap.java index 2eceadb..f62fb3a 100644 --- a/main/src/cgeo/geocaching/maps/AbstractMap.java +++ b/main/src/cgeo/geocaching/maps/AbstractMap.java @@ -19,7 +19,7 @@ public abstract class AbstractMap { MapActivityImpl mapActivity; - protected AbstractMap(MapActivityImpl activity) { + protected AbstractMap(final MapActivityImpl activity) { mapActivity = activity; } @@ -31,10 +31,10 @@ public abstract class AbstractMap { return mapActivity.getActivity(); } - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { mapActivity.superOnCreate(savedInstanceState); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { mapActivity.getActivity().requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); } } @@ -55,17 +55,17 @@ public abstract class AbstractMap { mapActivity.superOnDestroy(); } - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { final boolean result = mapActivity.superOnCreateOptionsMenu(menu); mapActivity.getActivity().getMenuInflater().inflate(R.menu.map_activity, menu); return result; } - public boolean onPrepareOptionsMenu(Menu menu) { + public boolean onPrepareOptionsMenu(final Menu menu) { return mapActivity.superOnPrepareOptionsMenu(menu); } - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { return mapActivity.superOnOptionsItemSelected(item); } diff --git a/main/src/cgeo/geocaching/maps/AbstractMapProvider.java b/main/src/cgeo/geocaching/maps/AbstractMapProvider.java index 620b953..bd42223 100644 --- a/main/src/cgeo/geocaching/maps/AbstractMapProvider.java +++ b/main/src/cgeo/geocaching/maps/AbstractMapProvider.java @@ -6,7 +6,7 @@ import cgeo.geocaching.maps.interfaces.MapSource; public abstract class AbstractMapProvider implements MapProvider { @Override - public void registerMapSource(MapSource mapSource) { + public void registerMapSource(final MapSource mapSource) { MapProviderFactory.registerMapSource(mapSource); } } diff --git a/main/src/cgeo/geocaching/maps/CGeoMap.java b/main/src/cgeo/geocaching/maps/CGeoMap.java index 9bbf7af..42e41b1 100644 --- a/main/src/cgeo/geocaching/maps/CGeoMap.java +++ b/main/src/cgeo/geocaching/maps/CGeoMap.java @@ -4,8 +4,10 @@ import butterknife.ButterKnife; import cgeo.geocaching.CacheListActivity; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.CompassActivity; import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; +import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.SearchResult; import cgeo.geocaching.Waypoint; @@ -15,13 +17,12 @@ import cgeo.geocaching.connector.gc.GCLogin; import cgeo.geocaching.connector.gc.MapTokens; import cgeo.geocaching.connector.gc.Tile; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.enumerations.LiveMapStrategy.Strategy; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; import cgeo.geocaching.enumerations.WaypointType; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.maps.interfaces.CachesOverlayItemImpl; import cgeo.geocaching.maps.interfaces.GeoPointImpl; import cgeo.geocaching.maps.interfaces.MapActivityImpl; @@ -31,13 +32,15 @@ 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.network.AndroidBeam; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.dialog.LiveMapInfoDialogBuilder; import cgeo.geocaching.utils.AngleUtils; import cgeo.geocaching.utils.CancellableHandler; +import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.LeastRecentlyUsedSet; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MapUtils; @@ -45,6 +48,7 @@ import cgeo.geocaching.utils.MapUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import rx.Subscription; import rx.functions.Action0; @@ -61,11 +65,13 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.location.Location; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; @@ -123,16 +129,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"; @@ -141,15 +137,14 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // Those are initialized in onCreate() and will never be null afterwards private Resources res; private Activity activity; - private CgeoApplication app; private MapItemFactory mapItemFactory; private String mapTitle; - private LeastRecentlyUsedSet<Geocache> caches; + final private LeastRecentlyUsedSet<Geocache> caches = new LeastRecentlyUsedSet<>(MAX_CACHES + DataStore.getAllCachesCount()); private MapViewImpl mapView; private CachesOverlay overlayCaches; private PositionAndScaleOverlay overlayPositionAndScale; - final private GeoDirHandler geoDirUpdate; + final private GeoDirHandler geoDirUpdate = new UpdateLoc(this); private SearchResult searchIntent = null; private String geocodeIntent = null; private Geopoint coordsIntent = null; @@ -161,7 +156,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { private MapTokens tokens = null; private boolean noMapTokenShowed = false; // map status data - private boolean followMyLocation = false; + private static boolean followMyLocation = true; // threads private Subscription loadTimer; private LoadDetails loadDetailsThread = null; @@ -191,19 +186,20 @@ public class CGeoMap extends AbstractMap implements ViewFactory { private boolean centered = false; // if map is already centered private boolean alreadyCentered = false; // -""- for setting my location private static final Set<String> dirtyCaches = new HashSet<>(); + // flag for honeycomb special popup menu handling + private boolean honeycombMenu = false; /** * if live map is enabled, this is the minimum zoom level, independent of the stored setting */ private static final int MIN_LIVEMAP_ZOOM = 12; // Thread pooling - private static BlockingQueue<Runnable> displayQueue = new ArrayBlockingQueue<>(1); - private static ThreadPoolExecutor displayExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, displayQueue, new ThreadPoolExecutor.DiscardOldestPolicy()); - private static BlockingQueue<Runnable> downloadQueue = new ArrayBlockingQueue<>(1); - private static ThreadPoolExecutor downloadExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, downloadQueue, new ThreadPoolExecutor.DiscardOldestPolicy()); - private static BlockingQueue<Runnable> loadQueue = new ArrayBlockingQueue<>(1); - - private static ThreadPoolExecutor loadExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, loadQueue, new ThreadPoolExecutor.DiscardOldestPolicy()); + private static final BlockingQueue<Runnable> displayQueue = new ArrayBlockingQueue<>(1); + private static final ThreadPoolExecutor displayExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, displayQueue, new ThreadPoolExecutor.DiscardOldestPolicy()); + private static final BlockingQueue<Runnable> downloadQueue = new ArrayBlockingQueue<>(1); + private static final ThreadPoolExecutor downloadExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, downloadQueue, new ThreadPoolExecutor.DiscardOldestPolicy()); + private static final BlockingQueue<Runnable> loadQueue = new ArrayBlockingQueue<>(1); + private static final ThreadPoolExecutor loadExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, loadQueue, new ThreadPoolExecutor.DiscardOldestPolicy()); // handlers /** Updates the titles */ @@ -224,29 +220,9 @@ public class CGeoMap extends AbstractMap implements ViewFactory { switch (what) { case UPDATE_TITLE: - // set title - final StringBuilder title = new StringBuilder(); - - if (map.mapMode == MapMode.LIVE && map.isLiveEnabled) { - title.append(map.res.getString(R.string.map_live)); - } else { - title.append(map.mapTitle); - } - - map.countVisibleCaches(); - if (!map.caches.isEmpty() && !map.mapTitle.contains("[")) { - title.append(" [").append(map.cachesCnt); - if (map.cachesCnt != map.caches.size()) { - title.append('/').append(map.caches.size()); - } - title.append(']'); - } + map.setTitle(); + map.setSubtitle(); - if (Settings.isDebug() && map.lastSearchResult != null && StringUtils.isNotBlank(map.lastSearchResult.getUrl())) { - title.append('[').append(map.lastSearchResult.getUrl()).append(']'); - } - - map.setTitle(title.toString()); break; case INVALIDATE_MAP: map.mapView.repaintRequired(null); @@ -261,7 +237,8 @@ public class CGeoMap extends AbstractMap implements ViewFactory { final private Handler displayHandler = new DisplayHandler(this); - private void setTitle(final String title) { + private void setTitle() { + final String title = calculateTitle(); /* Compatibility for the old Action Bar, only used by the maps activity at the moment */ final TextView titleview = ButterKnife.findById(activity, R.id.actionbar_title); if (titleview != null) { @@ -273,10 +250,84 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } } + private String calculateTitle() { + if (isLiveEnabled) { + return res.getString(R.string.map_live); + } + if (mapMode == MapMode.SINGLE) { + final Geocache cache = getSingleModeCache(); + if (cache != null) { + return cache.getName(); + } + } + return StringUtils.defaultIfEmpty(mapTitle, res.getString(R.string.map_map)); + } + + @Nullable + private Geocache getSingleModeCache() { + // use a copy of the caches list to avoid concurrent modification + for (final Geocache geocache : new ArrayList<>(caches)) { + if (geocache.getGeocode().equals(geocodeIntent)) { + return geocache; + } + } + return null; + } + + private void setSubtitle() { + final String subtitle = calculateSubtitle(); + if (StringUtils.isEmpty(subtitle)) { + return; + } + + /* Compatibility for the old Action Bar, only used by the maps activity at the moment */ + final TextView titleView = ButterKnife.findById(activity, R.id.actionbar_title); + if (titleView != null) { + titleView.setText(titleView.getText().toString() + ' ' + subtitle); + } + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)) { + setSubtitleIceCreamSandwich(subtitle); + } + } + + private String calculateSubtitle() { + // count caches in the sub title + countVisibleCaches(); + final StringBuilder subtitle = new StringBuilder(); + if (!isLiveEnabled && mapMode == MapMode.SINGLE) { + final Geocache cache = getSingleModeCache(); + if (cache != null) { + return Formatter.formatMapSubtitle(cache); + } + } + if (!caches.isEmpty()) { + final int totalCount = caches.size(); + + if (cachesCnt != totalCount && Settings.isDebug()) { + subtitle.append(cachesCnt).append('/').append(res.getQuantityString(R.plurals.cache_counts, totalCount, totalCount)); + } + else { + subtitle.append(res.getQuantityString(R.plurals.cache_counts, cachesCnt, cachesCnt)); + } + } + + if (Settings.isDebug() && lastSearchResult != null && StringUtils.isNotBlank(lastSearchResult.getUrl())) { + subtitle.append(" [").append(lastSearchResult.getUrl()).append(']'); + } + + return subtitle.toString(); + } + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void setTitleIceCreamSandwich(final String title) { activity.getActionBar().setTitle(title); } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setSubtitleIceCreamSandwich(final String subtitle) { + activity.getActionBar().setSubtitle(subtitle); + } + /** Updates the progress. */ private static final class ShowProgressHandler extends Handler { private int counter = 0; @@ -331,7 +382,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { if (msg.what == UPDATE_PROGRESS) { if (waitDialog != null) { final int secondsElapsed = (int) ((System.currentTimeMillis() - detailProgressTime) / 1000); - int secondsRemaining; + final int secondsRemaining; if (detailProgress > 0) { secondsRemaining = (detailTotal - detailProgress) * secondsElapsed / detailProgress; } else { @@ -367,25 +418,10 @@ public class CGeoMap extends AbstractMap implements ViewFactory { public CGeoMap(final MapActivityImpl activity) { super(activity); - geoDirUpdate = new UpdateLoc(this); } protected void countVisibleCaches() { - final List<Geocache> protectedCaches = caches.getAsList(); - - int count = 0; - if (!protectedCaches.isEmpty()) { - final Viewport viewport = mapView.getViewport(); - - for (final Geocache cache : protectedCaches) { - if (cache != null && cache.getCoords() != null) { - if (viewport.contains(cache)) { - count++; - } - } - } - } - cachesCnt = count; + cachesCnt = mapView.getViewport().count(caches.getAsList()); } @Override @@ -404,10 +440,6 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // class init res = this.getResources(); activity = this.getActivity(); - app = (CgeoApplication) activity.getApplication(); - - final int countBubbleCnt = DataStore.getAllCachesCount(); - caches = new LeastRecentlyUsedSet<>(MAX_CACHES + countBubbleCnt); final MapProvider mapProvider = Settings.getMapProvider(); mapItemFactory = mapProvider.getMapItemFactory(); @@ -415,14 +447,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_TITLE); } else { mapMode = MapMode.LIVE; @@ -461,7 +493,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { activity.getActionBar().setDisplayHomeAsUpEnabled(true); } activity.setContentView(mapProvider.getMapLayoutId()); - setTitle(res.getString(R.string.map_map)); + setTitle(); // initialize map mapView = (MapViewImpl) activity.findViewById(mapProvider.getMapViewId()); @@ -475,18 +507,21 @@ public class CGeoMap extends AbstractMap implements ViewFactory { mapView.clearOverlays(); overlayCaches = mapView.createAddMapOverlay(mapView.getContext(), getResources().getDrawable(R.drawable.marker)); - overlayPositionAndScale = mapView.createAddPositionAndScaleOverlay(); + + + overlayPositionAndScale = mapView.createAddPositionAndScaleOverlay(coordsIntent, geocodeIntent); if (trailHistory != null) { overlayPositionAndScale.setHistory(trailHistory); } + mapView.repaintRequired(null); - setZoom(Settings.getMapZoom()); + setZoom(Settings.getMapZoom(mapMode)); mapView.getMapController().setCenter(Settings.getMapCenter()); if (null == mapStateIntent) { - followMyLocation = mapMode == MapMode.LIVE; + followMyLocation = followMyLocation && (mapMode == MapMode.LIVE); } else { followMyLocation = 1 == mapStateIntent[3]; if ((overlayCaches.getCircles() ? 1 : 0) != mapStateIntent[4]) { @@ -504,9 +539,22 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } prepareFilterBar(); - if (!app.isLiveMapHintShownInThisSession() && !Settings.getHideLiveMapHint() && Settings.getLiveMapHintShowCount() <= 3) { + // Check for Honeycomb fake overflow button and attach popup + final View overflowActionBar = ButterKnife.findById(activity, R.id.overflowActionBar); + if (overflowActionBar != null) { + honeycombMenu = true; + overflowActionBar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + showPopupHoneycomb(v); + } + }); + } + + if (!CgeoApplication.getInstance().isLiveMapHintShownInThisSession() && Settings.getLiveMapHintShowCount() <= 3) { LiveMapInfoDialogBuilder.create(activity).show(); } + AndroidBeam.disable(activity); } private void initMyLocationSwitchButton(final CheckBox locSwitch) { @@ -522,7 +570,6 @@ public class CGeoMap extends AbstractMap implements ViewFactory { /** * Set the zoom of the map. The zoom is restricted to a certain minimum in case of live map. * - * @param zoom */ private void setZoom(final int zoom) { mapView.getMapController().setZoom(isLiveEnabled ? Math.max(zoom, MIN_LIVEMAP_ZOOM) : zoom); @@ -546,18 +593,28 @@ public class CGeoMap extends AbstractMap implements ViewFactory { resumeSubscription = Subscriptions.from(geoDirUpdate.start(GeoDirHandler.UPDATE_GEODIR), startTimer()); if (!CollectionUtils.isEmpty(dirtyCaches)) { - for (final String geocode : dirtyCaches) { - final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); - if (cache != null) { - // new collection type needs to remove first - caches.remove(cache); - // re-add to update the freshness - caches.add(cache); + new AsyncTask<Void, Void, Void>() { + @Override + public Void doInBackground(final Void... params) { + for (final String geocode : dirtyCaches) { + final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_WAYPOINTS); + if (cache != null) { + // new collection type needs to remove first + caches.remove(cache); + // re-add to update the freshness + caches.add(cache); + } + } + return null; } - } - dirtyCaches.clear(); - // Update display - displayExecutor.execute(new DisplayRunnable(this)); + + @Override + public void onPostExecute(final Void result) { + dirtyCaches.clear(); + // Update display + displayExecutor.execute(new DisplayRunnable(CGeoMap.this)); + } + }.execute(); } } @@ -581,10 +638,37 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private void showPopupHoneycomb(final View view) { + // Inflate the core menu ourselves + final android.widget.PopupMenu popupMenu = new android.widget.PopupMenu(getActivity(), view); + final MenuInflater inflater = new MenuInflater(getActivity()); + inflater.inflate(R.menu.map_activity, popupMenu.getMenu()); + + // continue processing menu items as usual + onCreateOptionsMenu(popupMenu.getMenu()); + + onPrepareOptionsMenu(popupMenu.getMenu()); + + popupMenu.setOnMenuItemClickListener( + new android.widget.PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(final MenuItem item) { + return onOptionsItemSelected(item); + } + } + ); + // display menu + popupMenu.show(); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public boolean onCreateOptionsMenu(final Menu menu) { // menu inflation happens in Google/Mapsforge specific classes - super.onCreateOptionsMenu(menu); + // skip it for honeycomb - handled specially in @see showPopupHoneycomb + if (!honeycombMenu) { + super.onCreateOptionsMenu(menu); + } MapProviderFactory.addMapviewMenuItems(menu); @@ -592,7 +676,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.ICE_CREAM_SANDWICH) { /* if we have an Actionbar find the my position toggle */ final MenuItem item = menu.findItem(R.id.menu_toggle_mypos); myLocSwitch = new CheckBox(activity); @@ -640,7 +724,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { item = menu.findItem(R.id.menu_theme_mode); // show theme selection item.setVisible(mapView.hasMapThemes()); - menu.findItem(R.id.menu_as_list).setVisible(!isLoading()); + menu.findItem(R.id.menu_as_list).setVisible(!isLoading() && caches.size() > 1); menu.findItem(R.id.submenu_strategy).setVisible(isLiveEnabled); @@ -657,6 +741,8 @@ public class CGeoMap extends AbstractMap implements ViewFactory { default: // DETAILED menu.findItem(R.id.menu_strategy_detailed).setChecked(true); } + menu.findItem(R.id.menu_hint).setVisible(mapMode == MapMode.SINGLE); + menu.findItem(R.id.menu_compass).setVisible(mapMode == MapMode.SINGLE); } catch (final RuntimeException e) { Log.e("CGeoMap.onPrepareOptionsMenu", e); } @@ -685,6 +771,10 @@ public class CGeoMap extends AbstractMap implements ViewFactory { lastSearchResult = null; searchIntent = null; ActivityMixin.invalidateOptionsMenu(activity); + if (mapMode != MapMode.SINGLE) { + mapTitle = StringUtils.EMPTY; + } + updateMapTitle(); return true; case R.id.menu_store_caches: if (!isLoading()) { @@ -714,7 +804,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { public void call(final Integer selectedListId) { storeCaches(geocodes, selectedListId); } - }, true, StoredList.TEMPORARY_LIST_ID); + }, true, StoredList.TEMPORARY_LIST.id); } else { storeCaches(geocodes, StoredList.STANDARD_LIST_ID); } @@ -742,24 +832,30 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } case R.id.menu_strategy_fastest: { item.setChecked(true); - Settings.setLiveMapStrategy(Strategy.FASTEST); + Settings.setLiveMapStrategy(LivemapStrategy.FASTEST); return true; } case R.id.menu_strategy_fast: { item.setChecked(true); - Settings.setLiveMapStrategy(Strategy.FAST); + Settings.setLiveMapStrategy(LivemapStrategy.FAST); return true; } case R.id.menu_strategy_auto: { item.setChecked(true); - Settings.setLiveMapStrategy(Strategy.AUTO); + Settings.setLiveMapStrategy(LivemapStrategy.AUTO); return true; } case R.id.menu_strategy_detailed: { item.setChecked(true); - Settings.setLiveMapStrategy(Strategy.DETAILED); + Settings.setLiveMapStrategy(LivemapStrategy.DETAILED); return true; } + case R.id.menu_hint: + menuShowHint(); + return true; + case R.id.menu_compass: + menuCompass(); + return true; default: final MapSource mapSource = MapProviderFactory.getMapSource(id); if (mapSource != null) { @@ -771,6 +867,20 @@ public class CGeoMap extends AbstractMap implements ViewFactory { return false; } + private void menuCompass() { + final Geocache cache = getSingleModeCache(); + if (cache != null) { + CompassActivity.startActivityCache(this.getActivity(), cache); + } + } + + private void menuShowHint() { + final Geocache cache = getSingleModeCache(); + if (cache != null) { + cache.showHintToast(getActivity()); + } + } + private void selectMapTheme() { final File[] themeFiles = Settings.getMapThemeFiles(); @@ -853,7 +963,12 @@ public class CGeoMap extends AbstractMap implements ViewFactory { if (restartRequired) { mapRestart(); } else if (mapView != null) { // changeMapSource can be called by onCreate() + mapStateIntent = currentMapState(); mapView.setMapSource(); + // re-center the map + centered = false; + centerMap(geocodeIntent, searchIntent, coordsIntent, mapStateIntent); + // re-build menues ActivityMixin.invalidateOptionsMenu(activity); } @@ -864,27 +979,27 @@ public class CGeoMap extends AbstractMap implements ViewFactory { * Restart the current activity with the default map source. */ private void mapRestart() { - // close old mapview - activity.finish(); - // 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_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); } + // close old map + activity.finish(); + // start the new map activity.startActivity(mapIntent); } @@ -909,13 +1024,13 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } private void savePrefs() { - Settings.setMapZoom(mapView.getMapZoomLevel()); + Settings.setMapZoom(mapMode, mapView.getMapZoomLevel()); Settings.setMapCenter(mapView.getMapViewCenter()); } // Set center of map to my location if appropriate. - private void myLocationInMiddle(final IGeoData geo) { - if (followMyLocation && !geo.isPseudoLocation()) { + private void myLocationInMiddle(final GeoData geo) { + if (followMyLocation) { centerMap(geo.getCoords()); } } @@ -931,8 +1046,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(""); - boolean locationValid = false; + Location currentLocation = Sensors.getInstance().currentGeo(); float currentHeading; private long timeLastPositionOverlayCalculation = 0; @@ -946,16 +1060,10 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } @Override - public void updateGeoDir(final IGeoData geo, final float dir) { - if (geo.isPseudoLocation()) { - locationValid = false; - } else { - locationValid = true; - - currentLocation = geo.getLocation(); - currentHeading = DirectionProvider.getDirectionNow(dir); - repaintPositionOverlay(); - } + public void updateGeoDir(final GeoData geo, final float dir) { + currentLocation = geo; + currentHeading = AngleUtils.getDirectionNow(dir); + repaintPositionOverlay(); } /** @@ -969,23 +1077,24 @@ public class CGeoMap extends AbstractMap implements ViewFactory { try { final CGeoMap map = mapRef.get(); if (map != null) { - final boolean needsRepaintForDistance = needsRepaintForDistance(); + final boolean needsRepaintForDistanceOrAccuracy = needsRepaintForDistanceOrAccuracy(); final boolean needsRepaintForHeading = needsRepaintForHeading(); - if (needsRepaintForDistance) { - if (map.followMyLocation) { + if (needsRepaintForDistanceOrAccuracy) { + if (CGeoMap.followMyLocation) { map.centerMap(new Geopoint(currentLocation)); } } - if (needsRepaintForDistance || needsRepaintForHeading) { + if (needsRepaintForDistanceOrAccuracy || needsRepaintForHeading) { + map.overlayPositionAndScale.setCoordinates(currentLocation); map.overlayPositionAndScale.setHeading(currentHeading); map.mapView.repaintRequired(map.overlayPositionAndScale); } } } catch (final RuntimeException e) { - Log.w("Failed to update location."); + Log.w("Failed to update location", e); } } } @@ -998,11 +1107,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { return Math.abs(AngleUtils.difference(currentHeading, map.overlayPositionAndScale.getHeading())) > MIN_HEADING_DELTA; } - boolean needsRepaintForDistance() { - if (!locationValid) { - return false; - } - + boolean needsRepaintForDistanceOrAccuracy() { final CGeoMap map = mapRef.get(); if (map == null) { return false; @@ -1011,6 +1116,9 @@ public class CGeoMap extends AbstractMap implements ViewFactory { float dist = Float.MAX_VALUE; if (lastLocation != null) { + if (lastLocation.getAccuracy() != currentLocation.getAccuracy()) { + return true; + } dist = currentLocation.distanceTo(lastLocation); } @@ -1028,7 +1136,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } /** - * Starts the {@link LoadTimer}. + * Starts the load timer. */ private Subscription startTimer() { @@ -1037,7 +1145,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { displayPoint(coordsIntent); loadTimer = Subscriptions.empty(); } else { - loadTimer = startLoadTimer(); + loadTimer = Schedulers.newThread().createWorker().schedulePeriodically(new LoadTimerAction(this), 0, 250, TimeUnit.MILLISECONDS); } return loadTimer; } @@ -1072,7 +1180,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { // update title on any change if (moved || !viewportNow.equals(previousViewport)) { - map.displayHandler.sendEmptyMessage(UPDATE_TITLE); + map.updateMapTitle(); } previousZoom = zoomNow; @@ -1094,16 +1202,8 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } /** - * loading timer Triggers every 250ms and checks for viewport change and starts a {@link LoadRunnable}. - */ - private Subscription startLoadTimer() { - return Schedulers.newThread().createWorker().schedulePeriodically(new LoadTimerAction(this), 0, 250, TimeUnit.MILLISECONDS); - } - - /** * get if map is loading something * - * @return */ public boolean isLoading() { return !loadTimer.isUnsubscribed() && @@ -1114,7 +1214,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { /** * Worker thread that loads caches and waypoints from the database and then spawns the {@link DownloadRunnable}. - * started by {@link LoadTimer} + * started by the load timer. */ private static class LoadRunnable extends DoRunnable { @@ -1134,7 +1234,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { showProgressHandler.sendEmptyMessage(SHOW_PROGRESS); loadThreadRun = System.currentTimeMillis(); - SearchResult searchResult; + final SearchResult searchResult; if (mapMode == MapMode.LIVE) { searchResult = isLiveEnabled ? new SearchResult() : new SearchResult(DataStore.loadStoredInViewport(mapView.getViewport(), Settings.getCacheType())); } else { @@ -1250,9 +1350,6 @@ public class CGeoMap extends AbstractMap implements ViewFactory { //render displayExecutor.execute(new DisplayRunnable(this)); - } catch (final ThreadDeath e) { - Log.d("DownloadThread stopped"); - displayHandler.sendEmptyMessage(UPDATE_TITLE); } finally { showProgressHandler.sendEmptyMessage(HIDE_PROGRESS); // hide progress } @@ -1302,19 +1399,14 @@ public class CGeoMap extends AbstractMap implements ViewFactory { } itemsToDisplay.add(getCacheItem(cache)); } - - overlayCaches.updateItems(itemsToDisplay); - displayHandler.sendEmptyMessage(INVALIDATE_MAP); - - } else { + } + // don't add other waypoints to overlayCaches if just one point should be displayed + if (coordsIntent == null) { overlayCaches.updateItems(itemsToDisplay); - displayHandler.sendEmptyMessage(INVALIDATE_MAP); } + displayHandler.sendEmptyMessage(INVALIDATE_MAP); - displayHandler.sendEmptyMessage(UPDATE_TITLE); - } catch (final ThreadDeath e) { - Log.d("DisplayThread stopped"); - displayHandler.sendEmptyMessage(UPDATE_TITLE); + updateMapTitle(); } finally { showProgressHandler.sendEmptyMessage(HIDE_PROGRESS); } @@ -1327,11 +1419,15 @@ public class CGeoMap extends AbstractMap implements ViewFactory { final CachesOverlayItemImpl item = getWaypointItem(waypoint); overlayCaches.updateItems(item); displayHandler.sendEmptyMessage(INVALIDATE_MAP); - displayHandler.sendEmptyMessage(UPDATE_TITLE); + updateMapTitle(); cachesCnt = 1; } + private void updateMapTitle() { + displayHandler.sendEmptyMessage(UPDATE_TITLE); + } + private static abstract class DoRunnable implements Runnable { private final WeakReference<CGeoMap> mapRef; @@ -1538,7 +1634,7 @@ public class CGeoMap extends AbstractMap implements ViewFactory { if (myLocSwitch != null) { myLocSwitch.setChecked(followMyLocation); if (followMyLocation) { - myLocationInMiddle(app.currentGeo()); + myLocationInMiddle(Sensors.getInstance().currentGeo()); } } } @@ -1610,42 +1706,40 @@ 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_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_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); fromActivity.startActivity(mapIntent); } diff --git a/main/src/cgeo/geocaching/maps/CachesOverlay.java b/main/src/cgeo/geocaching/maps/CachesOverlay.java index 3c6109e..9649c0d 100644 --- a/main/src/cgeo/geocaching/maps/CachesOverlay.java +++ b/main/src/cgeo/geocaching/maps/CachesOverlay.java @@ -10,7 +10,7 @@ import cgeo.geocaching.activity.Progress; import cgeo.geocaching.connector.gc.GCMap; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.LoadFlags; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.interfaces.CachesOverlayItemImpl; import cgeo.geocaching.maps.interfaces.GeoPointImpl; import cgeo.geocaching.maps.interfaces.ItemizedOverlayImpl; @@ -43,13 +43,13 @@ public class CachesOverlay extends AbstractItemizedOverlay { private List<CachesOverlayItemImpl> items = new ArrayList<>(); private Context context = null; private boolean displayCircles = false; - private Progress progress = new Progress(); + private final Progress progress = new Progress(); private Paint blockedCircle = null; private PaintFlagsDrawFilter setFilter = null; private PaintFlagsDrawFilter removeFilter = null; private MapItemFactory mapItemFactory = null; - public CachesOverlay(ItemizedOverlayImpl ovlImpl, Context contextIn) { + public CachesOverlay(final ItemizedOverlayImpl ovlImpl, final Context contextIn) { super(ovlImpl); populate(); @@ -60,19 +60,19 @@ public class CachesOverlay extends AbstractItemizedOverlay { mapItemFactory = mapProvider.getMapItemFactory(); } - void updateItems(CachesOverlayItemImpl item) { - List<CachesOverlayItemImpl> itemsPre = new ArrayList<>(); + void updateItems(final CachesOverlayItemImpl item) { + final List<CachesOverlayItemImpl> itemsPre = new ArrayList<>(); itemsPre.add(item); updateItems(itemsPre); } - void updateItems(List<CachesOverlayItemImpl> itemsPre) { + void updateItems(final List<CachesOverlayItemImpl> itemsPre) { if (itemsPre == null) { return; } - for (CachesOverlayItemImpl item : itemsPre) { + for (final CachesOverlayItemImpl item : itemsPre) { item.setMarker(boundCenterBottom(item.getMarker(0))); } @@ -97,7 +97,7 @@ public class CachesOverlay extends AbstractItemizedOverlay { } @Override - public void draw(Canvas canvas, MapViewImpl mapView, boolean shadow) { + public void draw(final Canvas canvas, final MapViewImpl mapView, final boolean shadow) { drawInternal(canvas, mapView.getMapProjection()); @@ -105,15 +105,15 @@ public class CachesOverlay extends AbstractItemizedOverlay { } @Override - public void drawOverlayBitmap(Canvas canvas, Point drawPosition, - MapProjectionImpl projection, byte drawZoomLevel) { + public void drawOverlayBitmap(final Canvas canvas, final Point drawPosition, + final MapProjectionImpl projection, final byte drawZoomLevel) { drawInternal(canvas, projection); super.drawOverlayBitmap(canvas, drawPosition, projection, drawZoomLevel); } - private void drawInternal(Canvas canvas, MapProjectionImpl projection) { + private void drawInternal(final Canvas canvas, final MapProjectionImpl projection) { if (!displayCircles || items.isEmpty()) { return; } @@ -129,7 +129,7 @@ public class CachesOverlay extends AbstractItemizedOverlay { final int radius = calculateDrawingRadius(projection); final Point center = new Point(); - for (CachesOverlayItemImpl item : items) { + for (final CachesOverlayItemImpl item : items) { if (item.applyDistanceRule()) { final Geopoint itemCoord = item.getCoord().getCoords(); final GeoPointImpl itemGeo = mapItemFactory.getGeoPointBase(itemCoord); @@ -158,11 +158,9 @@ public class CachesOverlay extends AbstractItemizedOverlay { * reality and therefore the minor changes due to the projection will not make any visible difference at the zoom * levels which are used to see the circles. * - * @param projection - * @return */ - private int calculateDrawingRadius(MapProjectionImpl projection) { - float[] distanceArray = new float[1]; + private int calculateDrawingRadius(final MapProjectionImpl projection) { + final float[] distanceArray = new float[1]; final Geopoint itemCoord = items.get(0).getCoord().getCoords(); Location.distanceBetween(itemCoord.getLatitude(), itemCoord.getLongitude(), @@ -202,7 +200,7 @@ public class CachesOverlay extends AbstractItemizedOverlay { } @Override - public boolean onTap(int index) { + public boolean onTap(final int index) { try { if (items.size() <= index) { @@ -232,7 +230,7 @@ public class CachesOverlay extends AbstractItemizedOverlay { if (StringUtils.equalsIgnoreCase(coordType, "cache") && StringUtils.isNotBlank(coordinate.getGeocode())) { final Geocache cache = DataStore.loadCache(coordinate.getGeocode(), LoadFlags.LOAD_CACHE_OR_DB); if (cache != null) { - RequestDetailsThread requestDetailsThread = new RequestDetailsThread(cache); + final RequestDetailsThread requestDetailsThread = new RequestDetailsThread(cache); if (!requestDetailsThread.requestRequired()) { // don't show popup if we have enough details progress.dismiss(); @@ -253,21 +251,19 @@ public class CachesOverlay extends AbstractItemizedOverlay { } progress.dismiss(); - } catch (NotFoundException e) { + } catch (final NotFoundException e) { Log.e("CachesOverlay.onTap", e); - if (progress != null) { - progress.dismiss(); - } + progress.dismiss(); } return true; } @Override - public CachesOverlayItemImpl createItem(int index) { + public CachesOverlayItemImpl createItem(final int index) { try { return items.get(index); - } catch (Exception e) { + } catch (final Exception e) { Log.e("CachesOverlay.createItem", e); } @@ -278,7 +274,7 @@ public class CachesOverlay extends AbstractItemizedOverlay { public int size() { try { return items.size(); - } catch (Exception e) { + } catch (final Exception e) { Log.e("CachesOverlay.size", e); } diff --git a/main/src/cgeo/geocaching/maps/DirectionDrawer.java b/main/src/cgeo/geocaching/maps/DirectionDrawer.java new file mode 100644 index 0000000..c746221 --- /dev/null +++ b/main/src/cgeo/geocaching/maps/DirectionDrawer.java @@ -0,0 +1,60 @@ +package cgeo.geocaching.maps; + +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.maps.interfaces.MapItemFactory; +import cgeo.geocaching.maps.interfaces.MapProjectionImpl; +import cgeo.geocaching.settings.Settings; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.location.Location; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +public class DirectionDrawer { + private Geopoint currentCoords; + private final Geopoint destinationCoords; + private final MapItemFactory mapItemFactory; + private final float width; + + private Paint line = null; + + public DirectionDrawer(final Geopoint coords) { + this.destinationCoords = coords; + this.mapItemFactory = Settings.getMapProvider().getMapItemFactory(); + + final DisplayMetrics metrics = new DisplayMetrics(); + final WindowManager windowManager = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); + windowManager.getDefaultDisplay().getMetrics(metrics); + + width = 4f * metrics.density; + + } + + public void setCoordinates(final Location coordinatesIn) { + currentCoords = new Geopoint(coordinatesIn); + } + + void drawDirection(final Canvas canvas, final MapProjectionImpl projection) { + if (currentCoords == null) { + return; + } + + if (line == null) { + line = new Paint(); + line.setAntiAlias(true); + line.setStrokeWidth(width); + line.setColor(0x80EB391E); + } + + final Point pos = new Point(); + final Point dest = new Point(); + projection.toPixels(mapItemFactory.getGeoPointBase(currentCoords), pos); + projection.toPixels(mapItemFactory.getGeoPointBase(destinationCoords), dest); + + canvas.drawLine(pos.x, pos.y, dest.x, dest.y, line); + } +} diff --git a/main/src/cgeo/geocaching/maps/DistanceDrawer.java b/main/src/cgeo/geocaching/maps/DistanceDrawer.java new file mode 100644 index 0000000..9dde8e6 --- /dev/null +++ b/main/src/cgeo/geocaching/maps/DistanceDrawer.java @@ -0,0 +1,130 @@ +package cgeo.geocaching.maps; + +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.maps.interfaces.MapViewImpl; + +import android.content.Context; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.location.Location; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +public class DistanceDrawer { + private final Geopoint destinationCoords; + + private Paint paintBox = null; + private Paint paintBoxShadow = null; + private Paint paintText = null; + private BlurMaskFilter blurBoxShadow = null; + + private final boolean needsInvertedColors; + private float pixelDensity = 0; + private final float boxWidth, boxHeight, boxCornerRadius, boxShadowSize, boxPadding; + private final float textHeight, maxTextWidth; + private final float boxX, boxY; + + private String distanceText = null; + + public DistanceDrawer(final MapViewImpl mapView, final Geopoint destinationCoords) { + this.destinationCoords = destinationCoords; + + final DisplayMetrics metrics = new DisplayMetrics(); + final WindowManager windowManager = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); + windowManager.getDefaultDisplay().getMetrics(metrics); + + pixelDensity = metrics.density; + + boxPadding = 2; + boxWidth = 100 * pixelDensity + 3 * boxPadding; + boxHeight = 30 * pixelDensity + 2 * boxPadding; + boxCornerRadius = 5 * pixelDensity; + boxShadowSize = 1 * pixelDensity; + textHeight = 20 * pixelDensity; + + needsInvertedColors = mapView.needsInvertedColors(); + boxX = metrics.widthPixels - boxWidth; + boxY = 0; + + maxTextWidth = boxWidth - 3 * boxPadding; + } + + public void setCoordinates(final Location location) { + final Geopoint currentCoords = new Geopoint(location); + + final float distance = currentCoords.distanceTo(destinationCoords); + distanceText = Units.getDistanceFromKilometers(distance); + } + + void drawDistance(final Canvas canvas) { + if (distanceText == null) { + return; + } + + if (blurBoxShadow == null) { + blurBoxShadow = new BlurMaskFilter(3, BlurMaskFilter.Blur.NORMAL); + + paintBoxShadow = new Paint(); + paintBoxShadow.setAntiAlias(true); + paintBoxShadow.setMaskFilter(blurBoxShadow); + + paintBox = new Paint(); + paintBox.setAntiAlias(true); + + paintText = new Paint(); + paintText.setAntiAlias(true); + paintText.setTextAlign(Paint.Align.LEFT); + paintText.setTypeface(Typeface.DEFAULT_BOLD); + + final int TRANSPARENCY = 0x80000000; + if (needsInvertedColors) { + paintBoxShadow.setColor(0x000000 | TRANSPARENCY); + paintBox.setColor(0xFFFFFF | TRANSPARENCY); + paintText.setColor(0xFF000000); + } else { + paintBoxShadow.setColor(0xFFFFFF | TRANSPARENCY); + paintBox.setColor(0x000000 | TRANSPARENCY); + paintText.setColor(0xFFFFFFFF); + } + } + + /* Calculate text size */ + final Rect textBounds = new Rect(); + paintText.setTextSize(textHeight); + paintText.getTextBounds(distanceText, 0, distanceText.length(), textBounds); + while (textBounds.height() > maxTextWidth) { + paintText.setTextSize(paintText.getTextSize() - 1); + paintText.getTextBounds(distanceText, 0, distanceText.length(), textBounds); + } + + final float textX = (boxWidth - 3 * boxPadding - textBounds.width()) / 2 + boxX + 2 * boxPadding; + final float textY = (boxHeight + textBounds.height()) / 2 + boxY; + + /* Paint background box */ + canvas.drawRoundRect( + new RectF( + boxX - boxShadowSize, boxY - boxShadowSize - boxCornerRadius, + boxX + boxWidth + boxShadowSize + boxCornerRadius, boxY + boxHeight + boxShadowSize + ), + boxCornerRadius, boxCornerRadius, + paintBoxShadow + ); + canvas.drawRoundRect( + new RectF( + boxX, boxY - boxCornerRadius, + boxX + boxWidth + boxCornerRadius, boxY + boxHeight + ), + boxCornerRadius, boxCornerRadius, + paintBox + ); + + /* Paint distance */ + canvas.drawText(distanceText, textX, textY, paintText); + } +} diff --git a/main/src/cgeo/geocaching/maps/LivemapStrategy.java b/main/src/cgeo/geocaching/maps/LivemapStrategy.java new file mode 100644 index 0000000..c135fc8 --- /dev/null +++ b/main/src/cgeo/geocaching/maps/LivemapStrategy.java @@ -0,0 +1,45 @@ +package cgeo.geocaching.maps; + +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.R; + +import java.util.EnumSet; + +/** + * Defines the strategy for the Live Map + */ +public enum LivemapStrategy { + FASTEST(1, EnumSet.of(Flag.LOAD_TILES), R.string.map_strategy_fastest), + FAST(2, EnumSet.of(Flag.LOAD_TILES, Flag.PARSE_TILES), R.string.map_strategy_fast), + AUTO(3, EnumSet.noneOf(Flag.class), R.string.map_strategy_auto), + DETAILED(4, EnumSet.allOf(Flag.class), R.string.map_strategy_detailed); + + public final int id; + public final EnumSet<Flag> flags; + private final int stringId; + + public enum Flag { + LOAD_TILES, // 2x2 tiles filling the complete viewport + PARSE_TILES, // parse PNG images + SEARCH_NEARBY // searchByCoords() + } + + LivemapStrategy(final int id, final EnumSet<Flag> flags, final int stringId) { + this.id = id; + this.flags = flags; + this.stringId = stringId; + } + + public static LivemapStrategy getById(final int id) { + for (final LivemapStrategy strategy : LivemapStrategy.values()) { + if (strategy.id == id) { + return strategy; + } + } + return AUTO; + } + + public final String getL10n() { + return CgeoApplication.getInstance().getBaseContext().getResources().getString(stringId); + } +} 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/maps/MapProviderFactory.java b/main/src/cgeo/geocaching/maps/MapProviderFactory.java index 8685d94..b504020 100644 --- a/main/src/cgeo/geocaching/maps/MapProviderFactory.java +++ b/main/src/cgeo/geocaching/maps/MapProviderFactory.java @@ -33,7 +33,8 @@ public class MapProviderFactory { public static boolean isGoogleMapsInstalled() { // Check if API key is available - if (StringUtils.isBlank(CgeoApplication.getInstance().getString(R.string.maps_api_key))) { + final String mapsKey = CgeoApplication.getInstance().getString(R.string.maps_api_key); + if (StringUtils.length(mapsKey) < 30 || StringUtils.contains(mapsKey, "key")) { Log.w("No Google API key available."); return false; } @@ -41,7 +42,7 @@ public class MapProviderFactory { // Check if API is available try { Class.forName("com.google.android.maps.MapActivity"); - } catch (ClassNotFoundException e) { + } catch (final ClassNotFoundException ignored) { return false; } @@ -59,7 +60,7 @@ public class MapProviderFactory { return provider1 == provider2 && provider1.isSameActivity(source1, source2); } - public static void addMapviewMenuItems(Menu menu) { + public static void addMapviewMenuItems(final Menu menu) { final SubMenu parentMenu = menu.findItem(R.id.menu_select_mapview).getSubMenu(); final int currentSource = Settings.getMapSource().getNumericalId(); @@ -78,8 +79,8 @@ public class MapProviderFactory { * @return the map source, or <tt>null</tt> if <tt>id</tt> does not correspond to a registered map source */ @Nullable - public static MapSource getMapSource(int id) { - for (MapSource mapSource : mapSources) { + public static MapSource getMapSource(final int id) { + for (final MapSource mapSource : mapSources) { if (mapSource.getNumericalId() == id) { return mapSource; } @@ -109,7 +110,7 @@ public class MapProviderFactory { */ public static void deleteOfflineMapSources() { final ArrayList<MapSource> deletion = new ArrayList<>(); - for (MapSource mapSource : mapSources) { + for (final MapSource mapSource : mapSources) { if (mapSource instanceof MapsforgeMapProvider.OfflineMapSource) { deletion.add(mapSource); } diff --git a/main/src/cgeo/geocaching/maps/PositionAndScaleOverlay.java b/main/src/cgeo/geocaching/maps/PositionAndScaleOverlay.java index 63fcd73..9a6e4b9 100644 --- a/main/src/cgeo/geocaching/maps/PositionAndScaleOverlay.java +++ b/main/src/cgeo/geocaching/maps/PositionAndScaleOverlay.java @@ -1,5 +1,8 @@ package cgeo.geocaching.maps; +import cgeo.geocaching.DataStore; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.maps.interfaces.GeneralOverlay; import cgeo.geocaching.maps.interfaces.MapProjectionImpl; import cgeo.geocaching.maps.interfaces.MapViewImpl; @@ -16,22 +19,40 @@ public class PositionAndScaleOverlay implements GeneralOverlay { PositionDrawer positionDrawer = null; ScaleDrawer scaleDrawer = null; + DirectionDrawer directionDrawer = null; + DistanceDrawer distanceDrawer = null; - public PositionAndScaleOverlay(OverlayImpl ovlImpl) { + public PositionAndScaleOverlay(final OverlayImpl ovlImpl, final MapViewImpl mapView, final Geopoint coords, final String geocode) { this.ovlImpl = ovlImpl; positionDrawer = new PositionDrawer(); scaleDrawer = new ScaleDrawer(); + + if (coords != null) { + directionDrawer = new DirectionDrawer(coords); + distanceDrawer = new DistanceDrawer(mapView, coords); + } else if (geocode != null) { + final Viewport bounds = DataStore.getBounds(geocode); + if (bounds != null) { + directionDrawer = new DirectionDrawer(bounds.center); + distanceDrawer = new DistanceDrawer(mapView, bounds.center); + } + } } - public void setCoordinates(Location coordinatesIn) { + public void setCoordinates(final Location coordinatesIn) { positionDrawer.setCoordinates(coordinatesIn); + if (directionDrawer != null) { + directionDrawer.setCoordinates(coordinatesIn); + distanceDrawer.setCoordinates(coordinatesIn); + } + } public Location getCoordinates() { return positionDrawer.getCoordinates(); } - public void setHeading(float bearingNow) { + public void setHeading(final float bearingNow) { positionDrawer.setHeading(bearingNow); } @@ -40,21 +61,27 @@ public class PositionAndScaleOverlay implements GeneralOverlay { } @Override - public void drawOverlayBitmap(Canvas canvas, Point drawPosition, - MapProjectionImpl projection, byte drawZoomLevel) { + public void drawOverlayBitmap(final Canvas canvas, final Point drawPosition, + final MapProjectionImpl projection, final byte drawZoomLevel) { drawInternal(canvas, projection, getOverlayImpl().getMapViewImpl()); } @Override - public void draw(Canvas canvas, MapViewImpl mapView, boolean shadow) { + public void draw(final Canvas canvas, final MapViewImpl mapView, final boolean shadow) { drawInternal(canvas, mapView.getMapProjection(), mapView); } - private void drawInternal(Canvas canvas, MapProjectionImpl projection, MapViewImpl mapView) { + private void drawInternal(final Canvas canvas, final MapProjectionImpl projection, final MapViewImpl mapView) { + if (directionDrawer != null) { + directionDrawer.drawDirection(canvas, projection); + } positionDrawer.drawPosition(canvas, projection); scaleDrawer.drawScale(canvas, mapView); + if (distanceDrawer != null) { + distanceDrawer.drawDistance(canvas); + } } @Override @@ -66,7 +93,7 @@ public class PositionAndScaleOverlay implements GeneralOverlay { return positionDrawer.getHistory(); } - public void setHistory(ArrayList<Location> history) { + public void setHistory(final ArrayList<Location> history) { positionDrawer.setHistory(history); } } diff --git a/main/src/cgeo/geocaching/maps/PositionDrawer.java b/main/src/cgeo/geocaching/maps/PositionDrawer.java index 08244ef..c7d1734 100644 --- a/main/src/cgeo/geocaching/maps/PositionDrawer.java +++ b/main/src/cgeo/geocaching/maps/PositionDrawer.java @@ -2,7 +2,7 @@ package cgeo.geocaching.maps; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.interfaces.GeoPointImpl; import cgeo.geocaching.maps.interfaces.MapItemFactory; import cgeo.geocaching.maps.interfaces.MapProjectionImpl; @@ -28,21 +28,21 @@ public class PositionDrawer { private Paint accuracyCircle = null; private Paint historyLine = null; private Paint historyLineShadow = null; - private Point center = new Point(); - private Point left = new Point(); + private final Point center = new Point(); + private final Point left = new Point(); private Bitmap arrow = null; private int widthArrowHalf = 0; private int heightArrowHalf = 0; private PaintFlagsDrawFilter setfil = null; private PaintFlagsDrawFilter remfil = null; - private PositionHistory positionHistory = new PositionHistory(); - private MapItemFactory mapItemFactory; + private final PositionHistory positionHistory = new PositionHistory(); + private final MapItemFactory mapItemFactory; public PositionDrawer() { this.mapItemFactory = Settings.getMapProvider().getMapItemFactory(); } - void drawPosition(Canvas canvas, MapProjectionImpl projection) { + void drawPosition(final Canvas canvas, final MapProjectionImpl projection) { if (coordinates == null || location == null) { return; } @@ -76,20 +76,20 @@ public class PositionDrawer { canvas.setDrawFilter(setfil); - double latitude = coordinates.getLatitude(); - double longitude = coordinates.getLongitude(); - float accuracy = coordinates.getAccuracy(); + final double latitude = coordinates.getLatitude(); + final double longitude = coordinates.getLongitude(); + final float accuracy = coordinates.getAccuracy(); - float[] result = new float[1]; + final float[] result = new float[1]; Location.distanceBetween(latitude, longitude, latitude, longitude + 1, result); - float longitudeLineDistance = result[0]; + final float longitudeLineDistance = result[0]; final Geopoint leftCoords = new Geopoint(latitude, longitude - accuracy / longitudeLineDistance); - GeoPointImpl leftGeo = mapItemFactory.getGeoPointBase(leftCoords); + final GeoPointImpl leftGeo = mapItemFactory.getGeoPointBase(leftCoords); projection.toPixels(leftGeo, left); projection.toPixels(location, center); - int radius = center.x - left.x; + final int radius = center.x - left.x; accuracyCircle.setColor(0x66000000); accuracyCircle.setStyle(Style.STROKE); @@ -106,23 +106,23 @@ public class PositionDrawer { final ArrayList<Location> paintHistory = new ArrayList<>(positionHistory.getHistory()); paintHistory.add(coordinates); - int size = paintHistory.size(); + final int size = paintHistory.size(); if (size > 1) { int alphaCnt = size - 201; if (alphaCnt < 1) { alphaCnt = 1; } - Point pointNow = new Point(); - Point pointPrevious = new Point(); - Location prev = paintHistory.get(0); + final Point pointNow = new Point(); + final Point pointPrevious = new Point(); + final Location prev = paintHistory.get(0); projection.toPixels(mapItemFactory.getGeoPointBase(new Geopoint(prev)), pointPrevious); for (int cnt = 1; cnt < size; cnt++) { - Location now = paintHistory.get(cnt); + final Location now = paintHistory.get(cnt); projection.toPixels(mapItemFactory.getGeoPointBase(new Geopoint(now)), pointNow); - int alpha; + final int alpha; if ((alphaCnt - cnt) > 0) { alpha = 255 / (alphaCnt - cnt); } @@ -147,10 +147,10 @@ public class PositionDrawer { heightArrowHalf = arrow.getHeight() / 2; } - int marginLeft = center.x - widthArrowHalf; - int marginTop = center.y - heightArrowHalf; + final int marginLeft = center.x - widthArrowHalf; + final int marginTop = center.y - heightArrowHalf; - Matrix matrix = new Matrix(); + final Matrix matrix = new Matrix(); matrix.setRotate(heading, widthArrowHalf, heightArrowHalf); matrix.postTranslate(marginLeft, marginTop); @@ -163,11 +163,11 @@ public class PositionDrawer { return positionHistory.getHistory(); } - public void setHistory(ArrayList<Location> history) { + public void setHistory(final ArrayList<Location> history) { positionHistory.setHistory(history); } - public void setHeading(float bearingNow) { + public void setHeading(final float bearingNow) { heading = bearingNow; } @@ -175,7 +175,7 @@ public class PositionDrawer { return heading; } - public void setCoordinates(Location coordinatesIn) { + public void setCoordinates(final Location coordinatesIn) { coordinates = coordinatesIn; location = mapItemFactory.getGeoPointBase(new Geopoint(coordinates)); } diff --git a/main/src/cgeo/geocaching/maps/PositionHistory.java b/main/src/cgeo/geocaching/maps/PositionHistory.java index af13740..4394eba 100644 --- a/main/src/cgeo/geocaching/maps/PositionHistory.java +++ b/main/src/cgeo/geocaching/maps/PositionHistory.java @@ -24,7 +24,7 @@ public class PositionHistory { /** * Adds the current position to the trail history to be able to show the trail on the map. */ - void rememberTrailPosition(Location coordinates) { + void rememberTrailPosition(final Location coordinates) { if (coordinates.getAccuracy() >= 50f) { return; } @@ -36,7 +36,7 @@ public class PositionHistory { return; } - Location historyRecent = history.get(history.size() - 1); + final Location historyRecent = history.get(history.size() - 1); if (historyRecent.distanceTo(coordinates) <= MINIMUM_DISTANCE_METERS) { return; } @@ -56,7 +56,7 @@ public class PositionHistory { return history; } - public void setHistory(ArrayList<Location> history) { + public void setHistory(final ArrayList<Location> history) { this.history = history; } diff --git a/main/src/cgeo/geocaching/maps/ScaleDrawer.java b/main/src/cgeo/geocaching/maps/ScaleDrawer.java index 95c987d..e905873 100644 --- a/main/src/cgeo/geocaching/maps/ScaleDrawer.java +++ b/main/src/cgeo/geocaching/maps/ScaleDrawer.java @@ -1,8 +1,8 @@ package cgeo.geocaching.maps; import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; import cgeo.geocaching.maps.interfaces.GeoPointImpl; import cgeo.geocaching.maps.interfaces.MapViewImpl; @@ -25,8 +25,8 @@ public class ScaleDrawer { private float pixelDensity = 0; public ScaleDrawer() { - DisplayMetrics metrics = new DisplayMetrics(); - WindowManager windowManager = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); + final DisplayMetrics metrics = new DisplayMetrics(); + final WindowManager windowManager = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); windowManager.getDefaultDisplay().getMetrics(metrics); pixelDensity = metrics.density; } @@ -36,7 +36,7 @@ public class ScaleDrawer { return scale * Math.floor(distance / scale); } - void drawScale(Canvas canvas, MapViewImpl mapView) { + void drawScale(final Canvas canvas, final MapViewImpl mapView) { final double span = mapView.getLongitudeSpan() / 1e6; final GeoPointImpl center = mapView.getMapViewCenter(); diff --git a/main/src/cgeo/geocaching/maps/interfaces/MapActivityImpl.java b/main/src/cgeo/geocaching/maps/interfaces/MapActivityImpl.java index 3596d5f..1e69b44 100644 --- a/main/src/cgeo/geocaching/maps/interfaces/MapActivityImpl.java +++ b/main/src/cgeo/geocaching/maps/interfaces/MapActivityImpl.java @@ -33,5 +33,8 @@ public interface MapActivityImpl { boolean superOnOptionsItemSelected(MenuItem item); + /** + * called from the pseudo actionbar layout + */ public abstract void navigateUp(View view); } diff --git a/main/src/cgeo/geocaching/maps/interfaces/MapItemFactory.java b/main/src/cgeo/geocaching/maps/interfaces/MapItemFactory.java index 22c6698..f69de03 100644 --- a/main/src/cgeo/geocaching/maps/interfaces/MapItemFactory.java +++ b/main/src/cgeo/geocaching/maps/interfaces/MapItemFactory.java @@ -1,7 +1,7 @@ package cgeo.geocaching.maps.interfaces; import cgeo.geocaching.IWaypoint; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; public interface MapItemFactory { diff --git a/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java b/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java index 4a6d733..1876dfc 100644 --- a/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java +++ b/main/src/cgeo/geocaching/maps/interfaces/MapViewImpl.java @@ -1,6 +1,7 @@ package cgeo.geocaching.maps.interfaces; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.maps.CachesOverlay; import cgeo.geocaching.maps.PositionAndScaleOverlay; @@ -46,7 +47,7 @@ public interface MapViewImpl { CachesOverlay createAddMapOverlay(Context context, Drawable drawable); - PositionAndScaleOverlay createAddPositionAndScaleOverlay(); + PositionAndScaleOverlay createAddPositionAndScaleOverlay(final Geopoint coords, final String geocode); void setMapSource(); diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java index b9e40d7..02e4243 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlay.java @@ -18,10 +18,10 @@ import java.util.concurrent.locks.ReentrantLock; public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlayItem> implements ItemizedOverlayImpl { - private CachesOverlay base; - private Lock lock = new ReentrantLock(); + private final CachesOverlay base; + private final Lock lock = new ReentrantLock(); - public MapsforgeCacheOverlay(Context contextIn, Drawable markerIn) { + public MapsforgeCacheOverlay(final Context contextIn, final Drawable markerIn) { super(boundCenterBottom(markerIn)); base = new CachesOverlay(this, contextIn); } @@ -32,7 +32,7 @@ public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlay } @Override - protected MapsforgeCacheOverlayItem createItem(int i) { + protected MapsforgeCacheOverlayItem createItem(final int i) { if (base == null) { return null; } @@ -50,7 +50,7 @@ public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlay } @Override - protected boolean onTap(int arg0) { + protected boolean onTap(final int arg0) { if (base == null) { return false; } @@ -59,8 +59,8 @@ public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlay } @Override - protected void drawOverlayBitmap(Canvas canvas, Point drawPosition, - Projection projection, byte drawZoomLevel) { + protected void drawOverlayBitmap(final Canvas canvas, final Point drawPosition, + final Projection projection, final byte drawZoomLevel) { base.drawOverlayBitmap(canvas, drawPosition, new MapsforgeMapProjection(projection), drawZoomLevel); } @@ -70,28 +70,28 @@ public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlay } @Override - public Drawable superBoundCenterBottom(Drawable marker) { + public Drawable superBoundCenterBottom(final Drawable marker) { return ItemizedOverlay.boundCenterBottom(marker); } @Override - public void superSetLastFocusedItemIndex(int i) { + public void superSetLastFocusedItemIndex(final int i) { // nothing to do } @Override - public boolean superOnTap(int index) { + public boolean superOnTap(final int index) { return super.onTap(index); } @Override - public void superDraw(Canvas canvas, MapViewImpl mapView, boolean shadow) { + public void superDraw(final Canvas canvas, final MapViewImpl mapView, final boolean shadow) { // nothing to do here... } @Override - public void superDrawOverlayBitmap(Canvas canvas, Point drawPosition, - MapProjectionImpl projection, byte drawZoomLevel) { + public void superDrawOverlayBitmap(final Canvas canvas, final Point drawPosition, + final MapProjectionImpl projection, final byte drawZoomLevel) { super.drawOverlayBitmap(canvas, drawPosition, (Projection) projection.getImpl(), drawZoomLevel); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlayItem.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlayItem.java index 27ca664..4a1b080 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlayItem.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeCacheOverlayItem.java @@ -12,7 +12,7 @@ public class MapsforgeCacheOverlayItem extends OverlayItem implements CachesOver final private IWaypoint coord; final private boolean applyDistanceRule; - public MapsforgeCacheOverlayItem(IWaypoint coordinate, boolean applyDistanceRule) { + public MapsforgeCacheOverlayItem(final IWaypoint coordinate, final boolean applyDistanceRule) { super(new GeoPoint(coordinate.getCoords().getLatitudeE6(), coordinate.getCoords().getLongitudeE6()), coordinate.getName(), ""); this.coord = coordinate; @@ -25,7 +25,7 @@ public class MapsforgeCacheOverlayItem extends OverlayItem implements CachesOver } @Override - public Drawable getMarker(int index) { + public Drawable getMarker(final int index) { return getMarker(); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeGeoPoint.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeGeoPoint.java index 197bd76..25269e6 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeGeoPoint.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeGeoPoint.java @@ -1,6 +1,6 @@ package cgeo.geocaching.maps.mapsforge; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.interfaces.GeoPointImpl; import org.mapsforge.core.GeoPoint; @@ -9,7 +9,7 @@ public class MapsforgeGeoPoint extends GeoPoint implements GeoPointImpl { private static final long serialVersionUID = 1L; - public MapsforgeGeoPoint(int latitudeE6, int longitudeE6) { + public MapsforgeGeoPoint(final int latitudeE6, final int longitudeE6) { super(latitudeE6, longitudeE6); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapActivity.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapActivity.java index 94213ba..5220e05 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapActivity.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapActivity.java @@ -16,7 +16,7 @@ import android.view.View; public class MapsforgeMapActivity extends MapActivity implements MapActivityImpl, FilteredActivity { - private AbstractMap mapBase; + private final AbstractMap mapBase; public MapsforgeMapActivity() { mapBase = new CGeoMap(this); @@ -28,7 +28,7 @@ public class MapsforgeMapActivity extends MapActivity implements MapActivityImpl } @Override - protected void onCreate(Bundle icicle) { + protected void onCreate(final Bundle icicle) { mapBase.onCreate(icicle); } @@ -53,17 +53,17 @@ public class MapsforgeMapActivity extends MapActivity implements MapActivityImpl } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { return mapBase.onCreateOptionsMenu(menu); } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { return mapBase.onOptionsItemSelected(item); } @Override - public boolean onPrepareOptionsMenu(Menu menu) { + public boolean onPrepareOptionsMenu(final Menu menu) { return mapBase.onPrepareOptionsMenu(menu); } @@ -73,12 +73,12 @@ public class MapsforgeMapActivity extends MapActivity implements MapActivityImpl } @Override - public void superOnCreate(Bundle savedInstanceState) { + public void superOnCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override - public boolean superOnCreateOptionsMenu(Menu menu) { + public boolean superOnCreateOptionsMenu(final Menu menu) { return super.onCreateOptionsMenu(menu); } @@ -88,7 +88,7 @@ public class MapsforgeMapActivity extends MapActivity implements MapActivityImpl } @Override - public boolean superOnOptionsItemSelected(MenuItem item) { + public boolean superOnOptionsItemSelected(final MenuItem item) { return super.onOptionsItemSelected(item); } @@ -108,17 +108,17 @@ public class MapsforgeMapActivity extends MapActivity implements MapActivityImpl } @Override - public boolean superOnPrepareOptionsMenu(Menu menu) { + public boolean superOnPrepareOptionsMenu(final Menu menu) { return super.onPrepareOptionsMenu(menu); } @Override - public void navigateUp(View view) { + public void navigateUp(final View view) { ActivityMixin.navigateUp(this); } @Override - public void showFilterMenu(View view) { + public void showFilterMenu(final View view) { // do nothing, the filter bar only shows the global filter } } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapController.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapController.java index 8b2e1e9..cfce07d 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapController.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapController.java @@ -8,26 +8,26 @@ import org.mapsforge.core.GeoPoint; public class MapsforgeMapController implements MapControllerImpl { - private MapController mapController; - private int maxZoomLevel; + private final MapController mapController; + private final int maxZoomLevel; - public MapsforgeMapController(MapController mapControllerIn, int maxZoomLevelIn) { + public MapsforgeMapController(final MapController mapControllerIn, final int maxZoomLevelIn) { mapController = mapControllerIn; maxZoomLevel = maxZoomLevelIn; } @Override - public void animateTo(GeoPointImpl geoPoint) { + public void animateTo(final GeoPointImpl geoPoint) { mapController.setCenter(castToGeoPoint(geoPoint)); } - private static GeoPoint castToGeoPoint(GeoPointImpl geoPoint) { + private static GeoPoint castToGeoPoint(final GeoPointImpl geoPoint) { assert geoPoint instanceof GeoPoint; return (GeoPoint) geoPoint; } @Override - public void setCenter(GeoPointImpl geoPoint) { + public void setCenter(final GeoPointImpl geoPoint) { mapController.setCenter(castToGeoPoint(geoPoint)); } @@ -36,19 +36,19 @@ public class MapsforgeMapController implements MapControllerImpl { * mapzoom-1 is used to be compatible with Google Maps zoom levels */ @Override - public void setZoom(int mapzoom) { + public void setZoom(final int mapzoom) { // Google Maps and OSM Maps use different zoom levels for the same view. // All OSM Maps zoom levels are offset by 1 so they match Google Maps. mapController.setZoom(Math.min(mapzoom - 1, maxZoomLevel)); } @Override - public void zoomToSpan(int latSpanE6, int lonSpanE6) { + public void zoomToSpan(final int latSpanE6, final int lonSpanE6) { if (latSpanE6 != 0 && lonSpanE6 != 0) { // calculate zoomlevel - int distDegree = Math.max(latSpanE6, lonSpanE6); - int zoomLevel = (int) Math.floor(Math.log(360.0 * 1e6 / distDegree) / Math.log(2)); + final int distDegree = Math.max(latSpanE6, lonSpanE6); + final int zoomLevel = (int) Math.floor(Math.log(360.0 * 1e6 / distDegree) / Math.log(2)); mapController.setZoom(zoomLevel + 1); } } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapItemFactory.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapItemFactory.java index 4ade09c..8adf4ad 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapItemFactory.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapItemFactory.java @@ -1,7 +1,7 @@ package cgeo.geocaching.maps.mapsforge; import cgeo.geocaching.IWaypoint; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.interfaces.CachesOverlayItemImpl; import cgeo.geocaching.maps.interfaces.GeoPointImpl; import cgeo.geocaching.maps.interfaces.MapItemFactory; @@ -14,7 +14,7 @@ public class MapsforgeMapItemFactory implements MapItemFactory { } @Override - public CachesOverlayItemImpl getCachesOverlayItem(final IWaypoint coordinate, boolean applyDistanceRule) { + public CachesOverlayItemImpl getCachesOverlayItem(final IWaypoint coordinate, final boolean applyDistanceRule) { return new MapsforgeCacheOverlayItem(coordinate, applyDistanceRule); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProjection.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProjection.java index 68d7123..a042eca 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProjection.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProjection.java @@ -10,14 +10,14 @@ import android.graphics.Point; public class MapsforgeMapProjection implements MapProjectionImpl { - private Projection projection; + private final Projection projection; - public MapsforgeMapProjection(Projection projectionIn) { + public MapsforgeMapProjection(final Projection projectionIn) { projection = projectionIn; } @Override - public void toPixels(GeoPointImpl leftGeo, Point left) { + public void toPixels(final GeoPointImpl leftGeo, final Point left) { projection.toPixels((GeoPoint) leftGeo, left); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProvider.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProvider.java index 01b10ec..76d645c 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProvider.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapProvider.java @@ -7,8 +7,6 @@ import cgeo.geocaching.maps.MapProviderFactory; import cgeo.geocaching.maps.interfaces.MapItemFactory; import cgeo.geocaching.maps.interfaces.MapProvider; import cgeo.geocaching.maps.interfaces.MapSource; -import cgeo.geocaching.maps.mapsforge.v024.MapsforgeMapActivity024; -import cgeo.geocaching.maps.mapsforge.v024.MapsforgeMapItemFactory024; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; @@ -30,7 +28,6 @@ public final class MapsforgeMapProvider extends AbstractMapProvider { public static final String MAPSFORGE_CYCLEMAP_ID = "MAPSFORGE_CYCLEMAP"; public static final String MAPSFORGE_MAPNIK_ID = "MAPSFORGE_MAPNIK"; - private boolean oldMap = false; private MapItemFactory mapItemFactory = new MapsforgeMapItemFactory(); private MapsforgeMapProvider() { @@ -56,13 +53,13 @@ public final class MapsforgeMapProvider extends AbstractMapProvider { return Collections.emptyList(); } - File directory = new File(directoryPath); + final File directory = new File(directoryPath); if (directory.isDirectory()) { try { - ArrayList<String> mapFileList = new ArrayList<>(); + final ArrayList<String> mapFileList = new ArrayList<>(); final File[] files = directory.listFiles(); if (ArrayUtils.isNotEmpty(files)) { - for (File file : files) { + for (final File file : files) { if (file.getName().endsWith(".map")) { if (MapsforgeMapProvider.isValidMapFile(file.getAbsolutePath())) { mapFileList.add(file.getAbsolutePath()); @@ -72,69 +69,44 @@ public final class MapsforgeMapProvider extends AbstractMapProvider { Collections.sort(mapFileList, String.CASE_INSENSITIVE_ORDER); } return mapFileList; - } catch (Exception e) { + } catch (final Exception e) { Log.e("MapsforgeMapProvider.getOfflineMaps: ", e); } } return Collections.emptyList(); } - public static boolean isValidMapFile(String mapFileIn) { + public static boolean isValidMapFile(final String mapFileIn) { if (StringUtils.isEmpty(mapFileIn)) { return false; } - MapDatabase mapDB = new MapDatabase(); - FileOpenResult result = mapDB.openFile(new File(mapFileIn)); + final MapDatabase mapDB = new MapDatabase(); + final FileOpenResult result = mapDB.openFile(new File(mapFileIn)); mapDB.closeFile(); - boolean isValid = result.isSuccess(); - - if (!isValid) { - isValid = isMapfile024(mapFileIn); - } - - return isValid; - } - - private static boolean isMapfile024(String mapFileIn) { - return mapFileIn != null && org.mapsforge.android.mapsold.MapDatabase.isValidMapFile(mapFileIn); + return result.isSuccess(); } @Override public boolean isSameActivity(final MapSource source1, final MapSource source2) { - return source1 == source2 || - !isMapfile024(Settings.getMapFile()) || - (!(source1 instanceof OfflineMapSource) && !(source2 instanceof OfflineMapSource)); + return source1.getNumericalId() == source2.getNumericalId() || (!(source1 instanceof OfflineMapSource) && !(source2 instanceof OfflineMapSource)); } @Override public Class<? extends Activity> getMapClass() { - final MapSource source = Settings.getMapSource(); - if (source instanceof OfflineMapSource && isMapfile024(Settings.getMapFile())) { - oldMap = true; - mapItemFactory = new MapsforgeMapItemFactory024(); - return MapsforgeMapActivity024.class; - } - oldMap = false; mapItemFactory = new MapsforgeMapItemFactory(); return MapsforgeMapActivity.class; } @Override public int getMapViewId() { - if (oldMap) { - return R.id.mfmap_old; - } return R.id.mfmap; } @Override public int getMapLayoutId() { - if (oldMap) { - return R.layout.map_mapsforge_old; - } return R.layout.map_mapsforge; } @@ -152,7 +124,7 @@ public final class MapsforgeMapProvider extends AbstractMapProvider { private final String fileName; - public OfflineMapSource(final String fileName, MapProvider mapProvider, final String name, final MapGeneratorInternal generator) { + public OfflineMapSource(final String fileName, final MapProvider mapProvider, final String name, final MapGeneratorInternal generator) { super(fileName, mapProvider, name, generator); this.fileName = fileName; } @@ -171,7 +143,7 @@ public final class MapsforgeMapProvider extends AbstractMapProvider { MapProviderFactory.deleteOfflineMapSources(); final Resources resources = CgeoApplication.getInstance().getResources(); final List<String> offlineMaps = getOfflineMaps(); - for (String mapFile : offlineMaps) { + for (final String mapFile : offlineMaps) { final String mapName = StringUtils.capitalize(StringUtils.substringBeforeLast(new File(mapFile).getName(), ".")); registerMapSource(new OfflineMapSource(mapFile, this, mapName + " (" + resources.getString(R.string.map_source_osm_offline) + ")", MapGeneratorInternal.DATABASE_RENDERER)); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapSource.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapSource.java index 861e567..23e9a23 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapSource.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapSource.java @@ -9,7 +9,7 @@ class MapsforgeMapSource extends AbstractMapSource { private final MapGeneratorInternal generator; - public MapsforgeMapSource(final String id, MapProvider mapProvider, String name, MapGeneratorInternal generator) { + public MapsforgeMapSource(final String id, final MapProvider mapProvider, final String name, final MapGeneratorInternal generator) { super(id, mapProvider, name); this.generator = generator; } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java index d95cc80..71bf583 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeMapView.java @@ -1,7 +1,8 @@ package cgeo.geocaching.maps.mapsforge; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Viewport; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Viewport; import cgeo.geocaching.maps.CachesOverlay; import cgeo.geocaching.maps.PositionAndScaleOverlay; import cgeo.geocaching.maps.interfaces.GeneralOverlay; @@ -40,12 +41,12 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { private OnMapDragListener onDragListener; private final MapsforgeMapController mapController = new MapsforgeMapController(getController(), getMapGenerator().getZoomLevelMax()); - public MapsforgeMapView(Context context, AttributeSet attrs) { + public MapsforgeMapView(final Context context, final AttributeSet attrs) { super(context, attrs); initialize(context); } - private void initialize(Context context) { + private void initialize(final Context context) { if (isInEditMode()) { return; } @@ -56,7 +57,7 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { } @Override - public void draw(@NonNull Canvas canvas) { + public void draw(@NonNull final Canvas canvas) { try { // Google Maps and OSM Maps use different zoom levels for the same view. // Here we don't want the Google Maps compatible zoom level, but the actual one. @@ -65,13 +66,13 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { } super.draw(canvas); - } catch (Exception e) { + } catch (final Exception e) { Log.e("MapsforgeMapView.draw", e); } } @Override - public void displayZoomControls(boolean takeFocus) { + public void displayZoomControls(final boolean takeFocus) { // nothing to do here } @@ -83,7 +84,7 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { @Override @NonNull public GeoPointImpl getMapViewCenter() { - GeoPoint point = getMapPosition().getMapCenter(); + final GeoPoint point = getMapPosition().getMapCenter(); return new MapsforgeGeoPoint(point.latitudeE6, point.longitudeE6); } @@ -103,16 +104,16 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { } @Override - public CachesOverlay createAddMapOverlay(Context context, Drawable drawable) { + public CachesOverlay createAddMapOverlay(final Context context, final Drawable drawable) { - MapsforgeCacheOverlay ovl = new MapsforgeCacheOverlay(context, drawable); + final MapsforgeCacheOverlay ovl = new MapsforgeCacheOverlay(context, drawable); getOverlays().add(ovl); return ovl.getBase(); } @Override - public PositionAndScaleOverlay createAddPositionAndScaleOverlay() { - MapsforgeOverlay ovl = new MapsforgeOverlay(); + public PositionAndScaleOverlay createAddPositionAndScaleOverlay(final Geopoint coords, final String geocode) { + final MapsforgeOverlay ovl = new MapsforgeOverlay(this, coords, geocode); getOverlays().add(ovl); return (PositionAndScaleOverlay) ovl.getBase(); } @@ -122,12 +123,12 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { int span = 0; - Projection projection = getProjection(); + final Projection projection = getProjection(); if (projection != null && getHeight() > 0) { - GeoPoint low = projection.fromPixels(0, 0); - GeoPoint high = projection.fromPixels(0, getHeight()); + final GeoPoint low = projection.fromPixels(0, 0); + final GeoPoint high = projection.fromPixels(0, getHeight()); if (low != null && high != null) { span = Math.abs(high.latitudeE6 - low.latitudeE6); @@ -142,11 +143,11 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { int span = 0; - Projection projection = getProjection(); + final Projection projection = getProjection(); if (projection != null && getWidth() > 0) { - GeoPoint low = projection.fromPixels(0, 0); - GeoPoint high = projection.fromPixels(getWidth(), 0); + final GeoPoint low = projection.fromPixels(0, 0); + final GeoPoint high = projection.fromPixels(getWidth(), 0); if (low != null && high != null) { span = Math.abs(high.longitudeE6 - low.longitudeE6); @@ -191,7 +192,7 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { newMapType = ((MapsforgeMapSource) mapSource).getGenerator(); } - MapGenerator mapGenerator = MapGeneratorFactory.createMapGenerator(newMapType); + final MapGenerator mapGenerator = MapGeneratorFactory.createMapGenerator(newMapType); // When swapping map sources, make sure we aren't exceeding max zoom. See bug #1535 final int maxZoom = mapGenerator.getZoomLevelMax(); @@ -230,11 +231,11 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { @Override public void setMapTheme() { - String customRenderTheme = Settings.getCustomRenderThemeFilePath(); + final String customRenderTheme = Settings.getCustomRenderThemeFilePath(); if (StringUtils.isNotEmpty(customRenderTheme)) { try { setRenderTheme(new File(customRenderTheme)); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException ignored) { Toast.makeText( getContext(), getContext().getResources().getString(R.string.warn_rendertheme_missing), @@ -247,38 +248,38 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { } @Override - public void repaintRequired(GeneralOverlay overlay) { + public void repaintRequired(final GeneralOverlay overlay) { if (null == overlay) { invalidate(); } else { try { - Overlay ovl = (Overlay) overlay.getOverlayImpl(); + final Overlay ovl = (Overlay) overlay.getOverlayImpl(); if (ovl != null) { ovl.requestRedraw(); } - } catch (Exception e) { + } catch (final Exception e) { Log.e("MapsforgeMapView.repaintRequired", e); } } } @Override - public void setOnDragListener(OnMapDragListener onDragListener) { + public void setOnDragListener(final OnMapDragListener onDragListener) { this.onDragListener = onDragListener; } @Override - public boolean onTouchEvent(MotionEvent ev) { + public boolean onTouchEvent(final MotionEvent ev) { gestureDetector.onTouchEvent(ev); return super.onTouchEvent(ev); } private class GestureListener extends SimpleOnGestureListener { @Override - public boolean onDoubleTap(MotionEvent e) { + public boolean onDoubleTap(final MotionEvent e) { if (onDragListener != null) { onDragListener.onDrag(); } @@ -286,8 +287,8 @@ public class MapsforgeMapView extends MapView implements MapViewImpl { } @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, - float distanceX, float distanceY) { + public boolean onScroll(final MotionEvent e1, final MotionEvent e2, + final float distanceX, final float distanceY) { if (onDragListener != null) { onDragListener.onDrag(); } diff --git a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeOverlay.java b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeOverlay.java index 3df4ab0..3926eb6 100644 --- a/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeOverlay.java +++ b/main/src/cgeo/geocaching/maps/mapsforge/MapsforgeOverlay.java @@ -1,5 +1,6 @@ package cgeo.geocaching.maps.mapsforge; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.maps.PositionAndScaleOverlay; import cgeo.geocaching.maps.interfaces.GeneralOverlay; import cgeo.geocaching.maps.interfaces.MapViewImpl; @@ -17,15 +18,15 @@ import java.util.concurrent.locks.ReentrantLock; public class MapsforgeOverlay extends Overlay implements OverlayImpl { private PositionAndScaleOverlay overlayBase = null; - private Lock lock = new ReentrantLock(); + private final Lock lock = new ReentrantLock(); - public MapsforgeOverlay() { - overlayBase = new PositionAndScaleOverlay(this); + public MapsforgeOverlay(final MapViewImpl mapView, final Geopoint coords, final String geocode) { + overlayBase = new PositionAndScaleOverlay(this, mapView, coords, geocode); } @Override - protected void drawOverlayBitmap(Canvas canvas, Point drawPosition, - Projection projection, byte drawZoomLevel) { + protected void drawOverlayBitmap(final Canvas canvas, final Point drawPosition, + final Projection projection, final byte drawZoomLevel) { if (overlayBase != null) { overlayBase.drawOverlayBitmap(canvas, drawPosition, new MapsforgeMapProjection(projection), drawZoomLevel); diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java deleted file mode 100644 index a8111ed..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlay.java +++ /dev/null @@ -1,113 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.maps.CachesOverlay; -import cgeo.geocaching.maps.interfaces.ItemizedOverlayImpl; -import cgeo.geocaching.maps.interfaces.MapProjectionImpl; -import cgeo.geocaching.maps.interfaces.MapViewImpl; - -import org.mapsforge.android.mapsold.ItemizedOverlay; -import org.mapsforge.android.mapsold.Projection; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Point; -import android.graphics.drawable.Drawable; - -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class MapsforgeCacheOverlay extends ItemizedOverlay<MapsforgeCacheOverlayItem> implements ItemizedOverlayImpl { - - private CachesOverlay base; - private Lock lock = new ReentrantLock(); - - public MapsforgeCacheOverlay(Context contextIn, Drawable markerIn) { - super(boundCenterBottom(markerIn)); - base = new CachesOverlay(this, contextIn); - } - - @Override - public CachesOverlay getBase() { - return base; - } - - @Override - protected MapsforgeCacheOverlayItem createItem(int i) { - if (base == null) { - return null; - } - - return (MapsforgeCacheOverlayItem) base.createItem(i); - } - - @Override - public int size() { - if (base == null) { - return 0; - } - - return base.size(); - } - - @Override - protected boolean onTap(int arg0) { - if (base == null) { - return false; - } - - return base.onTap(arg0); - } - - @Override - protected void drawOverlayBitmap(Canvas canvas, Point drawPosition, - Projection projection, byte drawZoomLevel) { - base.drawOverlayBitmap(canvas, drawPosition, new MapsforgeMapProjection(projection), drawZoomLevel); - } - - @Override - public void superPopulate() { - populate(); - } - - @Override - public Drawable superBoundCenterBottom(Drawable marker) { - return ItemizedOverlay.boundCenterBottom(marker); - } - - @Override - public void superSetLastFocusedItemIndex(int i) { - // nothing to do - } - - @Override - public boolean superOnTap(int index) { - return super.onTap(index); - } - - @Override - public void superDraw(Canvas canvas, MapViewImpl mapView, boolean shadow) { - // nothing to do here... - } - - @Override - public void superDrawOverlayBitmap(Canvas canvas, Point drawPosition, - MapProjectionImpl projection, byte drawZoomLevel) { - super.drawOverlayBitmap(canvas, drawPosition, (Projection) projection.getImpl(), drawZoomLevel); - } - - @Override - public void lock() { - lock.lock(); - } - - @Override - public void unlock() { - lock.unlock(); - } - - @Override - public MapViewImpl getMapViewImpl() { - return (MapViewImpl) internalMapView; - } - -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlayItem.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlayItem.java deleted file mode 100644 index 4e4a358..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeCacheOverlayItem.java +++ /dev/null @@ -1,37 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.IWaypoint; -import cgeo.geocaching.maps.interfaces.CachesOverlayItemImpl; - -import org.mapsforge.android.mapsold.GeoPoint; -import org.mapsforge.android.mapsold.OverlayItem; - -import android.graphics.drawable.Drawable; - -public class MapsforgeCacheOverlayItem extends OverlayItem implements CachesOverlayItemImpl { - final private IWaypoint coord; - final private boolean applyDistanceRule; - - public MapsforgeCacheOverlayItem(IWaypoint coordinate, boolean applyDistanceRule) { - super(new GeoPoint(coordinate.getCoords().getLatitudeE6(), coordinate.getCoords().getLongitudeE6()), coordinate.getName(), ""); - - this.coord = coordinate; - this.applyDistanceRule = applyDistanceRule; - } - - @Override - public IWaypoint getCoord() { - return coord; - } - - @Override - public Drawable getMarker(int index) { - return getMarker(); - } - - @Override - public boolean applyDistanceRule() { - return applyDistanceRule; - } - -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeGeoPoint.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeGeoPoint.java deleted file mode 100644 index c801e3f..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeGeoPoint.java +++ /dev/null @@ -1,18 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.maps.interfaces.GeoPointImpl; - -import org.mapsforge.android.mapsold.GeoPoint; - -public class MapsforgeGeoPoint extends GeoPoint implements GeoPointImpl { - - public MapsforgeGeoPoint(int latitudeE6, int longitudeE6) { - super(latitudeE6, longitudeE6); - } - - @Override - public Geopoint getCoords() { - return new Geopoint(getLatitudeE6() / 1e6, getLongitudeE6() / 1e6); - } -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapActivity024.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapActivity024.java deleted file mode 100644 index daeb2b8..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapActivity024.java +++ /dev/null @@ -1,124 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.activity.FilteredActivity; -import cgeo.geocaching.maps.AbstractMap; -import cgeo.geocaching.maps.CGeoMap; -import cgeo.geocaching.maps.interfaces.MapActivityImpl; - -import org.mapsforge.android.mapsold.MapActivity; - -import android.app.Activity; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -public class MapsforgeMapActivity024 extends MapActivity implements MapActivityImpl, FilteredActivity { - - private AbstractMap mapBase; - - public MapsforgeMapActivity024() { - mapBase = new CGeoMap(this); - } - - @Override - public Activity getActivity() { - return this; - } - - @Override - protected void onCreate(Bundle icicle) { - mapBase.onCreate(icicle); - } - - @Override - protected void onSaveInstanceState(final Bundle outState) { - mapBase.onSaveInstanceState(outState); - } - - @Override - protected void onDestroy() { - mapBase.onDestroy(); - } - - @Override - protected void onPause() { - mapBase.onPause(); - } - - @Override - protected void onResume() { - mapBase.onResume(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return mapBase.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return mapBase.onOptionsItemSelected(item); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - return mapBase.onPrepareOptionsMenu(menu); - } - - @Override - protected void onStop() { - mapBase.onStop(); - } - - @Override - public void superOnCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public boolean superOnCreateOptionsMenu(Menu menu) { - return super.onCreateOptionsMenu(menu); - } - - @Override - public void superOnDestroy() { - super.onDestroy(); - } - - @Override - public boolean superOnOptionsItemSelected(MenuItem item) { - return super.onOptionsItemSelected(item); - } - - @Override - public void superOnResume() { - super.onResume(); - } - - @Override - public void superOnStop() { - super.onStop(); - } - - @Override - public void superOnPause() { - super.onPause(); - } - - @Override - public boolean superOnPrepareOptionsMenu(Menu menu) { - return super.onPrepareOptionsMenu(menu); - } - - @Override - public void navigateUp(View view) { - ActivityMixin.navigateUp(this); - } - - @Override - public void showFilterMenu(View view) { - // do nothing, the filter bar only shows the global filter - } -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapController.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapController.java deleted file mode 100644 index db33d56..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapController.java +++ /dev/null @@ -1,55 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.maps.interfaces.GeoPointImpl; -import cgeo.geocaching.maps.interfaces.MapControllerImpl; - -import org.mapsforge.android.mapsold.GeoPoint; -import org.mapsforge.android.mapsold.MapController; - -public class MapsforgeMapController implements MapControllerImpl { - - private MapController mapController; - private int maxZoomLevel; - - public MapsforgeMapController(MapController mapControllerIn, int maxZoomLevelIn) { - mapController = mapControllerIn; - maxZoomLevel = maxZoomLevelIn; - } - - @Override - public void animateTo(GeoPointImpl geoPoint) { - mapController.setCenter(castToGeoPointImpl(geoPoint)); - } - - private static GeoPoint castToGeoPointImpl(GeoPointImpl geoPoint) { - assert geoPoint instanceof GeoPoint; - return (GeoPoint) geoPoint; - } - - @Override - public void setCenter(GeoPointImpl geoPoint) { - mapController.setCenter(castToGeoPointImpl(geoPoint)); - } - - /** - * Set the map zoom level to mapzoom-1 or maxZoomLevel, whichever is least - * mapzoom-1 is used to be compatible with Google Maps zoom levels - */ - @Override - public void setZoom(int mapzoom) { - // Google Maps and OSM Maps use different zoom levels for the same view. - // All OSM Maps zoom levels are offset by 1 so they match Google Maps. - mapController.setZoom(Math.min(mapzoom - 1, maxZoomLevel)); - } - - @Override - public void zoomToSpan(int latSpanE6, int lonSpanE6) { - - if (latSpanE6 != 0 && lonSpanE6 != 0) { - // calculate zoomlevel - int distDegree = Math.max(latSpanE6, lonSpanE6); - int zoomLevel = (int) Math.floor(Math.log(360.0 * 1e6 / distDegree) / Math.log(2)); - mapController.setZoom(zoomLevel + 1); - } - } -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapItemFactory024.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapItemFactory024.java deleted file mode 100644 index 4f1d34c..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapItemFactory024.java +++ /dev/null @@ -1,20 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.IWaypoint; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.maps.interfaces.CachesOverlayItemImpl; -import cgeo.geocaching.maps.interfaces.GeoPointImpl; -import cgeo.geocaching.maps.interfaces.MapItemFactory; - -public class MapsforgeMapItemFactory024 implements MapItemFactory { - - @Override - public GeoPointImpl getGeoPointBase(final Geopoint coords) { - return new MapsforgeGeoPoint(coords.getLatitudeE6(), coords.getLongitudeE6()); - } - - @Override - public CachesOverlayItemImpl getCachesOverlayItem(final IWaypoint coordinate, boolean applyDistanceRule) { - return new MapsforgeCacheOverlayItem(coordinate, applyDistanceRule); - } -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapProjection.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapProjection.java deleted file mode 100644 index 9d36b7d..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapProjection.java +++ /dev/null @@ -1,29 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.maps.interfaces.GeoPointImpl; -import cgeo.geocaching.maps.interfaces.MapProjectionImpl; - -import org.mapsforge.android.mapsold.GeoPoint; -import org.mapsforge.android.mapsold.Projection; - -import android.graphics.Point; - -public class MapsforgeMapProjection implements MapProjectionImpl { - - private Projection projection; - - public MapsforgeMapProjection(Projection projectionIn) { - projection = projectionIn; - } - - @Override - public void toPixels(GeoPointImpl leftGeo, Point left) { - projection.toPixels((GeoPoint) leftGeo, left); - } - - @Override - public Object getImpl() { - return projection; - } - -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java deleted file mode 100644 index 8dd15fc..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeMapView024.java +++ /dev/null @@ -1,254 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Viewport; -import cgeo.geocaching.maps.CachesOverlay; -import cgeo.geocaching.maps.PositionAndScaleOverlay; -import cgeo.geocaching.maps.interfaces.GeneralOverlay; -import cgeo.geocaching.maps.interfaces.GeoPointImpl; -import cgeo.geocaching.maps.interfaces.MapControllerImpl; -import cgeo.geocaching.maps.interfaces.MapProjectionImpl; -import cgeo.geocaching.maps.interfaces.MapViewImpl; -import cgeo.geocaching.maps.interfaces.OnMapDragListener; -import cgeo.geocaching.settings.Settings; -import cgeo.geocaching.utils.Log; - -import org.eclipse.jdt.annotation.NonNull; -import org.mapsforge.android.mapsold.GeoPoint; -import org.mapsforge.android.mapsold.MapDatabase; -import org.mapsforge.android.mapsold.MapView; -import org.mapsforge.android.mapsold.MapViewMode; -import org.mapsforge.android.mapsold.Overlay; -import org.mapsforge.android.mapsold.Projection; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.GestureDetector; -import android.view.GestureDetector.SimpleOnGestureListener; -import android.view.MotionEvent; -import android.widget.Toast; -public class MapsforgeMapView024 extends MapView implements MapViewImpl { - private GestureDetector gestureDetector; - private OnMapDragListener onDragListener; - private final MapsforgeMapController mapController = new MapsforgeMapController(getController(), getMaxZoomLevel()); - - public MapsforgeMapView024(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(context); - } - - private void initialize(Context context) { - if (isInEditMode()) { - return; - } - gestureDetector = new GestureDetector(context, new GestureListener()); - } - - @Override - public void draw(@NonNull Canvas canvas) { - try { - // Google Maps and OSM Maps use different zoom levels for the same view. - // Here we don't want the Google Maps compatible zoom level, but the actual one. - if (getActualMapZoomLevel() > 22) { // to avoid too close zoom level (mostly on Samsung Galaxy S series) - getController().setZoom(22); - } - - super.draw(canvas); - } catch (Exception e) { - Log.e("MapsforgeMapView024.draw", e); - } - } - - @Override - public void displayZoomControls(boolean takeFocus) { - // nothing to do here - } - - @Override - public MapControllerImpl getMapController() { - return mapController; - } - - @Override - @NonNull - public GeoPointImpl getMapViewCenter() { - GeoPoint point = getMapCenter(); - return new MapsforgeGeoPoint(point.getLatitudeE6(), point.getLongitudeE6()); - } - - @Override - public Viewport getViewport() { - return new Viewport(getMapViewCenter(), getLatitudeSpan() / 1e6, getLongitudeSpan() / 1e6); - } - - @Override - public void clearOverlays() { - getOverlays().clear(); - } - - @Override - public MapProjectionImpl getMapProjection() { - return new MapsforgeMapProjection(getProjection()); - } - - @Override - public CachesOverlay createAddMapOverlay(Context context, Drawable drawable) { - - MapsforgeCacheOverlay ovl = new MapsforgeCacheOverlay(context, drawable); - getOverlays().add(ovl); - return ovl.getBase(); - } - - @Override - public PositionAndScaleOverlay createAddPositionAndScaleOverlay() { - MapsforgeOverlay ovl = new MapsforgeOverlay(); - getOverlays().add(ovl); - return (PositionAndScaleOverlay) ovl.getBase(); - } - - @Override - public int getLatitudeSpan() { - - int span = 0; - - Projection projection = getProjection(); - - if (projection != null && getHeight() > 0) { - - GeoPoint low = projection.fromPixels(0, 0); - GeoPoint high = projection.fromPixels(0, getHeight()); - - if (low != null && high != null) { - span = Math.abs(high.getLatitudeE6() - low.getLatitudeE6()); - } - } - - return span; - } - - @Override - public int getLongitudeSpan() { - - int span = 0; - - Projection projection = getProjection(); - - if (projection != null && getWidth() > 0) { - GeoPoint low = projection.fromPixels(0, 0); - GeoPoint high = projection.fromPixels(getWidth(), 0); - - if (low != null && high != null) { - span = Math.abs(high.getLongitudeE6() - low.getLongitudeE6()); - } - } - - return span; - } - - @Override - public void preLoad() { - // Nothing to do here - } - - /** - * Get the map zoom level which is compatible with Google Maps. - * - * @return the current map zoom level +1 - */ - @Override - public int getMapZoomLevel() { - // Google Maps and OSM Maps use different zoom levels for the same view. - // All OSM Maps zoom levels are offset by 1 so they match Google Maps. - return getZoomLevel() + 1; - } - - /** - * Get the actual map zoom level - * - * @return the current map zoom level with no adjustments - */ - private int getActualMapZoomLevel() { - return getZoomLevel(); - } - - @Override - public void setMapSource() { - setMapViewMode(MapViewMode.CANVAS_RENDERER); - setMapFile(Settings.getMapFile()); - if (!MapDatabase.isValidMapFile(Settings.getMapFile())) { - Log.e("MapsforgeMapView024: Invalid map file"); - } - Toast.makeText( - getContext(), - getContext().getResources().getString(R.string.warn_deprecated_mapfile), - Toast.LENGTH_LONG) - .show(); - } - - @Override - public void repaintRequired(GeneralOverlay overlay) { - - if (null == overlay) { - invalidate(); - } else { - try { - Overlay ovl = (Overlay) overlay.getOverlayImpl(); - - if (ovl != null) { - ovl.requestRedraw(); - } - - } catch (Exception e) { - Log.e("MapsforgeMapView024.repaintRequired", e); - } - } - } - - @Override - public void setOnDragListener(OnMapDragListener onDragListener) { - this.onDragListener = onDragListener; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - gestureDetector.onTouchEvent(ev); - return super.onTouchEvent(ev); - } - - private class GestureListener extends SimpleOnGestureListener { - @Override - public boolean onDoubleTap(MotionEvent e) { - if (onDragListener != null) { - onDragListener.onDrag(); - } - return true; - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, - float distanceX, float distanceY) { - if (onDragListener != null) { - onDragListener.onDrag(); - } - return super.onScroll(e1, e2, distanceX, distanceY); - } - } - - @Override - public boolean needsInvertedColors() { - return false; - } - - @Override - public boolean hasMapThemes() { - // not supported - return false; - } - - @Override - public void setMapTheme() { - // not supported - } -} diff --git a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeOverlay.java b/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeOverlay.java deleted file mode 100644 index bfb3548..0000000 --- a/main/src/cgeo/geocaching/maps/mapsforge/v024/MapsforgeOverlay.java +++ /dev/null @@ -1,54 +0,0 @@ -package cgeo.geocaching.maps.mapsforge.v024; - -import cgeo.geocaching.maps.PositionAndScaleOverlay; -import cgeo.geocaching.maps.interfaces.GeneralOverlay; -import cgeo.geocaching.maps.interfaces.MapViewImpl; -import cgeo.geocaching.maps.interfaces.OverlayImpl; - -import org.mapsforge.android.mapsold.Overlay; -import org.mapsforge.android.mapsold.Projection; - -import android.graphics.Canvas; -import android.graphics.Point; - -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class MapsforgeOverlay extends Overlay implements OverlayImpl { - - private PositionAndScaleOverlay overlayBase = null; - private Lock lock = new ReentrantLock(); - - public MapsforgeOverlay() { - overlayBase = new PositionAndScaleOverlay(this); - } - - @Override - protected void drawOverlayBitmap(Canvas canvas, Point drawPosition, - Projection projection, byte drawZoomLevel) { - - if (overlayBase != null) { - overlayBase.drawOverlayBitmap(canvas, drawPosition, new MapsforgeMapProjection(projection), drawZoomLevel); - } - } - - public GeneralOverlay getBase() { - return overlayBase; - } - - @Override - public void lock() { - lock.lock(); - } - - @Override - public void unlock() { - lock.unlock(); - } - - @Override - public MapViewImpl getMapViewImpl() { - return (MapViewImpl) internalMapView; - } - -} diff --git a/main/src/cgeo/geocaching/network/AndroidBeam.java b/main/src/cgeo/geocaching/network/AndroidBeam.java new file mode 100644 index 0000000..68b8f94 --- /dev/null +++ b/main/src/cgeo/geocaching/network/AndroidBeam.java @@ -0,0 +1,93 @@ +package cgeo.geocaching.network; + +import cgeo.geocaching.CgeoApplication; + +import org.apache.commons.io.Charsets; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.Nullable; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.NfcAdapter.CreateNdefMessageCallback; +import android.nfc.NfcEvent; +import android.os.Build; + +/** + * utility class managing all NFC related tasks + */ +public class AndroidBeam { + + private AndroidBeam() { + // utility class + } + + /** + * returns the URI transmitted via Android Beam, or the URI contained in the data of the intent + */ + @Nullable + public static Uri getUri(final Intent intent) { + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { + final NdefMessage msg = (NdefMessage) intent.getExtras().getParcelableArray(NfcAdapter.EXTRA_NDEF_MESSAGES)[0]; + return Uri.parse("http://" + new String(msg.getRecords()[0].getPayload(), Charsets.UTF_8)); + } + return intent.getData(); + } + + // Do not support older devices than Android 4.0 + // Although there even are 2.3 devices (Nexus S) + // 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 or null for no sharing. */ + @Nullable + public String getAndroidBeamUri(); + } + + public static void enable(final Activity activity, final ActivitySharingInterface sharingInterface) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + initializeICSAndroidBeam(activity, createMessageCallback(sharingInterface)); + } + } + + public static void disable(final Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + initializeICSAndroidBeam(activity, null); + } + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private static void initializeICSAndroidBeam(final Activity activity, final CreateNdefMessageCallback messageCallback) { + final NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity); + if (nfcAdapter == null) { + return; + } + nfcAdapter.setNdefPushMessageCallback(messageCallback, activity); + + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private static CreateNdefMessageCallback createMessageCallback(final ActivitySharingInterface sharingInterface) { + return new NfcAdapter.CreateNdefMessageCallback() { + @Override + public NdefMessage createNdefMessage(final NfcEvent event) { + String uri = sharingInterface.getAndroidBeamUri(); + if (uri == null) { + return null; + } + // normalize our modified URLs for beaming + uri = StringUtils.replace(uri, "geocaching.com//", "geocaching.com/"); + final NdefRecord[] records = { + NdefRecord.createUri(uri), + NdefRecord.createApplicationRecord(CgeoApplication.getInstance().getPackageName()) + }; + return new NdefMessage(records); + } + }; + } + +} diff --git a/main/src/cgeo/geocaching/network/Cookies.java b/main/src/cgeo/geocaching/network/Cookies.java index bcfc893..27013ba 100644 --- a/main/src/cgeo/geocaching/network/Cookies.java +++ b/main/src/cgeo/geocaching/network/Cookies.java @@ -29,7 +29,7 @@ public abstract class Cookies { } public static String dumpCookieStore() { - StringBuilder cookies = new StringBuilder(); + final StringBuilder cookies = new StringBuilder(); for (final Cookie cookie : cookieStore.getCookies()) { cookies.append(cookie.getName()); cookies.append('='); diff --git a/main/src/cgeo/geocaching/network/DownloadProgress.java b/main/src/cgeo/geocaching/network/DownloadProgress.java new file mode 100644 index 0000000..e0f20dc --- /dev/null +++ b/main/src/cgeo/geocaching/network/DownloadProgress.java @@ -0,0 +1,15 @@ +package cgeo.geocaching.network; + +public class DownloadProgress { + + private DownloadProgress() { + // Do not instantiate + } + + public static final int MSG_DONE = -1; + public static final int MSG_SERVER_FAIL = -2; + public static final int MSG_NO_REGISTRATION = -3; + public static final int MSG_WAITING = 0; + public static final int MSG_LOADING = 1; + public static final int MSG_LOADED = 2; +} diff --git a/main/src/cgeo/geocaching/network/HtmlImage.java b/main/src/cgeo/geocaching/network/HtmlImage.java index 31edc9f..fe67af4 100644 --- a/main/src/cgeo/geocaching/network/HtmlImage.java +++ b/main/src/cgeo/geocaching/network/HtmlImage.java @@ -12,13 +12,13 @@ import cgeo.geocaching.utils.ImageUtils; import cgeo.geocaching.utils.ImageUtils.ContainerDrawable; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.RxUtils; +import cgeo.geocaching.utils.RxUtils.ObservableCache; import ch.boye.httpclientandroidlib.HttpResponse; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -44,22 +44,14 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.util.Date; +import java.util.HashMap; +import java.util.Map; -public class HtmlImage implements Html.ImageGetter { +/** + * All-purpose image getter that can also be used as a ImageGetter interface when displaying caches. + */ - // This class implements an all-purpose image getter that can also be used as a ImageGetter interface - // when displaying caches. An instance mainly has three possible use cases: - // - If onlySave is true, getDrawable() will return null immediately and will queue the image retrieval - // and saving in the loading subject. Downloads will start in parallel when the blocking - // waitForBackgroundLoading() method is called, and they can be cancelled through the given handler. - // - If onlySave is false and the instance is called through fetchDrawable(), then an observable for the - // given URL will be returned. This observable will emit the local copy of the image if it is present, - // regardless of its freshness, then if needed an updated fresher copy after retrieving it from the network. - // - If onlySave is false and the instance is used as an ImageGetter, only the final version of the - // image will be returned, unless a view has been provided. If it has, then a dummy drawable is returned - // and is updated when the image is available, possibly several times if we had a stale copy of the image - // and then got a new one from the network. +public class HtmlImage implements Html.ImageGetter { private static final String[] BLOCKED = new String[] { "gccounter.de", @@ -79,9 +71,9 @@ public class HtmlImage implements Html.ImageGetter { }; public static final String SHARED = "shared"; - final private String geocode; + @NonNull final private String geocode; /** - * on error: return large error image, if <code>true</code>, otherwise empty 1x1 image + * on error: return large error image, if {@code true}, otherwise empty 1x1 image */ final private boolean returnErrorImage; final private int listId; @@ -89,25 +81,57 @@ public class HtmlImage implements Html.ImageGetter { final private int maxWidth; final private int maxHeight; final private Resources resources; - final private TextView view; + protected final TextView view; + final private Map<String, BitmapDrawable> cache = new HashMap<>(); + + final private ObservableCache<String, BitmapDrawable> observableCache = new ObservableCache<>(new Func1<String, Observable<BitmapDrawable>>() { + @Override + public Observable<BitmapDrawable> call(final String url) { + return fetchDrawableUncached(url); + } + }); // Background loading final private PublishSubject<Observable<String>> loading = PublishSubject.create(); - final private Observable<String> waitForEnd = Observable.merge(loading).publish().refCount(); - final CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe()); + final private Observable<String> waitForEnd = Observable.merge(loading).cache(); + final private CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe()); /** - * Create a new HtmlImage object with different behaviours depending on <tt>onlySave</tt> and <tt>view</tt> values. + * Create a new HtmlImage object with different behaviors depending on <tt>onlySave</tt> and <tt>view</tt> values. + * There are the three possible use cases: + * <ul> + * <li>If onlySave is true, {@link #getDrawable(String)} will return <tt>null</tt> immediately and will queue the + * image retrieval and saving in the loading subject. Downloads will start in parallel when the blocking + * {@link #waitForEndObservable(cgeo.geocaching.utils.CancellableHandler)} method is called, and they can be + * cancelled through the given handler.</li> + * <li>If <tt>onlySave</tt> is <tt>false</tt> and the instance is called through {@link #fetchDrawable(String)}, + * then an observable for the given URL will be returned. This observable will emit the local copy of the image if + * it is present regardless of its freshness, then if needed an updated fresher copy after retrieving it from the + * network.</li> + * <li>If <tt>onlySave</tt> is <tt>false</tt> and the instance is used as an {@link android.text.Html.ImageGetter}, + * only the final version of the image will be returned, unless a view has been provided. If it has, then a dummy + * drawable is returned and is updated when the image is available, possibly several times if we had a stale copy of + * the image and then got a new one from the network.</li> + * </ul> * - * @param geocode the geocode of the item for which we are requesting the image - * @param returnErrorImage set to <tt>true</tt> if an error image should be returned in case of a problem, - * <tt>false</tt> to get a transparent 1x1 image instead - * @param listId the list this cache belongs to, used to determine if an older image for the offline case can be used or not - * @param onlySave if set to <tt>true</tt>, {@link #getDrawable(String)} will only fetch and store the image, not return it - * @param view if non-null, {@link #getDrawable(String)} will return an initially empty drawable which will be redrawn when - * the image is ready through an invalidation of the given view + * @param geocode + * the geocode of the item for which we are requesting the image, or {@link #SHARED} to use the shared + * cache directory + * @param returnErrorImage + * set to <tt>true</tt> if an error image should be returned in case of a problem, <tt>false</tt> to get + * a transparent 1x1 image instead + * @param listId + * the list this cache belongs to, used to determine if an older image for the offline case can be used + * or not + * @param onlySave + * if set to <tt>true</tt>, {@link #getDrawable(String)} will only fetch and store the image, not return + * it + * @param view + * if non-null, {@link #getDrawable(String)} will return an initially empty drawable which will be + * redrawn when + * the image is ready through an invalidation of the given view */ - public HtmlImage(final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave, final TextView view) { + public HtmlImage(@NonNull final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave, final TextView view) { this.geocode = geocode; this.returnErrorImage = returnErrorImage; this.listId = listId; @@ -121,12 +145,12 @@ public class HtmlImage implements Html.ImageGetter { } /** - * Create a new HtmlImage object with different behaviours depending on <tt>onlySave</tt> value. No view object + * Create a new HtmlImage object with different behaviors depending on <tt>onlySave</tt> value. No view object * will be tied to this HtmlImage. * * For documentation, see {@link #HtmlImage(String, boolean, int, boolean, TextView)}. */ - public HtmlImage(final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) { + public HtmlImage(@NonNull final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) { this(geocode, returnErrorImage, listId, onlySave, null); } @@ -141,6 +165,9 @@ public class HtmlImage implements Html.ImageGetter { @Nullable @Override public BitmapDrawable getDrawable(final String url) { + if (cache.containsKey(url)) { + return cache.get(url); + } final Observable<BitmapDrawable> drawable = fetchDrawable(url); if (onlySave) { loading.onNext(drawable.map(new Func1<BitmapDrawable, String>() { @@ -149,30 +176,37 @@ public class HtmlImage implements Html.ImageGetter { return url; } })); + cache.put(url, null); return null; } - if (view == null) { - return drawable.toBlocking().lastOrDefault(null); - } + final BitmapDrawable result = view == null ? drawable.toBlocking().lastOrDefault(null) : getContainerDrawable(drawable); + cache.put(url, result); + return result; + } + + protected BitmapDrawable getContainerDrawable(final Observable<BitmapDrawable> drawable) { return new ContainerDrawable(view, drawable); } - // Caches are loaded from disk on a computation scheduler to avoid using more threads than cores while decoding - // the image. Downloads happen on downloadScheduler, in parallel with image decoding. public Observable<BitmapDrawable> fetchDrawable(final String url) { + return observableCache.get(url); + } + // Caches are loaded from disk on a computation scheduler to avoid using more threads than cores while decoding + // the image. Downloads happen on downloadScheduler, in parallel with image decoding. + private Observable<BitmapDrawable> fetchDrawableUncached(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() { - final Bitmap bitmap = loadCachedImage(FileUtils.urlToFile(url), true).getLeft(); - return bitmap != null ? Observable.from(ImageUtils.scaleBitmapToFitDisplay(bitmap)) : Observable.<BitmapDrawable>empty(); + public Observable<BitmapDrawable> call() { + final Bitmap bitmap = loadCachedImage(FileUtils.urlToFile(url), true).left; + return bitmap != null ? Observable.just(ImageUtils.scaleBitmapToFitDisplay(bitmap)) : Observable.<BitmapDrawable>empty(); } }).subscribeOn(RxUtils.computationScheduler); } @@ -187,9 +221,9 @@ public class HtmlImage implements Html.ImageGetter { subscriber.add(RxUtils.computationScheduler.createWorker().schedule(new Action0() { @Override public void call() { - final Pair<BitmapDrawable, Boolean> loaded = loadFromDisk(); - final BitmapDrawable bitmap = loaded.getLeft(); - if (loaded.getRight()) { + final ImmutablePair<BitmapDrawable, Boolean> loaded = loadFromDisk(); + final BitmapDrawable bitmap = loaded.left; + if (loaded.right) { subscriber.onNext(bitmap); subscriber.onCompleted(); return; @@ -206,14 +240,9 @@ public class HtmlImage implements Html.ImageGetter { })); } - private Pair<BitmapDrawable, Boolean> loadFromDisk() { - final Pair<Bitmap, Boolean> loadResult = loadImageFromStorage(url, pseudoGeocode, shared); - final Bitmap bitmap = loadResult.getLeft(); - return new ImmutablePair<>(bitmap != null ? - ImageUtils.scaleBitmapToFitDisplay(bitmap) : - null, - loadResult.getRight() - ); + private ImmutablePair<BitmapDrawable, Boolean> loadFromDisk() { + final ImmutablePair<Bitmap, Boolean> loadResult = loadImageFromStorage(url, pseudoGeocode, shared); + return scaleImage(loadResult); } private void downloadAndSave(final Subscriber<? super BitmapDrawable> subscriber) { @@ -233,27 +262,33 @@ public class HtmlImage implements Html.ImageGetter { } if (onlySave) { subscriber.onCompleted(); - } else { - RxUtils.computationScheduler.createWorker().schedule(new Action0() { - @Override - public void call() { - final Pair<BitmapDrawable, Boolean> loaded = loadFromDisk(); - final BitmapDrawable image = loaded.getLeft(); - if (image != null) { - subscriber.onNext(image); - } else { - subscriber.onNext(returnErrorImage ? - new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded)) : - ImageUtils.getTransparent1x1Drawable(resources)); - } - subscriber.onCompleted(); - } - }); + return; } + RxUtils.computationScheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + final ImmutablePair<BitmapDrawable, Boolean> loaded = loadFromDisk(); + final BitmapDrawable image = loaded.left; + if (image != null) { + subscriber.onNext(image); + } else { + subscriber.onNext(returnErrorImage ? + new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded)) : + ImageUtils.getTransparent1x1Drawable(resources)); + } + subscriber.onCompleted(); + } + }); } }); } + @SuppressWarnings("static-method") + protected ImmutablePair<BitmapDrawable, Boolean> scaleImage(final ImmutablePair<Bitmap, Boolean> loadResult) { + final Bitmap bitmap = loadResult.left; + return ImmutablePair.of(bitmap != null ? ImageUtils.scaleBitmapToFitDisplay(bitmap) : null, loadResult.right); + } + public Observable<String> waitForEndObservable(@Nullable final CancellableHandler handler) { if (handler != null) { handler.unsubscribeIfCancelled(subscription); @@ -319,14 +354,14 @@ public class HtmlImage implements Html.ImageGetter { * @param url the image URL * @param pseudoGeocode the geocode or the shared name * @param forceKeep keep the image if it is there, without checking its freshness - * @return <code>true</code> if the image was there and is fresh enough, <code>false</code> otherwise + * @return A pair whose first element is the bitmap if available, and the second one is <code>true</code> if the image is present and fresh enough. */ @NonNull - private Pair<Bitmap, Boolean> loadImageFromStorage(final String url, final String pseudoGeocode, final boolean forceKeep) { + private ImmutablePair<Bitmap, Boolean> loadImageFromStorage(final String url, @NonNull final String pseudoGeocode, final boolean forceKeep) { try { final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, false); - final Pair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep); - if (image.getRight() || image.getLeft() != null) { + final ImmutablePair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep); + if (image.right || image.left != null) { return image; } final File fileSec = LocalStorage.getStorageSecFile(pseudoGeocode, url, true); @@ -334,7 +369,7 @@ public class HtmlImage implements Html.ImageGetter { } catch (final Exception e) { Log.w("HtmlImage.loadImageFromStorage", e); } - return new ImmutablePair<>(null, false); + return ImmutablePair.of((Bitmap) null, false); } @Nullable @@ -366,17 +401,17 @@ public class HtmlImage implements Html.ImageGetter { * * @param file the file on disk * @param forceKeep keep the image if it is there, without checking its freshness - * @return a pair with <code>true</code> if the image was there and is fresh enough or <code>false</code> otherwise, - * and the image (possibly <code>null</code> if the first component is <code>false</code> and the image - * could not be loaded, or if the first component is <code>true</code> and <code>onlySave</code> is also + * @return a pair with <code>true</code> in the second component if the image was there and is fresh enough or <code>false</code> otherwise, + * and the image (possibly <code>null</code> if the second component is <code>false</code> and the image + * could not be loaded, or if the second component is <code>true</code> and <code>onlySave</code> is also * <code>true</code>) */ @NonNull - private Pair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) { + private ImmutablePair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) { if (file.exists()) { - final boolean freshEnough = listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (new Date().getTime() - (24 * 60 * 60 * 1000)) || forceKeep; - if (onlySave) { - return new ImmutablePair<>(null, true); + final boolean freshEnough = listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (System.currentTimeMillis() - (24 * 60 * 60 * 1000)) || forceKeep; + if (freshEnough && onlySave) { + return ImmutablePair.of((Bitmap) null, true); } final BitmapFactory.Options bfOptions = new BitmapFactory.Options(); bfOptions.inTempStorage = new byte[16 * 1024]; @@ -385,12 +420,11 @@ public class HtmlImage implements Html.ImageGetter { final Bitmap image = BitmapFactory.decodeFile(file.getPath(), bfOptions); if (image == null) { Log.e("Cannot decode bitmap from " + file.getPath()); - return new ImmutablePair<>(null, false); + return ImmutablePair.of((Bitmap) null, false); } - return new ImmutablePair<>(image, - freshEnough); + return ImmutablePair.of(image, freshEnough); } - return new ImmutablePair<>(null, false); + return ImmutablePair.of((Bitmap) null, false); } private void setSampleSize(final File file, final BitmapFactory.Options bfOptions) { diff --git a/main/src/cgeo/geocaching/network/Network.java b/main/src/cgeo/geocaching/network/Network.java index a49b302..7eb6f61 100644 --- a/main/src/cgeo/geocaching/network/Network.java +++ b/main/src/cgeo/geocaching/network/Network.java @@ -1,7 +1,9 @@ package cgeo.geocaching.network; +import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.files.LocalStorage; import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.JsonUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.TextUtils; @@ -26,11 +28,12 @@ import ch.boye.httpclientandroidlib.params.CoreConnectionPNames; import ch.boye.httpclientandroidlib.params.CoreProtocolPNames; import ch.boye.httpclientandroidlib.params.HttpParams; import ch.boye.httpclientandroidlib.util.EntityUtils; + +import com.fasterxml.jackson.databind.node.ObjectNode; + import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.Nullable; -import org.json.JSONException; -import org.json.JSONObject; import android.content.Context; import android.net.ConnectivityManager; @@ -43,6 +46,7 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.regex.Pattern; public abstract class Network { @@ -51,25 +55,25 @@ public abstract class Network { /** Native user agent, taken from a Android 2.2 Nexus **/ private final static String NATIVE_USER_AGENT = "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"; - private static final String PATTERN_PASSWORD = "(?<=[\\?&])[Pp]ass(w(or)?d)?=[^&#$]+"; + private static final Pattern PATTERN_PASSWORD = Pattern.compile("(?<=[\\?&])[Pp]ass(w(or)?d)?=[^&#$]+"); - private final static HttpParams clientParams = new BasicHttpParams(); + private final static HttpParams CLIENT_PARAMS = new BasicHttpParams(); static { - clientParams.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, CharEncoding.UTF_8); - clientParams.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 30000); - clientParams.setParameter(CoreConnectionPNames.SO_TIMEOUT, 90000); - clientParams.setParameter(ClientPNames.HANDLE_REDIRECTS, true); + CLIENT_PARAMS.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, CharEncoding.UTF_8); + CLIENT_PARAMS.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 30000); + CLIENT_PARAMS.setParameter(CoreConnectionPNames.SO_TIMEOUT, 30000); + CLIENT_PARAMS.setParameter(ClientPNames.HANDLE_REDIRECTS, true); } private static String hidePassword(final String message) { - return message.replaceAll(PATTERN_PASSWORD, "password=***"); + return PATTERN_PASSWORD.matcher(message).replaceAll("password=***"); } private static HttpClient getHttpClient() { final DefaultHttpClient client = new DefaultHttpClient(); client.setCookieStore(Cookies.cookieStore); - client.setParams(clientParams); + client.setParams(CLIENT_PARAMS); client.setRedirectStrategy(new LaxRedirectStrategy()); return new DecompressingHttpClient(client); } @@ -91,7 +95,7 @@ public abstract class Network { * * @param uri the URI to request * @param params the parameters to add to the POST request - * @params headers the headers to add to the request + * @param headers the headers to add to the request * @return the HTTP response, or null in case of an encoding error params */ @Nullable @@ -107,14 +111,14 @@ public abstract class Network { * @return the HTTP response, or null in case of an encoding error params */ @Nullable - public static HttpResponse postJsonRequest(final String uri, final JSONObject json) { - HttpPost request = new HttpPost(uri); + public static HttpResponse postJsonRequest(final String uri, final ObjectNode json) { + final HttpPost request = new HttpPost(uri); request.addHeader("Content-Type", "application/json; charset=utf-8"); if (json != null) { try { request.setEntity(new StringEntity(json.toString(), CharEncoding.UTF_8)); - } catch (UnsupportedEncodingException e) { - Log.e("postJsonRequest:JSON Entity: UnsupportedEncodingException"); + } catch (final UnsupportedEncodingException e) { + Log.e("postJsonRequest:JSON Entity: UnsupportedEncodingException", e); return null; } } @@ -170,7 +174,7 @@ public abstract class Network { @Nullable private static HttpResponse request(final String method, final String uri, @Nullable final Parameters params, @Nullable final Parameters headers, @Nullable final File cacheFile) { - HttpRequestBase request; + final HttpRequestBase request; if (method.equals("GET")) { final String fullUri = params == null ? uri : Uri.parse(uri).buildUpon().encodedQuery(params.toString()).build().toString(); request = new HttpGet(fullUri); @@ -221,6 +225,10 @@ public abstract class Network { */ @Nullable private static HttpResponse doLogRequest(final HttpRequestBase request) { + if (!isNetworkConnected()) { + return null; + } + final String reqLogStr = request.getMethod() + " " + hidePassword(request.getURI().toString()); Log.d(reqLogStr); @@ -228,7 +236,7 @@ public abstract class Network { final long before = System.currentTimeMillis(); try { final HttpResponse response = client.execute(request); - int status = response.getStatusLine().getStatusCode(); + final int status = response.getStatusLine().getStatusCode(); if (status == 200) { Log.d(status + formatTimeSpan(before) + reqLogStr); } else { @@ -344,14 +352,14 @@ public abstract class Network { * @return a JSON object if the request was successful and the body could be decoded, <code>null</code> otherwise */ @Nullable - public static JSONObject requestJSON(final String uri, @Nullable final Parameters params) { + public static ObjectNode requestJSON(final String uri, @Nullable final Parameters params) { final HttpResponse response = request("GET", uri, params, new Parameters("Accept", "application/json, text/javascript, */*; q=0.01"), null); final String responseData = getResponseData(response, false); if (responseData != null) { try { - return new JSONObject(responseData); - } catch (final JSONException e) { - Log.w("Network.requestJSON", e); + return (ObjectNode) JsonUtils.reader.readTree(responseData); + } catch (final IOException e) { + Log.w("requestJSON", e); } } @@ -369,7 +377,7 @@ public abstract class Network { if (!isSuccess(response)) { return null; } - assert(response != null); + assert response != null; final HttpEntity entity = response.getEntity(); if (entity == null) { return null; @@ -383,11 +391,11 @@ public abstract class Network { } @Nullable - private static String getResponseDataNoError(final HttpResponse response, boolean replaceWhitespace) { + private static String getResponseDataNoError(final HttpResponse response, final boolean replaceWhitespace) { try { - String data = EntityUtils.toString(response.getEntity(), CharEncoding.UTF_8); + final String data = EntityUtils.toString(response.getEntity(), CharEncoding.UTF_8); return replaceWhitespace ? TextUtils.replaceWhitespace(data) : data; - } catch (Exception e) { + } catch (final Exception e) { Log.e("getResponseData", e); return null; } @@ -420,7 +428,7 @@ public abstract class Network { * @return the body if the response comes from a successful HTTP request, <code>null</code> otherwise */ @Nullable - public static String getResponseData(@Nullable final HttpResponse response, boolean replaceWhitespace) { + public static String getResponseData(@Nullable final HttpResponse response, final boolean replaceWhitespace) { if (!isSuccess(response)) { return null; } @@ -429,7 +437,7 @@ public abstract class Network { } @Nullable - public static String rfc3986URLEncode(String text) { + public static String rfc3986URLEncode(final String text) { final String encoded = encode(text); return encoded != null ? StringUtils.replace(encoded.replace("+", "%20"), "%7E", "~") : null; } @@ -438,7 +446,7 @@ public abstract class Network { public static String decode(final String text) { try { return URLDecoder.decode(text, CharEncoding.UTF_8); - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { Log.e("Network.decode", e); } return null; @@ -448,25 +456,26 @@ public abstract class Network { public static String encode(final String text) { try { return URLEncoder.encode(text, CharEncoding.UTF_8); - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { Log.e("Network.encode", e); } return null; } + private static ConnectivityManager connectivityManager = null; + /** * Checks if the device has network connection. * - * @param context - * context of the application, cannot be null - * * @return <code>true</code> if the device is connected to the network. */ - public static boolean isNetworkConnected(Context context) { - ConnectivityManager conMan = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetwork = conMan.getActiveNetworkInfo(); - - return activeNetwork != null && activeNetwork.isConnected(); + public static boolean isNetworkConnected() { + if (connectivityManager == null) { + // Concurrent assignment would not hurt + connectivityManager = (ConnectivityManager) CgeoApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + } + final NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); + return activeNetworkInfo != null && activeNetworkInfo.isConnected(); } } diff --git a/main/src/cgeo/geocaching/network/OAuth.java b/main/src/cgeo/geocaching/network/OAuth.java index cfc62fc..4f1fcc0 100644 --- a/main/src/cgeo/geocaching/network/OAuth.java +++ b/main/src/cgeo/geocaching/network/OAuth.java @@ -6,10 +6,8 @@ import ch.boye.httpclientandroidlib.NameValuePair; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; import java.util.ArrayList; -import java.util.Date; import java.util.List; public class OAuth { @@ -18,16 +16,15 @@ public class OAuth { final String method, final boolean https, final Parameters params, - @Nullable final String token, - @Nullable final String tokenSecret, + final OAuthTokens tokens, final String consumerKey, final String consumerSecret) { params.put( "oauth_consumer_key", consumerKey, "oauth_nonce", CryptUtils.md5(Long.toString(System.currentTimeMillis())), "oauth_signature_method", "HMAC-SHA1", - "oauth_timestamp", Long.toString(new Date().getTime() / 1000), - "oauth_token", StringUtils.defaultString(token), + "oauth_timestamp", Long.toString(System.currentTimeMillis() / 1000), + "oauth_token", StringUtils.defaultString(tokens.getTokenPublic()), "oauth_version", "1.0"); params.sort(); @@ -36,19 +33,16 @@ public class OAuth { paramsEncoded.add(nameValue.getName() + "=" + OAuth.percentEncode(nameValue.getValue())); } - final String keysPacked = consumerSecret + "&" + StringUtils.defaultString(tokenSecret); // both even if empty some of them! + final String keysPacked = consumerSecret + "&" + StringUtils.defaultString(tokens.getTokenSecret()); // both even if empty some of them! final @NonNull String joinedParams = StringUtils.join(paramsEncoded.toArray(), '&'); final String requestPacked = method + "&" + OAuth.percentEncode((https ? "https" : "http") + "://" + host + path) + "&" + OAuth.percentEncode(joinedParams); params.put("oauth_signature", CryptUtils.base64Encode(CryptUtils.hashHmac(requestPacked, keysPacked))); } /** - * percent encode following http://tools.ietf.org/html/rfc5849#section-3.6 - * - * @param url - * @return + * Percent encode following http://tools.ietf.org/html/rfc5849#section-3.6 */ - static String percentEncode(@NonNull String url) { + static String percentEncode(@NonNull final String url) { return StringUtils.replace(Network.rfc3986URLEncode(url), "*", "%2A"); } } diff --git a/main/src/cgeo/geocaching/network/OAuthTokens.java b/main/src/cgeo/geocaching/network/OAuthTokens.java new file mode 100644 index 0000000..9f45e7f --- /dev/null +++ b/main/src/cgeo/geocaching/network/OAuthTokens.java @@ -0,0 +1,38 @@ +package cgeo.geocaching.network; + +import cgeo.geocaching.connector.oc.OCApiConnector; +import cgeo.geocaching.settings.Settings; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.eclipse.jdt.annotation.NonNull; + +import android.util.Pair; + +public class OAuthTokens extends Pair<String, String> { + + public OAuthTokens(@NonNull final OCApiConnector connector) { + this(Settings.getTokenPair(connector.getTokenPublicPrefKeyId(), connector.getTokenSecretPrefKeyId())); + } + + public OAuthTokens(final ImmutablePair<String, String> tokenPair) { + this(tokenPair.left, tokenPair.right); + } + + public OAuthTokens(final String pub, final String secret) { + super(pub, secret); + } + + public boolean isValid() { + return StringUtils.isNotBlank(getTokenPublic()) && StringUtils.isNotBlank(getTokenSecret()); + } + + public String getTokenPublic() { + return first; + } + + public String getTokenSecret() { + return second; + } + +} diff --git a/main/src/cgeo/geocaching/network/Parameters.java b/main/src/cgeo/geocaching/network/Parameters.java index 9cb0da5..d6285da 100644 --- a/main/src/cgeo/geocaching/network/Parameters.java +++ b/main/src/cgeo/geocaching/network/Parameters.java @@ -3,6 +3,7 @@ package cgeo.geocaching.network; import ch.boye.httpclientandroidlib.NameValuePair; import ch.boye.httpclientandroidlib.client.utils.URLEncodedUtils; import ch.boye.httpclientandroidlib.message.BasicNameValuePair; + import org.apache.commons.lang3.CharEncoding; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; diff --git a/main/src/cgeo/geocaching/network/Send2CgeoDownloader.java b/main/src/cgeo/geocaching/network/Send2CgeoDownloader.java new file mode 100644 index 0000000..5f5be56 --- /dev/null +++ b/main/src/cgeo/geocaching/network/Send2CgeoDownloader.java @@ -0,0 +1,70 @@ +package cgeo.geocaching.network; + +import cgeo.geocaching.Geocache; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.CancellableHandler; +import cgeo.geocaching.utils.RxUtils; + +import ch.boye.httpclientandroidlib.HttpResponse; + +import org.apache.commons.lang3.StringUtils; + +import rx.Scheduler.Worker; +import rx.functions.Action0; + +import java.util.concurrent.TimeUnit; + +public class Send2CgeoDownloader { + + private Send2CgeoDownloader() { + // Do not instantiate + } + + /** + * Asynchronously load caches from the send2cgeo server. + * + * @param handler the handler to which progress information will be sent + * @param listId the list into which caches will be stored + */ + public static void loadFromWeb(final CancellableHandler handler, final int listId) { + final Worker worker = RxUtils.networkScheduler.createWorker(); + handler.unsubscribeIfCancelled(worker); + worker.schedule(new Action0() { + private final Parameters PARAMS = new Parameters("code", StringUtils.defaultString(Settings.getWebDeviceCode())); + private long baseTime = System.currentTimeMillis(); + + @Override + public void call() { + if (System.currentTimeMillis() - baseTime >= 3 * 60000) { // maximum: 3 minutes + handler.sendEmptyMessage(DownloadProgress.MSG_DONE); + return; + } + + // Download new code + final HttpResponse responseFromWeb = Network.getRequest("http://send2.cgeo.org/read.html", PARAMS); + + if (responseFromWeb != null && responseFromWeb.getStatusLine().getStatusCode() == 200) { + final String response = Network.getResponseData(responseFromWeb); + if (response != null && response.length() > 2) { + handler.sendMessage(handler.obtainMessage(DownloadProgress.MSG_LOADING, response)); + Geocache.storeCache(null, response, listId, false, null); + handler.sendMessage(handler.obtainMessage(DownloadProgress.MSG_LOADED, response)); + baseTime = System.currentTimeMillis(); + worker.schedule(this); + } else if ("RG".equals(response)) { + //Server returned RG (registration) and this device no longer registered. + Settings.setWebNameCode(null, null); + handler.sendEmptyMessage(DownloadProgress.MSG_NO_REGISTRATION); + handler.cancel(); + } else { + worker.schedule(this, 5, TimeUnit.SECONDS); + handler.sendEmptyMessage(DownloadProgress.MSG_WAITING); + } + } else { + handler.sendEmptyMessage(DownloadProgress.MSG_SERVER_FAIL); + handler.cancel(); + } + } + }); + } +} diff --git a/main/src/cgeo/geocaching/network/SmileyImage.java b/main/src/cgeo/geocaching/network/SmileyImage.java new file mode 100644 index 0000000..9bb811e --- /dev/null +++ b/main/src/cgeo/geocaching/network/SmileyImage.java @@ -0,0 +1,40 @@ +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 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) { + super(geocode, false, StoredList.STANDARD_LIST_ID, false, view); + } + + @Override + protected ImmutablePair<BitmapDrawable, Boolean> scaleImage(final ImmutablePair<Bitmap, Boolean> loadResult) { + final Bitmap bitmap = loadResult.left; + if (bitmap == null) { + return ImmutablePair.of((BitmapDrawable) null, loadResult.right); + } + final BitmapDrawable drawable = new BitmapDrawable(view.getResources(), bitmap); + drawable.setBounds(ImageUtils.scaleImageToLineHeight(drawable, view)); + return ImmutablePair.of(drawable, loadResult.right); + } + + @Override + protected BitmapDrawable getContainerDrawable(final Observable<BitmapDrawable> drawable) { + return new LineHeightContainerDrawable(view, drawable); + } + +} diff --git a/main/src/cgeo/geocaching/network/StatusUpdater.java b/main/src/cgeo/geocaching/network/StatusUpdater.java index 82650d1..bc4a5db 100644 --- a/main/src/cgeo/geocaching/network/StatusUpdater.java +++ b/main/src/cgeo/geocaching/network/StatusUpdater.java @@ -4,8 +4,7 @@ import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.utils.RxUtils; import cgeo.geocaching.utils.Version; -import org.json.JSONException; -import org.json.JSONObject; +import com.fasterxml.jackson.databind.node.ObjectNode; import rx.functions.Action0; import rx.subjects.BehaviorSubject; @@ -31,11 +30,11 @@ public class StatusUpdater { this.url = url; } - Status(final JSONObject response) { - message = get(response, "message"); - messageId = get(response, "message_id"); - icon = get(response, "icon"); - url = get(response, "url"); + Status(final ObjectNode response) { + message = response.path("message").asText(null); + messageId = response.path("message_id").asText(null); + icon = response.path("icon").asText(null); + url = response.path("url").asText(null); } final static public Status closeoutStatus = @@ -55,7 +54,7 @@ public class StatusUpdater { RxUtils.networkScheduler.createWorker().schedulePeriodically(new Action0() { @Override public void call() { - final JSONObject response = + final ObjectNode response = Network.requestJSON("http://status.cgeo.org/api/status.json", new Parameters("version_code", String.valueOf(Version.getVersionCode(CgeoApplication.getInstance())), "version_name", Version.getVersionName(CgeoApplication.getInstance()), @@ -67,12 +66,4 @@ public class StatusUpdater { }, 0, 1800, TimeUnit.SECONDS); } - private static String get(final JSONObject json, final String key) { - try { - return json.getString(key); - } catch (final JSONException e) { - return null; - } - } - } diff --git a/main/src/cgeo/geocaching/playservices/LocationProvider.java b/main/src/cgeo/geocaching/playservices/LocationProvider.java new file mode 100644 index 0000000..027ae29 --- /dev/null +++ b/main/src/cgeo/geocaching/playservices/LocationProvider.java @@ -0,0 +1,160 @@ +package cgeo.geocaching.playservices; + +import cgeo.geocaching.sensors.GeoData; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.observers.Subscribers; +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 implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { + + 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 final ReplaySubject<GeoData> subject = ReplaySubject.createWithSize(1); + private final GoogleApiClient 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"); + LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, LOCATION_REQUEST, this, RxUtils.looperCallbacksLooper); + } else if (lowPowerCount.get() > 0) { + Log.d("LocationProvider: requesting low-power locations"); + LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, LOCATION_REQUEST_LOW_POWER, this, RxUtils.looperCallbacksLooper); + } else { + Log.d("LocationProvider: stopping location requests"); + LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this); + } + } + } + + private static Observable<GeoData> get(final Context context, final AtomicInteger reference) { + final LocationProvider instance = getInstance(context); + return Observable.create(new OnSubscribe<GeoData>() { + @Override + public void call(final Subscriber<? super GeoData> 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(Subscribers.from(subscriber))); + } + }); + } + + public static Observable<GeoData> getMostPrecise(final Context context) { + return get(context, mostPreciseCount).onBackpressureDrop(); + } + + public static Observable<GeoData> getLowPower(final Context context) { + // Low-power location without the last stored location + final Observable<GeoData> lowPowerObservable = get(context, lowPowerCount).skip(1); + + // High-power location without the last stored location + final Observable<GeoData> highPowerObservable = get(context, mostPreciseCount).skip(1); + + // Use either low-power (with a 6 seconds head start) or high-power observables to obtain a location + // no less precise than 20 meters. + final Observable<GeoData> untilPreciseEnoughObservable = + lowPowerObservable.mergeWith(highPowerObservable.delaySubscription(6, TimeUnit.SECONDS)) + .takeUntil(new Func1<GeoData, Boolean>() { + @Override + public Boolean call(final GeoData geoData) { + return geoData.getAccuracy() <= 20; + } + }); + + // After sending the last known location, try to get a precise location then use the low-power mode. If no + // location information is given for 25 seconds (if the network location is turned off for example), get + // back to the precise location and try again. + return subject.first().concatWith(untilPreciseEnoughObservable.concatWith(lowPowerObservable).timeout(25, TimeUnit.SECONDS).retry()).onBackpressureDrop(); + } + + /** + * Build a new geo data provider object. + * <p/> + * There is no need to instantiate more than one such object in an application, as observers can be added + * at will. + * + * @param context the context used to retrieve the system services + */ + private LocationProvider(final Context context) { + final GeoData initialLocation = GeoData.getInitialLocation(context); + subject.onNext(initialLocation != null ? initialLocation : GeoData.DUMMY_LOCATION); + locationClient = new GoogleApiClient.Builder(context) + .addApi(LocationServices.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + locationClient.connect(); + } + + @Override + public void onConnected(final Bundle bundle) { + updateRequest(); + } + + @Override + public void onConnectionFailed(final ConnectionResult connectionResult) { + Log.e("cannot connect to Google Play location service: " + connectionResult); + subject.onError(new RuntimeException("Connection failed: " + connectionResult)); + } + + @Override + public void onLocationChanged(final Location location) { + if (Settings.useLowPowerMode()) { + location.setProvider(GeoData.LOW_POWER_PROVIDER); + } + subject.onNext(new GeoData(location)); + } + + @Override + public void onConnectionSuspended(final int arg0) { + // empty + } +} diff --git a/main/src/cgeo/geocaching/sensors/DirectionProvider.java b/main/src/cgeo/geocaching/sensors/DirectionProvider.java deleted file mode 100644 index ed5d76a..0000000 --- a/main/src/cgeo/geocaching/sensors/DirectionProvider.java +++ /dev/null @@ -1,146 +0,0 @@ -package cgeo.geocaching.sensors; - -import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.utils.AngleUtils; -import cgeo.geocaching.utils.StartableHandlerThread; - -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.subjects.BehaviorSubject; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Handler; -import android.os.Process; -import android.view.Surface; -import android.view.WindowManager; - -public class DirectionProvider { - - private static final BehaviorSubject<Float> SUBJECT = BehaviorSubject.create(0.0f); - - private static final WindowManager WINDOW_MANAGER = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); - - private DirectionProvider() { - // utility class - } - - static class Listener implements SensorEventListener, StartableHandlerThread.Callback { - - private int count = 0; - - private SensorManager sensorManager; - - @Override - public void onSensorChanged(final SensorEvent event) { - SUBJECT.onNext(event.values[0]); - } - - @Override - public void onAccuracyChanged(final Sensor sensor, final int accuracy) { - /* - * There is a bug in Android, which apparently causes this method to be called every - * time the sensor _value_ changed, even if the _accuracy_ did not change. Do not have any code in here. - * - * See for example https://code.google.com/p/android/issues/detail?id=14792 - */ - } - - @Override - public void start(final Context context, final Handler handler) { - if (!hasSensor(context)) { - return; - } - if (++count == 1) { - Sensor orientationSensor = getOrientationSensor(context); - sensorManager.registerListener(this, orientationSensor, SensorManager.SENSOR_DELAY_NORMAL, handler); - } - } - - @Override - public void stop() { - if (!hasSensor) { - return; - } - if (--count == 0) { - sensorManager.unregisterListener(this); - } - } - - /** - * Assume that there is an orientation sensor, unless we have really checked that - */ - private boolean hasSensor = true; - - /** - * Flag for one time check if there is a sensor. - */ - private boolean hasSensorChecked = false; - - public boolean hasSensor(Context context) { - if (!hasSensorChecked) { - hasSensor = getOrientationSensor(context) != null; - hasSensorChecked = true; - } - return hasSensor; - } - - // This will be removed when using a new location service. Until then, it is okay to be used. - @SuppressWarnings("deprecation") - private Sensor getOrientationSensor(final Context context) { - sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - return sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); - } - - } - - private static final StartableHandlerThread HANDLER_THREAD = - new StartableHandlerThread("DirectionProvider thread", Process.THREAD_PRIORITY_BACKGROUND, new Listener()); - - static { - HANDLER_THREAD.start(); - } - - public static Observable<Float> create(final Context context) { - return Observable.create(new OnSubscribe<Float>() { - @Override - public void call(final Subscriber<? super Float> subscriber) { - HANDLER_THREAD.start(subscriber, context); - SUBJECT.subscribe(subscriber); - } - }); - } - - /** - * 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; - } - } - -} diff --git a/main/src/cgeo/geocaching/sensors/GeoData.java b/main/src/cgeo/geocaching/sensors/GeoData.java index c0b3974..b8b16fd 100644 --- a/main/src/cgeo/geocaching/sensors/GeoData.java +++ b/main/src/cgeo/geocaching/sensors/GeoData.java @@ -1,67 +1,109 @@ package cgeo.geocaching.sensors; -import cgeo.geocaching.enumerations.LocationProviderType; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.Log; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import android.content.Context; import android.location.Location; import android.location.LocationManager; -class GeoData extends Location implements IGeoData { - private final boolean gpsEnabled; - private final int satellitesVisible; - private final int satellitesFixed; - private final boolean pseudoLocation; +public class GeoData extends Location { - GeoData(final Location location, final boolean gpsEnabled, final int satellitesVisible, final int satellitesFixed, final boolean pseudoLocation) { - super(location); - this.gpsEnabled = gpsEnabled; - this.satellitesVisible = satellitesVisible; - this.satellitesFixed = satellitesFixed; - this.pseudoLocation = pseudoLocation; + public static final String INITIAL_PROVIDER = "initial"; + public static final String HOME_PROVIDER = "home"; + 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); } - @Override - public Location getLocation() { - return this; + public GeoData(final Location location) { + super(location); } - private static LocationProviderType getLocationProviderType(final String provider) { - if (provider.equals(LocationManager.GPS_PROVIDER)) { - return LocationProviderType.GPS; + @Nullable + static Location best(@Nullable final Location gpsLocation, @Nullable final Location netLocation) { + if (netLocation == null || (gpsLocation != null && System.currentTimeMillis() <= gpsLocation.getTime() + 30000)) { + return gpsLocation; } - if (provider.equals(LocationManager.NETWORK_PROVIDER)) { - return LocationProviderType.NETWORK; + if (gpsLocation == null) { + return netLocation; } - return LocationProviderType.LAST; + return gpsLocation.getTime() >= netLocation.getTime() ? gpsLocation : netLocation; } - @Override public LocationProviderType getLocationProvider() { - return getLocationProviderType(getProvider()); + switch (getProvider()) { + case LocationManager.GPS_PROVIDER: + return LocationProviderType.GPS; + case LocationManager.NETWORK_PROVIDER: + return LocationProviderType.NETWORK; + case FUSED_PROVIDER: + // LocationManager.FUSED_PROVIDER constant is not available at API level 9 + return LocationProviderType.FUSED; + case LOW_POWER_PROVIDER: + return LocationProviderType.LOW_POWER; + case HOME_PROVIDER: + return LocationProviderType.HOME; + default: + return LocationProviderType.LAST; + } } - @Override + @NonNull public Geopoint getCoords() { return new Geopoint(this); } - @Override - public boolean getGpsEnabled() { - return gpsEnabled; - } - - @Override - public int getSatellitesVisible() { - return satellitesVisible; - } - - @Override - public int getSatellitesFixed() { - return satellitesFixed; + @Nullable + public static GeoData getInitialLocation(final Context context) { + final LocationManager geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (geoManager != 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); + } + } 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); + } + } else { + Log.w("No LocationManager available"); + } + final String homeLocationStr = Settings.getHomeLocation(); + if (StringUtils.isNotBlank(homeLocationStr)) { + try { + assert homeLocationStr != null; + final Geopoint homeLocation = new Geopoint(homeLocationStr); + Log.i("No last known location available, using home location"); + final Location initialLocation = new Location(HOME_PROVIDER); + initialLocation.setLatitude(homeLocation.getLatitude()); + initialLocation.setLongitude(homeLocation.getLongitude()); + return new GeoData(initialLocation); + } catch (final Geopoint.ParseException e) { + Log.w("Unable to parse home location " + homeLocationStr, e); + } + } + Log.i("No last known location nor home location available"); + return null; } - @Override - public boolean isPseudoLocation() { - return pseudoLocation; - } -} +}
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/sensors/GeoDataProvider.java b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java index a4799cb..dab05d0 100644 --- a/main/src/cgeo/geocaching/sensors/GeoDataProvider.java +++ b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java @@ -1,25 +1,13 @@ package cgeo.geocaching.sensors; import cgeo.geocaching.utils.Log; -import cgeo.geocaching.utils.StartableHandlerThread; +import cgeo.geocaching.utils.RxUtils.LooperCallbacks; import org.apache.commons.lang3.StringUtils; import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.observables.ConnectableObservable; -import rx.subjects.BehaviorSubject; -import rx.subscriptions.CompositeSubscription; -import rx.subscriptions.Subscriptions; import android.content.Context; -import android.location.GpsSatellite; -import android.location.GpsStatus; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; @@ -27,40 +15,13 @@ import android.os.Bundle; import java.util.concurrent.TimeUnit; -public class GeoDataProvider implements OnSubscribe<IGeoData> { +public class GeoDataProvider extends LooperCallbacks<GeoData> { - private static final String LAST_LOCATION_PSEUDO_PROVIDER = "last"; + private final Context context; private final LocationManager geoManager; - private final LocationData gpsLocation = new LocationData(); - private final LocationData netLocation = new LocationData(); - private final BehaviorSubject<IGeoData> subject; - private static final StartableHandlerThread handlerThread = - new StartableHandlerThread("GeoDataProvider thread", android.os.Process.THREAD_PRIORITY_BACKGROUND); - static { - handlerThread.start(); - } - - public boolean gpsEnabled = false; - public int satellitesVisible = 0; - public int satellitesFixed = 0; - - 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. @@ -71,117 +32,51 @@ public class GeoDataProvider implements OnSubscribe<IGeoData> { * @param context the context used to retrieve the system services */ protected GeoDataProvider(final Context context) { - geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - subject = BehaviorSubject.create(findInitialLocation()); + super(2500, TimeUnit.MILLISECONDS); + this.context = context.getApplicationContext(); + geoManager = (LocationManager) this.context.getSystemService(Context.LOCATION_SERVICE); } - public static Observable<IGeoData> create(final Context context) { - final GeoDataProvider provider = new GeoDataProvider(context); - return provider.worker.refCount(); + public static Observable<GeoData> create(final Context context) { + return Observable.create(new GeoDataProvider(context)).onBackpressureDrop(); } @Override - public void call(final Subscriber<? super IGeoData> subscriber) { - subject.subscribe(subscriber); - } - - final ConnectableObservable<IGeoData> worker = new ConnectableObservable<IGeoData>(this) { - private int debugSessionCounter = 0; - - private final Object lock = new Object(); - private int count = 0; - - final private GpsStatus.Listener gpsStatusListener = new GpsStatusListener(); - final private Listener networkListener = new Listener(LocationManager.NETWORK_PROVIDER, netLocation); - final private Listener gpsListener = new Listener(LocationManager.GPS_PROVIDER, gpsLocation); - - @Override - public void connect(Action1<? super Subscription> connection) { - final CompositeSubscription subscription = new CompositeSubscription(); - AndroidSchedulers.handlerThread(handlerThread.getHandler()).createWorker().schedule(new Action0() { - @Override - public void call() { - synchronized(lock) { - if (count++ == 0) { - Log.d("GeoDataProvider: starting the GPS and network listeners" + " (" + ++debugSessionCounter + ")"); - geoManager.addGpsStatusListener(gpsStatusListener); - for (final Listener listener : new Listener[] { networkListener, gpsListener }) { - try { - geoManager.requestLocationUpdates(listener.locationProvider, 0, 0, listener); - } catch (final Exception e) { - Log.w("There is no location provider " + listener.locationProvider); - } - } - } - } - - subscription.add(Subscriptions.create(new Action0() { - @Override - public void call() { - AndroidSchedulers.handlerThread(handlerThread.getHandler()).createWorker().schedule(new Action0() { - @Override - public void call() { - synchronized (lock) { - if (--count == 0) { - Log.d("GeoDataProvider: stopping the GPS and network listeners" + " (" + debugSessionCounter + ")"); - geoManager.removeUpdates(networkListener); - geoManager.removeUpdates(gpsListener); - geoManager.removeGpsStatusListener(gpsStatusListener); - } - } - } - }, 2500, TimeUnit.MILLISECONDS); - } - })); - } - }); - connection.call(subscription); + public void onStart() { + final GeoData initialLocation = GeoData.getInitialLocation(context); + if (initialLocation != null) { + subject.onNext(initialLocation); } - }; - - private IGeoData findInitialLocation() { - final Location initialLocation = new Location(LAST_LOCATION_PSEUDO_PROVIDER); + Log.d("GeoDataProvider: starting the GPS and network listeners"); 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"); - } + 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) { - // 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); + Log.w("Unable to create network location provider: " + e.getMessage()); } - // Start with an historical GeoData just in case someone queries it before we get - // a chance to get any information. - return new GeoData(initialLocation, false, 0, 0, true); } - private static void copyCoords(final Location target, final Location source) { - target.setLatitude(source.getLatitude()); - target.setLongitude(source.getLongitude()); + @Override + protected void onStop() { + Log.d("GeoDataProvider: stopping the GPS and network listeners"); + geoManager.removeUpdates(networkListener); + geoManager.removeUpdates(gpsListener); } 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 @@ -198,85 +93,11 @@ public class GeoDataProvider implements OnSubscribe<IGeoData> { public void onProviderEnabled(final String provider) { // nothing } - - @Override - public void onLocationChanged(final Location location) { - locationData.update(location); - selectBest(); - } - } - - private final class GpsStatusListener implements GpsStatus.Listener { - - @Override - public void onGpsStatusChanged(final int event) { - boolean changed = false; - switch (event) { - case GpsStatus.GPS_EVENT_FIRST_FIX: - case GpsStatus.GPS_EVENT_SATELLITE_STATUS: { - final GpsStatus status = geoManager.getGpsStatus(null); - int visible = 0; - int fixed = 0; - for (final GpsSatellite satellite : status.getSatellites()) { - if (satellite.usedInFix()) { - fixed++; - } - visible++; - } - if (visible != satellitesVisible || fixed != satellitesFixed) { - satellitesVisible = visible; - satellitesFixed = fixed; - changed = true; - } - break; - } - case GpsStatus.GPS_EVENT_STARTED: - if (!gpsEnabled) { - gpsEnabled = true; - changed = true; - } - break; - case GpsStatus.GPS_EVENT_STOPPED: - if (gpsEnabled) { - gpsEnabled = false; - satellitesFixed = 0; - satellitesVisible = 0; - changed = true; - } - break; - default: - throw new IllegalStateException(); - } - - if (changed) { - 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 int visible = gpsLocation.isRecent() ? satellitesVisible : 0; - final boolean pseudoLocation = StringUtils.equals(locationData.location.getProvider(), LAST_LOCATION_PSEUDO_PROVIDER); - final IGeoData current = new GeoData(locationData.location, gpsEnabled, visible, satellitesFixed, pseudoLocation); + final GeoData current = new GeoData(location); subject.onNext(current); } diff --git a/main/src/cgeo/geocaching/sensors/GeoDirHandler.java b/main/src/cgeo/geocaching/sensors/GeoDirHandler.java index 0f30142..4743140 100644 --- a/main/src/cgeo/geocaching/sensors/GeoDirHandler.java +++ b/main/src/cgeo/geocaching/sensors/GeoDirHandler.java @@ -1,27 +1,25 @@ package cgeo.geocaching.sensors; -import cgeo.geocaching.CgeoApplication; -import cgeo.geocaching.settings.Settings; - import org.apache.commons.lang3.tuple.ImmutablePair; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; -import rx.functions.Func1; import rx.functions.Func2; import rx.subscriptions.CompositeSubscription; +import java.util.concurrent.TimeUnit; + /** * GeoData and Direction handler. * <p> - * To use this class, override {@link #updateGeoDir(IGeoData, float)}. You need to start the handler using + * To use this class, override {@link #updateGeoDir(cgeo.geocaching.sensors.GeoData, float)}. You need to start the handler using * {@link #start(int)}. A good place to do so might be the {@code onResume} method of the Activity. Stop the Handler * 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 { @@ -29,8 +27,7 @@ public abstract class GeoDirHandler { public static final int UPDATE_GEODATA = 1 << 1; public static final int UPDATE_DIRECTION = 1 << 2; public static final int UPDATE_GEODIR = 1 << 3; - - private static final CgeoApplication app = CgeoApplication.getInstance(); + public static final int LOW_POWER = 1 << 4; /** * Update method called when new geodata is available. This method is called on the UI thread. @@ -38,7 +35,7 @@ public abstract class GeoDirHandler { * * @param geoData the new geographical data */ - public void updateGeoData(final IGeoData geoData) { + public void updateGeoData(final GeoData geoData) { } /** @@ -60,41 +57,46 @@ public abstract class GeoDirHandler { * If the device goes fast enough, or if the compass use is not enabled in the settings, * the GPS direction information will be used instead of the compass one. */ - public void updateGeoDir(final IGeoData geoData, final float direction) { + public void updateGeoDir(final GeoData geoData, final float direction) { } - private static Observable<Float> fixedDirection() { - return app.directionObservable().map(new Func1<Float, Float>() { - @Override - public Float call(final Float direction) { - final IGeoData geoData = app.currentGeo(); - return fixDirection(geoData, direction); - } - }); - + private static <T> Observable<T> throttleIfNeeded(final Observable<T> observable, final long windowDuration, final TimeUnit unit) { + return windowDuration > 0 ? observable.throttleFirst(windowDuration, unit) : observable; } - 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; + /** + * Register the current GeoDirHandler for GeoData and direction information (if the preferences allow it). + * + * @param flags a combination of UPDATE_GEODATA, UPDATE_DIRECTION, UPDATE_GEODIR, and LOW_POWER + * @return a subscription which can be used to stop the handler + */ + public Subscription start(final int flags) { + return start(flags, 0, TimeUnit.SECONDS); } /** - * Register the current GeoDirHandler for GeoData and direction information (if the - * preferences allow it). + * Register the current GeoDirHandler for GeoData and direction information (if the preferences allow it). + * + * @param flags a combination of UPDATE_GEODATA, UPDATE_DIRECTION, UPDATE_GEODIR, and LOW_POWER + * @param windowDuration if greater than 0, the size of the window duration during which no new value will be presented + * @param unit the unit for the windowDuration + * @return a subscription which can be used to stop the handler */ - public Subscription start(final int flags) { + public Subscription start(final int flags, final long windowDuration, final TimeUnit unit) { final CompositeSubscription subscriptions = new CompositeSubscription(); + final boolean lowPower = (flags & LOW_POWER) != 0; + final Sensors sensors = Sensors.getInstance(); + if ((flags & UPDATE_GEODATA) != 0) { - subscriptions.add(app.geoDataObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<IGeoData>() { + subscriptions.add(throttleIfNeeded(sensors.geoDataObservable(lowPower), windowDuration, unit).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<GeoData>() { @Override - public void call(final IGeoData geoData) { + public void call(final GeoData geoData) { updateGeoData(geoData); } })); } if ((flags & UPDATE_DIRECTION) != 0) { - subscriptions.add(fixedDirection().observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Float>() { + subscriptions.add(throttleIfNeeded(sensors.directionObservable(), windowDuration, unit).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Float>() { @Override public void call(final Float direction) { updateDirection(direction); @@ -102,14 +104,15 @@ public abstract class GeoDirHandler { })); } if ((flags & UPDATE_GEODIR) != 0) { - subscriptions.add(Observable.combineLatest(app.geoDataObservable(), app.directionObservable(), new Func2<IGeoData, Float, ImmutablePair<IGeoData, Float>>() { + // combineOnLatest() does not implement backpressure handling, so we need to explicitely use a backpressure operator there. + subscriptions.add(throttleIfNeeded(Observable.combineLatest(sensors.geoDataObservable(lowPower), sensors.directionObservable(), new Func2<GeoData, Float, ImmutablePair<GeoData, Float>>() { @Override - public ImmutablePair<IGeoData, Float> call(final IGeoData geoData, final Float direction) { - return ImmutablePair.of(geoData, fixDirection(geoData, direction)); + public ImmutablePair<GeoData, Float> call(final GeoData geoData, final Float direction) { + return ImmutablePair.of(geoData, direction); } - }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<ImmutablePair<IGeoData, Float>>() { + }), windowDuration, unit).onBackpressureDrop().observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<ImmutablePair<GeoData, Float>>() { @Override - public void call(final ImmutablePair<IGeoData, Float> geoDir) { + public void call(final ImmutablePair<GeoData, Float> geoDir) { updateGeoDir(geoDir.left, geoDir.right); } })); diff --git a/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java b/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java new file mode 100644 index 0000000..d7aa113 --- /dev/null +++ b/main/src/cgeo/geocaching/sensors/GpsStatusProvider.java @@ -0,0 +1,99 @@ +package cgeo.geocaching.sensors; + +import cgeo.geocaching.sensors.GpsStatusProvider.Status; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils.LooperCallbacks; + +import rx.Observable; + +import android.content.Context; +import android.location.GpsSatellite; +import android.location.GpsStatus; +import android.location.LocationManager; + +public class GpsStatusProvider extends LooperCallbacks<Status> { + + public static class Status { + final public boolean gpsEnabled; + final public int satellitesVisible; + final public int satellitesFixed; + + public Status(final boolean gpsEnabled, final int satellitesVisible, final int satellitesFixed) { + this.gpsEnabled = gpsEnabled; + this.satellitesVisible = satellitesVisible; + this.satellitesFixed = satellitesFixed; + } + } + + private final LocationManager geoManager; + 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/> + * There is no need to instantiate more than one such object in an application, as observers can be added + * at will. + * + * @param context the context used to retrieve the system services + */ + protected GpsStatusProvider(final Context context) { + geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + public static Observable<Status> create(final Context context) { + return Observable.create(new GpsStatusProvider(context)); + } + + @Override + protected void onStart() { + Log.d("GpsStatusProvider: starting the GPS status listener"); + subject.onNext(NO_GPS); + geoManager.addGpsStatusListener(gpsStatusListener); + } + + @Override + protected void onStop() { + Log.d("GpsStatusProvider: stopping the GPS status listener"); + geoManager.removeGpsStatusListener(gpsStatusListener); + } + + private final class GpsStatusListener implements GpsStatus.Listener { + + @Override + public void onGpsStatusChanged(final int event) { + switch (event) { + case GpsStatus.GPS_EVENT_FIRST_FIX: + case GpsStatus.GPS_EVENT_SATELLITE_STATUS: { + final GpsStatus status = geoManager.getGpsStatus(null); + int visible = 0; + int fixed = 0; + for (final GpsSatellite satellite : status.getSatellites()) { + if (satellite.usedInFix()) { + fixed++; + } + visible++; + } + if (visible == latest.satellitesVisible && fixed == latest.satellitesFixed) { + return; + } + latest = new Status(true, visible, fixed); + break; + } + case GpsStatus.GPS_EVENT_STARTED: + latest = new Status(true, 0, 0); + break; + case GpsStatus.GPS_EVENT_STOPPED: + latest = new Status(false, 0, 0); + break; + default: + throw new IllegalStateException(); + } + + subject.onNext(latest); + } + } + +} diff --git a/main/src/cgeo/geocaching/sensors/IGeoData.java b/main/src/cgeo/geocaching/sensors/IGeoData.java deleted file mode 100644 index 5b4f046..0000000 --- a/main/src/cgeo/geocaching/sensors/IGeoData.java +++ /dev/null @@ -1,22 +0,0 @@ -package cgeo.geocaching.sensors; - -import cgeo.geocaching.enumerations.LocationProviderType; -import cgeo.geocaching.geopoint.Geopoint; - -import android.location.Location; - -public interface IGeoData { - - public Location getLocation(); - public LocationProviderType getLocationProvider(); - - public boolean isPseudoLocation(); - - public Geopoint getCoords(); - public float getBearing(); - public float getSpeed(); - public float getAccuracy(); - public boolean getGpsEnabled(); - public int getSatellitesVisible(); - public int getSatellitesFixed(); -} diff --git a/main/src/cgeo/geocaching/enumerations/LocationProviderType.java b/main/src/cgeo/geocaching/sensors/LocationProviderType.java index f2c79fe..16a50b8 100644 --- a/main/src/cgeo/geocaching/enumerations/LocationProviderType.java +++ b/main/src/cgeo/geocaching/sensors/LocationProviderType.java @@ -1,10 +1,13 @@ -package cgeo.geocaching.enumerations; +package cgeo.geocaching.sensors; import cgeo.geocaching.R; public enum LocationProviderType { GPS(R.string.loc_gps), NETWORK(R.string.loc_net), + FUSED(R.string.loc_fused), + LOW_POWER(R.string.loc_low_power), + HOME(R.string.loc_home), LAST(R.string.loc_last); public final int resourceId; diff --git a/main/src/cgeo/geocaching/sensors/OrientationProvider.java b/main/src/cgeo/geocaching/sensors/OrientationProvider.java new file mode 100644 index 0000000..ce84ffb --- /dev/null +++ b/main/src/cgeo/geocaching/sensors/OrientationProvider.java @@ -0,0 +1,72 @@ +package cgeo.geocaching.sensors; + +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils.LooperCallbacks; + +import rx.Observable; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +public class OrientationProvider extends LooperCallbacks<Float> implements SensorEventListener { + + private final SensorManager sensorManager; + private final Sensor orientationSensor; + + @SuppressWarnings("deprecation") + 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"); + } + } + + @SuppressWarnings("deprecation") + public static boolean hasOrientationSensor(final Context context) { + return ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)).getDefaultSensor(Sensor.TYPE_ORIENTATION) != null; + } + + @Override + public void onSensorChanged(final SensorEvent event) { + subject.onNext(event.values[0]); + } + + @Override + public void onAccuracyChanged(final Sensor sensor, final int accuracy) { + /* + * There is a bug in Android, which apparently causes this method to be called every + * time the sensor _value_ changed, even if the _accuracy_ did not change. Do not have any code in here. + * + * See for example https://code.google.com/p/android/issues/detail?id=14792 + */ + } + + @Override + public void onStart() { + if (orientationSensor != null) { + Log.d("OrientationProvider: starting the orientation provider"); + sensorManager.registerListener(this, orientationSensor, SensorManager.SENSOR_DELAY_NORMAL); + } else { + subject.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 OrientationProvider(context)).onBackpressureDrop(); + } + +} diff --git a/main/src/cgeo/geocaching/sensors/RotationProvider.java b/main/src/cgeo/geocaching/sensors/RotationProvider.java new file mode 100644 index 0000000..c63e39f --- /dev/null +++ b/main/src/cgeo/geocaching/sensors/RotationProvider.java @@ -0,0 +1,97 @@ +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"); + } + } + } + + public static boolean hasRotationSensor(final Context context) { + return ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)).getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) != null; + } + + @TargetApi(19) + public static boolean hasGeomagneticRotationSensor(final Context context) { + return ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)).getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR) != null; + } + + @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); + subject.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"); + try { + sensorManager.registerListener(this, rotationSensor, SensorManager.SENSOR_DELAY_NORMAL); + } catch (final Exception e) { + Log.w("RotationProvider: unable to register listener", e); + subject.onError(e); + } + } else { + subject.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)).onBackpressureDrop(); + } + +} diff --git a/main/src/cgeo/geocaching/sensors/Sensors.java b/main/src/cgeo/geocaching/sensors/Sensors.java new file mode 100644 index 0000000..c3f6ce7 --- /dev/null +++ b/main/src/cgeo/geocaching/sensors/Sensors.java @@ -0,0 +1,165 @@ +package cgeo.geocaching.sensors; + +import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.playservices.LocationProvider; +import cgeo.geocaching.sensors.GpsStatusProvider.Status; +import cgeo.geocaching.settings.Settings; +import cgeo.geocaching.utils.AngleUtils; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; + +import org.eclipse.jdt.annotation.NonNull; + +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func1; + +import android.content.Context; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class Sensors { + + private Observable<GeoData> geoDataObservable; + private Observable<GeoData> geoDataObservableLowPower; + private Observable<Float> directionObservable; + private Observable<Status> gpsStatusObservable; + @NonNull private volatile GeoData currentGeo = GeoData.DUMMY_LOCATION; + private volatile float currentDirection = 0.0f; + private volatile boolean hasValidLocation = false; + private final boolean hasCompassCapabilities; + private final CgeoApplication app = CgeoApplication.getInstance(); + + private static class InstanceHolder { + static final Sensors INSTANCE = new Sensors(); + } + + private final Action1<GeoData> rememberGeodataAction = new Action1<GeoData>() { + @Override + public void call(final GeoData geoData) { + currentGeo = geoData; + hasValidLocation = true; + } + }; + + private final Action1<Float> onNextrememberDirectionAction = new Action1<Float>() { + @Override + public void call(final Float direction) { + currentDirection = direction; + } + }; + + private Sensors() { + gpsStatusObservable = GpsStatusProvider.create(app).replay(1).refCount(); + final Context context = CgeoApplication.getInstance().getApplicationContext(); + hasCompassCapabilities = RotationProvider.hasRotationSensor(context) || OrientationProvider.hasOrientationSensor(context); + } + + public static final Sensors getInstance() { + return InstanceHolder.INSTANCE; + } + + private final Func1<Throwable, Observable<GeoData>> fallbackToGeodataProvider = new Func1<Throwable, Observable<GeoData>>() { + @Override + public Observable<GeoData> call(final Throwable throwable) { + Log.e("Cannot use Play Services location provider, falling back to GeoDataProvider", throwable); + Settings.setUseGooglePlayServices(false); + return GeoDataProvider.create(app); + } + }; + + public void setupGeoDataObservables(final boolean useGooglePlayServices, final boolean useLowPowerLocation) { + if (useGooglePlayServices) { + geoDataObservable = LocationProvider.getMostPrecise(app).onErrorResumeNext(fallbackToGeodataProvider).doOnNext(rememberGeodataAction); + if (useLowPowerLocation) { + geoDataObservableLowPower = LocationProvider.getLowPower(app).doOnNext(rememberGeodataAction).onErrorResumeNext(geoDataObservable); + } else { + geoDataObservableLowPower = geoDataObservable; + } + } else { + geoDataObservable = RxUtils.rememberLast(GeoDataProvider.create(app).doOnNext(rememberGeodataAction), null); + geoDataObservableLowPower = geoDataObservable; + } + } + + private static final Func1<GeoData, Float> GPS_TO_DIRECTION = new Func1<GeoData, Float>() { + @Override + public Float call(final GeoData geoData) { + return AngleUtils.reverseDirectionNow(geoData.getBearing()); + } + }; + + public void setupDirectionObservable(final boolean useLowPower) { + // If we have no magnetic sensor, there is no point in trying to setup any, we will always get the direction from the GPS. + if (!hasCompassCapabilities) { + Log.i("No compass capabilities, using only the GPS for the orientation"); + directionObservable = RxUtils.rememberLast(geoDataObservableLowPower.map(GPS_TO_DIRECTION).doOnNext(onNextrememberDirectionAction), 0f); + return; + } + + // Combine the magnetic direction observable with the GPS when compass is disabled or speed is high enough. + final AtomicBoolean useDirectionFromGps = new AtomicBoolean(false); + + // The rotation sensor seems to be bogus on some devices. We should start with the orientation one, except when we explicitely + // want to use the low-power geomagnetic rotation sensor or when we do not have an orientation sensor. + final boolean useRotationSensor = (useLowPower && RotationProvider.hasGeomagneticRotationSensor(app)) || !OrientationProvider.hasOrientationSensor(app); + final Observable<Float> sensorDirectionObservable = useRotationSensor ? RotationProvider.create(app, useLowPower) : OrientationProvider.create(app); + final Observable<Float> magneticDirectionObservable = sensorDirectionObservable.onErrorResumeNext(new Func1<Throwable, Observable<? extends Float>>() { + @Override + public Observable<? extends Float> call(final Throwable throwable) { + Log.e("Device orientation is not available due to sensors error, disabling compass", throwable); + Settings.setUseCompass(false); + return Observable.<Float>never().startWith(0.0f); + } + }).filter(new Func1<Float, Boolean>() { + @Override + public Boolean call(final Float aFloat) { + return Settings.isUseCompass() && !useDirectionFromGps.get(); + } + }); + + final Observable<Float> directionFromGpsObservable = geoDataObservableLowPower.filter(new Func1<GeoData, Boolean>() { + @Override + public Boolean call(final GeoData geoData) { + final boolean useGps = geoData.getSpeed() > 5.0f; + useDirectionFromGps.set(useGps); + return useGps || !Settings.isUseCompass(); + } + }).map(GPS_TO_DIRECTION); + + directionObservable = RxUtils.rememberLast(Observable.merge(magneticDirectionObservable, directionFromGpsObservable).doOnNext(onNextrememberDirectionAction), 0f); + } + + public Observable<GeoData> geoDataObservable(final boolean lowPower) { + return lowPower ? geoDataObservableLowPower : geoDataObservable; + } + + public Observable<Float> directionObservable() { + return directionObservable; + } + + public Observable<Status> gpsStatusObservable() { + if (gpsStatusObservable == null) { + gpsStatusObservable = GpsStatusProvider.create(app).share(); + } + return gpsStatusObservable; + } + + @NonNull + public GeoData currentGeo() { + return currentGeo; + } + + public boolean hasValidLocation() { + return hasValidLocation; + } + + public float currentDirection() { + return currentDirection; + } + + public boolean hasCompassCapabilities() { + return hasCompassCapabilities; + } + +} diff --git a/main/src/cgeo/geocaching/settings/AbstractAttributeBasedPrefence.java b/main/src/cgeo/geocaching/settings/AbstractAttributeBasedPrefence.java index 1930c17..aeeef9f 100644 --- a/main/src/cgeo/geocaching/settings/AbstractAttributeBasedPrefence.java +++ b/main/src/cgeo/geocaching/settings/AbstractAttributeBasedPrefence.java @@ -13,25 +13,25 @@ import android.util.AttributeSet; */ public abstract class AbstractAttributeBasedPrefence extends Preference { - public AbstractAttributeBasedPrefence(Context context) { + public AbstractAttributeBasedPrefence(final Context context) { super(context); } - public AbstractAttributeBasedPrefence(Context context, AttributeSet attrs) { + public AbstractAttributeBasedPrefence(final Context context, final AttributeSet attrs) { super(context, attrs); processAttributes(context, attrs, 0); } - public AbstractAttributeBasedPrefence(Context context, AttributeSet attrs, int defStyle) { + public AbstractAttributeBasedPrefence(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); processAttributes(context, attrs, defStyle); } - private void processAttributes(Context context, @Nullable AttributeSet attrs, int defStyle) { + private void processAttributes(final Context context, @Nullable final AttributeSet attrs, final int defStyle) { if (attrs == null) { return; } - TypedArray types = context.obtainStyledAttributes(attrs, getAttributeNames(), + final TypedArray types = context.obtainStyledAttributes(attrs, getAttributeNames(), defStyle, 0); processAttributeValues(types); @@ -42,7 +42,6 @@ public abstract class AbstractAttributeBasedPrefence extends Preference { /** * Evaluate the attributes which where requested in {@link AbstractAttributeBasedPrefence#getAttributeNames()}. * - * @param values */ protected abstract void processAttributeValues(TypedArray values); diff --git a/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java b/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java index 2f83028..7978d76 100644 --- a/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java +++ b/main/src/cgeo/geocaching/settings/AbstractCheckCredentialsPreference.java @@ -11,10 +11,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import rx.Observable; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action1; import rx.functions.Func0; -import rx.util.async.Async; import android.app.ProgressDialog; import android.content.Context; @@ -25,11 +24,11 @@ import android.util.AttributeSet; public abstract class AbstractCheckCredentialsPreference extends AbstractClickablePreference { - public AbstractCheckCredentialsPreference(Context context, AttributeSet attrs) { + public AbstractCheckCredentialsPreference(final Context context, final AttributeSet attrs) { super(context, attrs); } - public AbstractCheckCredentialsPreference(Context context, AttributeSet attrs, int defStyle) { + public AbstractCheckCredentialsPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @@ -57,7 +56,7 @@ public abstract class AbstractCheckCredentialsPreference extends AbstractClickab } @Override - public boolean onPreferenceClick(Preference preference) { + public boolean onPreferenceClick(final Preference preference) { final Resources res = settingsActivity.getResources(); final ImmutablePair<String, String> credentials = getCredentials(); @@ -74,10 +73,10 @@ public abstract class AbstractCheckCredentialsPreference extends AbstractClickab loginDialog.setCancelable(false); Cookies.clearCookies(); - AndroidObservable.bindActivity(settingsActivity, Async.start(new Func0<ImmutablePair<StatusCode, Observable<Drawable>>>() { + AppObservable.bindActivity(settingsActivity, Observable.defer(new Func0<Observable<ImmutablePair<StatusCode, Observable<Drawable>>>>() { @Override - public ImmutablePair<StatusCode, Observable<Drawable>> call() { - return login(); + public Observable<ImmutablePair<StatusCode, Observable<Drawable>>> call() { + return Observable.just(login()); } })).subscribeOn(RxUtils.networkScheduler).subscribe(new Action1<ImmutablePair<StatusCode, Observable<Drawable>>>() { @Override diff --git a/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java b/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java index f4080cd..57fdf36 100644 --- a/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java +++ b/main/src/cgeo/geocaching/settings/AbstractClickablePreference.java @@ -10,18 +10,18 @@ abstract class AbstractClickablePreference extends Preference { final SettingsActivity activity; - public AbstractClickablePreference(Context context, AttributeSet attrs) { + public AbstractClickablePreference(final Context context, final AttributeSet attrs) { super(context, attrs); activity = (SettingsActivity) context; } - public AbstractClickablePreference(Context context, AttributeSet attrs, int defStyle) { + public AbstractClickablePreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); activity = (SettingsActivity) context; } @Override - protected View onCreateView(ViewGroup parent) { + protected View onCreateView(final ViewGroup parent) { setOnPreferenceClickListener(getOnPreferenceClickListener(activity)); return super.onCreateView(parent); } diff --git a/main/src/cgeo/geocaching/settings/CapabilitiesPreference.java b/main/src/cgeo/geocaching/settings/CapabilitiesPreference.java index 7091f01..afb0b7f 100644 --- a/main/src/cgeo/geocaching/settings/CapabilitiesPreference.java +++ b/main/src/cgeo/geocaching/settings/CapabilitiesPreference.java @@ -23,20 +23,20 @@ public class CapabilitiesPreference extends AbstractAttributeBasedPrefence { private String connectorCode; - public CapabilitiesPreference(Context context) { + public CapabilitiesPreference(final Context context) { super(context); } - public CapabilitiesPreference(Context context, AttributeSet attrs) { + public CapabilitiesPreference(final Context context, final AttributeSet attrs) { super(context, attrs); } - public CapabilitiesPreference(Context context, AttributeSet attrs, int defStyle) { + public CapabilitiesPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @Override - public View getView(View convertView, ViewGroup parent) { + public View getView(final View convertView, final ViewGroup parent) { setOnPreferenceClickListener(new ClickListener()); return super.getView(convertView, parent); } @@ -44,15 +44,15 @@ public class CapabilitiesPreference extends AbstractAttributeBasedPrefence { private final class ClickListener implements OnPreferenceClickListener { @Override public boolean onPreferenceClick(final Preference preference) { - WebView htmlView = new WebView(preference.getContext()); + final WebView htmlView = new WebView(preference.getContext()); htmlView.loadDataWithBaseURL(null, createCapabilitiesMessage(), "text/html", "utf-8", null); - AlertDialog.Builder builder = new AlertDialog.Builder(preference.getContext()); + final AlertDialog.Builder builder = new AlertDialog.Builder(preference.getContext()); builder.setView(htmlView) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(R.string.settings_features) .setPositiveButton(R.string.err_none, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int id) { + public void onClick(final DialogInterface dialog, final int id) { dialog.cancel(); } }); @@ -63,11 +63,11 @@ public class CapabilitiesPreference extends AbstractAttributeBasedPrefence { public String createCapabilitiesMessage() { // TODO: this needs a better key for the connectors - IConnector connector = ConnectorFactory.getConnector(connectorCode + "1234"); - StringBuilder builder = new StringBuilder("<p>" + final IConnector connector = ConnectorFactory.getConnector(connectorCode + "1234"); + final StringBuilder builder = new StringBuilder("<p>" + TextUtils.htmlEncode(CgeoApplication.getInstance().getString(R.string.feature_description)) + "</p><ul>"); - for (String capability : connector.getCapabilities()) { + for (final String capability : connector.getCapabilities()) { builder.append("<li>").append(TextUtils.htmlEncode(capability)).append("</li>"); } @@ -76,7 +76,7 @@ public class CapabilitiesPreference extends AbstractAttributeBasedPrefence { } @Override - protected void processAttributeValues(TypedArray values) { + protected void processAttributeValues(final TypedArray values) { connectorCode = values.getString(0); } diff --git a/main/src/cgeo/geocaching/settings/CheckBoxWithPopupPreference.java b/main/src/cgeo/geocaching/settings/CheckBoxWithPopupPreference.java index 35df787..00b46cc 100644 --- a/main/src/cgeo/geocaching/settings/CheckBoxWithPopupPreference.java +++ b/main/src/cgeo/geocaching/settings/CheckBoxWithPopupPreference.java @@ -20,26 +20,26 @@ public class CheckBoxWithPopupPreference extends CheckBoxPreference { private String urlButton; private OnPreferenceChangeListener baseOnPrefChangeListener = null; - public CheckBoxWithPopupPreference(Context context) { + public CheckBoxWithPopupPreference(final Context context) { super(context); } - public CheckBoxWithPopupPreference(Context context, AttributeSet attrs) { + public CheckBoxWithPopupPreference(final Context context, final AttributeSet attrs) { super(context, attrs); processAttributes(context, attrs, 0); } - public CheckBoxWithPopupPreference(Context context, AttributeSet attrs, int defStyle) { + public CheckBoxWithPopupPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); processAttributes(context, attrs, defStyle); } - private void processAttributes(Context context, AttributeSet attrs, int defStyle) { + private void processAttributes(final Context context, final AttributeSet attrs, final int defStyle) { if (attrs == null) { return; // coward's retreat } - TypedArray types = context.obtainStyledAttributes(attrs, new int[] { + final TypedArray types = context.obtainStyledAttributes(attrs, new int[] { R.attr.title, R.attr.text, R.attr.url, R.attr.urlButton }, defStyle, 0); @@ -52,7 +52,7 @@ public class CheckBoxWithPopupPreference extends CheckBoxPreference { } @Override - protected View onCreateView(ViewGroup parent) { + protected View onCreateView(final ViewGroup parent) { if (baseOnPrefChangeListener == null) { baseOnPrefChangeListener = getOnPreferenceChangeListener(); @@ -61,7 +61,7 @@ public class CheckBoxWithPopupPreference extends CheckBoxPreference { // show dialog when checkbox enabled setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @Override - public boolean onPreferenceChange(final Preference preference, Object newValue) { + public boolean onPreferenceChange(final Preference preference, final Object newValue) { if (baseOnPrefChangeListener != null) { baseOnPrefChangeListener.onPreferenceChange(preference, newValue); } diff --git a/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java b/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java index f5d9ab5..8f7da2c 100644 --- a/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java +++ b/main/src/cgeo/geocaching/settings/CheckECCredentialsPreference.java @@ -14,11 +14,11 @@ import android.util.AttributeSet; public class CheckECCredentialsPreference extends AbstractCheckCredentialsPreference { - public CheckECCredentialsPreference(Context context, AttributeSet attrs) { + public CheckECCredentialsPreference(final Context context, final AttributeSet attrs) { super(context, attrs); } - public CheckECCredentialsPreference(Context context, AttributeSet attrs, int defStyle) { + public CheckECCredentialsPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } diff --git a/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java b/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java index 0269f3b..04f406f 100644 --- a/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java +++ b/main/src/cgeo/geocaching/settings/CheckGcCredentialsPreference.java @@ -13,11 +13,11 @@ import android.util.AttributeSet; public class CheckGcCredentialsPreference extends AbstractCheckCredentialsPreference { - public CheckGcCredentialsPreference(Context context, AttributeSet attrs) { + public CheckGcCredentialsPreference(final Context context, final AttributeSet attrs) { super(context, attrs); } - public CheckGcCredentialsPreference(Context context, AttributeSet attrs, int defStyle) { + public CheckGcCredentialsPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @@ -31,8 +31,7 @@ public class CheckGcCredentialsPreference extends AbstractCheckCredentialsPrefer final StatusCode loginResult = GCLogin.getInstance().login(); switch (loginResult) { case NO_ERROR: - GCLogin.detectGcCustomDate(); - return ImmutablePair.of(StatusCode.NO_ERROR, GCLogin.getInstance().downloadAvatarAndGetMemberStatus()); + return ImmutablePair.of(StatusCode.NO_ERROR, GCLogin.getInstance().downloadAvatar()); default: return ImmutablePair.of(loginResult, null); } diff --git a/main/src/cgeo/geocaching/settings/EditPasswordPreference.java b/main/src/cgeo/geocaching/settings/EditPasswordPreference.java index af07041..267a0b3 100644 --- a/main/src/cgeo/geocaching/settings/EditPasswordPreference.java +++ b/main/src/cgeo/geocaching/settings/EditPasswordPreference.java @@ -14,20 +14,20 @@ import android.util.AttributeSet; */ public class EditPasswordPreference extends EditTextPreference { - public EditPasswordPreference(Context context) { + public EditPasswordPreference(final Context context) { super(context); } - public EditPasswordPreference(Context context, AttributeSet attrs) { + public EditPasswordPreference(final Context context, final AttributeSet attrs) { super(context, attrs); } - public EditPasswordPreference(Context context, AttributeSet attrs, int defStyle) { + public EditPasswordPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @Override - public void setSummary(CharSequence summary) { + public void setSummary(final CharSequence summary) { if (StringUtils.isBlank(summary)) { super.setSummary(StringUtils.EMPTY); } else { diff --git a/main/src/cgeo/geocaching/settings/InfoPreference.java b/main/src/cgeo/geocaching/settings/InfoPreference.java index 8040a62..d1ece92 100644 --- a/main/src/cgeo/geocaching/settings/InfoPreference.java +++ b/main/src/cgeo/geocaching/settings/InfoPreference.java @@ -39,22 +39,22 @@ public class InfoPreference extends AbstractAttributeBasedPrefence { private LayoutInflater inflater; - public InfoPreference(Context context) { + public InfoPreference(final Context context) { super(context); init(context); } - public InfoPreference(Context context, AttributeSet attrs) { + public InfoPreference(final Context context, final AttributeSet attrs) { super(context, attrs); init(context); } - public InfoPreference(Context context, AttributeSet attrs, int defStyle) { + public InfoPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); init(context); } - private void init(Context context) { + private void init(final Context context) { inflater = ((Activity) context).getLayoutInflater(); setPersistent(false); } @@ -65,14 +65,14 @@ public class InfoPreference extends AbstractAttributeBasedPrefence { } @Override - protected void processAttributeValues(TypedArray values) { + protected void processAttributeValues(final TypedArray values) { text = values.getString(0); url = values.getString(1); urlButton = values.getString(2); } @Override - protected View onCreateView(ViewGroup parent) { + protected View onCreateView(final ViewGroup parent) { // show popup when clicked setOnPreferenceClickListener(new OnPreferenceClickListener() { @@ -91,14 +91,12 @@ public class InfoPreference extends AbstractAttributeBasedPrefence { /** * Add an info icon at the left hand side of the preference. * - * @param parent - * @return */ - private View addInfoIcon(ViewGroup parent) { - View preferenceView = super.onCreateView(parent); + private View addInfoIcon(final ViewGroup parent) { + final View preferenceView = super.onCreateView(parent); - ImageView iconView = (ImageView) inflater.inflate(R.layout.preference_info_icon, parent, false); - LinearLayout frame = (LinearLayout) preferenceView.findViewById(android.R.id.widget_frame); + final ImageView iconView = (ImageView) inflater.inflate(R.layout.preference_info_icon, parent, false); + final LinearLayout frame = (LinearLayout) preferenceView.findViewById(android.R.id.widget_frame); frame.setVisibility(View.VISIBLE); frame.addView(iconView); diff --git a/main/src/cgeo/geocaching/settings/OAuthPreference.java b/main/src/cgeo/geocaching/settings/OAuthPreference.java index 54bad6d..23f3a12 100644 --- a/main/src/cgeo/geocaching/settings/OAuthPreference.java +++ b/main/src/cgeo/geocaching/settings/OAuthPreference.java @@ -2,9 +2,9 @@ package cgeo.geocaching.settings; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; +import cgeo.geocaching.activity.OAuthAuthorizationActivity.OAuthParameters; import cgeo.geocaching.connector.oc.OCAuthParams; import cgeo.geocaching.connector.oc.OCAuthorizationActivity; -import cgeo.geocaching.network.OAuthAuthorizationActivity.OAuthParameters; import cgeo.geocaching.twitter.TwitterAuthorizationActivity; import android.content.Context; diff --git a/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java b/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java index 84c343a..da20039 100644 --- a/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java +++ b/main/src/cgeo/geocaching/settings/RegisterSend2CgeoPreference.java @@ -13,7 +13,7 @@ import ch.boye.httpclientandroidlib.HttpResponse; import org.apache.commons.lang3.StringUtils; import rx.Observable; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action1; import rx.functions.Func0; @@ -24,11 +24,11 @@ import android.util.AttributeSet; public class RegisterSend2CgeoPreference extends AbstractClickablePreference { - public RegisterSend2CgeoPreference(Context context, AttributeSet attrs) { + public RegisterSend2CgeoPreference(final Context context, final AttributeSet attrs) { super(context, attrs); } - public RegisterSend2CgeoPreference(Context context, AttributeSet attrs, int defStyle) { + public RegisterSend2CgeoPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @@ -36,7 +36,7 @@ public class RegisterSend2CgeoPreference extends AbstractClickablePreference { protected OnPreferenceClickListener getOnPreferenceClickListener(final SettingsActivity activity) { return new OnPreferenceClickListener() { @Override - public boolean onPreferenceClick(Preference preference) { + public boolean onPreferenceClick(final Preference preference) { // satisfy static code analysis if (activity == null) { return true; @@ -55,23 +55,25 @@ public class RegisterSend2CgeoPreference extends AbstractClickablePreference { activity.getString(R.string.init_sendToCgeo_registering), true); progressDialog.setCancelable(false); - AndroidObservable.bindActivity(activity, Observable.defer(new Func0<Observable<Integer>>() { + AppObservable.bindActivity(activity, Observable.defer(new Func0<Observable<Integer>>() { @Override public Observable<Integer> call() { final String nam = StringUtils.defaultString(deviceName); final String cod = StringUtils.defaultString(deviceCode); final Parameters params = new Parameters("name", nam, "code", cod); - HttpResponse response = Network.getRequest("http://send2.cgeo.org/auth.html", params); + final HttpResponse response = Network.getRequest("http://send2.cgeo.org/auth.html", params); if (response != null && response.getStatusLine().getStatusCode() == 200) { //response was OK final String[] strings = StringUtils.split(Network.getResponseData(response), ','); - Settings.setWebNameCode(nam, strings[0]); - try { - return Observable.from(Integer.parseInt(strings[1].trim())); - } catch (final Exception e) { - Log.e("RegisterSend2CgeoPreference", e); + if (strings != null) { + Settings.setWebNameCode(nam, strings[0]); + try { + 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 d93f83f..c15bc1b 100644 --- a/main/src/cgeo/geocaching/settings/Settings.java +++ b/main/src/cgeo/geocaching/settings/Settings.java @@ -6,12 +6,11 @@ import cgeo.geocaching.apps.cache.navi.NavigationAppFactory.NavigationAppsEnum; import cgeo.geocaching.connector.capability.ICredentials; import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.connector.gc.GCConstants; -import cgeo.geocaching.connector.gc.GCLogin; import cgeo.geocaching.enumerations.CacheType; -import cgeo.geocaching.enumerations.LiveMapStrategy.Strategy; import cgeo.geocaching.enumerations.LogType; -import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.list.StoredList; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.maps.CGeoMap.MapMode; import cgeo.geocaching.maps.MapProviderFactory; //import cgeo.geocaching.maps.google.v1.GoogleMapProvider; import cgeo.geocaching.maps.interfaces.GeoPointImpl; @@ -35,6 +34,8 @@ import android.content.SharedPreferences.Editor; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Environment; import android.preference.PreferenceManager; @@ -49,6 +50,10 @@ import java.util.Locale; */ public class Settings { + /** + * On opening a map, we limit the _initial_ zoom. The user can still zoom out afterwards. + */ + private static final int INITIAL_MAP_ZOOM_LIMIT = 16; private static final char HISTORY_SEPARATOR = ','; public static final int SHOW_WP_THRESHOLD_DEFAULT = 10; public static final int SHOW_WP_THRESHOLD_MAX = 50; @@ -66,11 +71,11 @@ public class Settings { 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 - private final static @NonNull String keyConsumerPublic = CryptUtils.rot13("ESnsCvAv3kEupF1GCR3jGj"); - private final static @NonNull String keyConsumerSecret = CryptUtils.rot13("7vQWceACV9umEjJucmlpFe9FCMZSeqIqfkQ2BnhV9x"); + private final static @NonNull String TWITTER_KEY_CONSUMER_PUBLIC = CryptUtils.rot13("ESnsCvAv3kEupF1GCR3jGj"); + private final static @NonNull String TWITTER_KEY_CONSUMER_SECRET = CryptUtils.rot13("7vQWceACV9umEjJucmlpFe9FCMZSeqIqfkQ2BnhV9x"); + + private static boolean useCompass = true; public enum CoordInputFormatEnum { Plain, @@ -78,7 +83,7 @@ public class Settings { Min, Sec; - static int DEFAULT_INT_VALUE = Min.ordinal(); + static final int DEFAULT_INT_VALUE = Min.ordinal(); public static CoordInputFormatEnum fromInt(final int id) { final CoordInputFormatEnum[] values = CoordInputFormatEnum.values(); @@ -109,13 +114,29 @@ 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.apply(); + return; + } + + if (currentVersion < 1) { + // migrate from non standard file location and integer based boolean types final Editor e = sharedPrefs.edit(); +<<<<<<< HEAD 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); @@ -171,16 +192,72 @@ public class Settings { e.putBoolean(getKey(R.string.pref_debug), old.getBoolean(getKey(R.string.pref_debug), false)); e.putBoolean(getKey(R.string.pref_hidelivemaphint), old.getInt(getKey(R.string.pref_hidelivemaphint), 0) != 0); 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_imperial), prefsV0.getInt(getKey(R.string.pref_units_imperial), 1) != 1); + e.putBoolean(getKey(R.string.pref_skin), prefsV0.getInt(getKey(R.string.pref_skin), 0) != 0); + e.putInt(getKey(R.string.pref_lastusedlist), prefsV0.getInt(getKey(R.string.pref_lastusedlist), StoredList.STANDARD_LIST_ID)); + e.putString(getKey(R.string.pref_cachetype), prefsV0.getString(getKey(R.string.pref_cachetype), CacheType.ALL.id)); + e.putString(getKey(R.string.pref_twitter_token_secret), prefsV0.getString(getKey(R.string.pref_twitter_token_secret), null)); + e.putString(getKey(R.string.pref_twitter_token_public), prefsV0.getString(getKey(R.string.pref_twitter_token_public), null)); + e.putInt(getKey(R.string.pref_version), prefsV0.getInt(getKey(R.string.pref_version), 0)); + e.putBoolean(getKey(R.string.pref_autoloaddesc), 0 != prefsV0.getInt(getKey(R.string.pref_autoloaddesc), 1)); + e.putBoolean(getKey(R.string.pref_ratingwanted), prefsV0.getBoolean(getKey(R.string.pref_ratingwanted), true)); + e.putBoolean(getKey(R.string.pref_friendlogswanted), prefsV0.getBoolean(getKey(R.string.pref_friendlogswanted), true)); + e.putBoolean(getKey(R.string.pref_useenglish), prefsV0.getBoolean(getKey(R.string.pref_useenglish), false)); + e.putBoolean(getKey(R.string.pref_usecompass), 0 != prefsV0.getInt(getKey(R.string.pref_usecompass), 1)); + e.putBoolean(getKey(R.string.pref_trackautovisit), prefsV0.getBoolean(getKey(R.string.pref_trackautovisit), false)); + e.putBoolean(getKey(R.string.pref_sigautoinsert), prefsV0.getBoolean(getKey(R.string.pref_sigautoinsert), false)); + e.putBoolean(getKey(R.string.pref_logimages), prefsV0.getBoolean(getKey(R.string.pref_logimages), false)); + e.putBoolean(getKey(R.string.pref_excludedisabled), 0 != prefsV0.getInt(getKey(R.string.pref_excludedisabled), 0)); + e.putBoolean(getKey(R.string.pref_excludemine), 0 != prefsV0.getInt(getKey(R.string.pref_excludemine), 0)); + e.putString(getKey(R.string.pref_mapfile), prefsV0.getString(getKey(R.string.pref_mapfile), null)); + e.putString(getKey(R.string.pref_signature), prefsV0.getString(getKey(R.string.pref_signature), null)); + e.putString(getKey(R.string.pref_pass_vote), prefsV0.getString(getKey(R.string.pref_pass_vote), null)); + e.putString(getKey(R.string.pref_password), prefsV0.getString(getKey(R.string.pref_password), null)); + e.putString(getKey(R.string.pref_username), prefsV0.getString(getKey(R.string.pref_username), null)); + e.putString(getKey(R.string.pref_memberstatus), prefsV0.getString(getKey(R.string.pref_memberstatus), "")); + e.putInt(getKey(R.string.pref_coordinputformat), prefsV0.getInt(getKey(R.string.pref_coordinputformat), CoordInputFormatEnum.DEFAULT_INT_VALUE)); + e.putBoolean(getKey(R.string.pref_log_offline), prefsV0.getBoolean(getKey(R.string.pref_log_offline), false)); + e.putBoolean(getKey(R.string.pref_choose_list), prefsV0.getBoolean(getKey(R.string.pref_choose_list), true)); + e.putBoolean(getKey(R.string.pref_loaddirectionimg), prefsV0.getBoolean(getKey(R.string.pref_loaddirectionimg), true)); + e.putString(getKey(R.string.pref_gccustomdate), prefsV0.getString(getKey(R.string.pref_gccustomdate), GCConstants.DEFAULT_GC_DATE)); + e.putInt(getKey(R.string.pref_showwaypointsthreshold), prefsV0.getInt(getKey(R.string.pref_showwaypointsthreshold), SHOW_WP_THRESHOLD_DEFAULT)); + e.putString(getKey(R.string.pref_cookiestore), prefsV0.getString(getKey(R.string.pref_cookiestore), null)); + e.putBoolean(getKey(R.string.pref_opendetailslastpage), prefsV0.getBoolean(getKey(R.string.pref_opendetailslastpage), false)); + e.putInt(getKey(R.string.pref_lastdetailspage), prefsV0.getInt(getKey(R.string.pref_lastdetailspage), 1)); + e.putInt(getKey(R.string.pref_defaultNavigationTool), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id)); + e.putInt(getKey(R.string.pref_defaultNavigationTool2), prefsV0.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id)); + e.putInt(getKey(R.string.pref_livemapstrategy), prefsV0.getInt(getKey(R.string.pref_livemapstrategy), LivemapStrategy.AUTO.id)); + e.putBoolean(getKey(R.string.pref_debug), prefsV0.getBoolean(getKey(R.string.pref_debug), false)); + e.putInt(getKey(R.string.pref_livemaphintshowcount), prefsV0.getInt(getKey(R.string.pref_livemaphintshowcount), 0)); +>>>>>>> 59b8b2e26a7fff6072c4d5d96f51035dc900e0bc e.putInt(getKey(R.string.pref_settingsversion), 1); // mark migrated - e.commit(); + e.apply(); } // changes for new settings dialog - if (oldVersion < 2) { + if (currentVersion < 2) { final Editor e = sharedPrefs.edit(); - e.putBoolean(getKey(R.string.pref_units), !isUseImperialUnits()); + e.putBoolean(getKey(R.string.pref_units_imperial), useImperialUnits()); // show waypoints threshold now as a slider int wpThreshold = getWayPointsThreshold(); @@ -209,7 +286,7 @@ public class Settings { e.putString(getKey(R.string.pref_gpxExportDir), getGpxExportDir()); e.putInt(getKey(R.string.pref_settingsversion), 2); // mark migrated - e.commit(); + e.apply(); } } @@ -237,40 +314,40 @@ public class Settings { return sharedPrefs.getFloat(getKey(prefKeyId), defaultValue); } - protected static boolean putString(final int prefKeyId, final String value) { + protected static void putString(final int prefKeyId, final String value) { final SharedPreferences.Editor edit = sharedPrefs.edit(); edit.putString(getKey(prefKeyId), value); - return edit.commit(); + edit.apply(); } - protected static boolean putBoolean(final int prefKeyId, final boolean value) { + protected static void putBoolean(final int prefKeyId, final boolean value) { final SharedPreferences.Editor edit = sharedPrefs.edit(); edit.putBoolean(getKey(prefKeyId), value); - return edit.commit(); + edit.apply(); } - private static boolean putInt(final int prefKeyId, final int value) { + private static void putInt(final int prefKeyId, final int value) { final SharedPreferences.Editor edit = sharedPrefs.edit(); edit.putInt(getKey(prefKeyId), value); - return edit.commit(); + edit.apply(); } - private static boolean putLong(final int prefKeyId, final long value) { + private static void putLong(final int prefKeyId, final long value) { final SharedPreferences.Editor edit = sharedPrefs.edit(); edit.putLong(getKey(prefKeyId), value); - return edit.commit(); + edit.apply(); } - private static boolean putFloat(final int prefKeyId, final float value) { + private static void putFloat(final int prefKeyId, final float value) { final SharedPreferences.Editor edit = sharedPrefs.edit(); edit.putFloat(getKey(prefKeyId), value); - return edit.commit(); + edit.apply(); } - private static boolean remove(final int prefKeyId) { + private static void remove(final int prefKeyId) { final SharedPreferences.Editor edit = sharedPrefs.edit(); edit.remove(getKey(prefKeyId)); - return edit.commit(); + edit.apply(); } private static boolean contains(final int prefKeyId) { @@ -317,7 +394,7 @@ public class Settings { } public static String getUsername() { - return getString(R.string.pref_username, null); + return getString(R.string.pref_username, StringUtils.EMPTY); } public static boolean isGCConnectorActive() { @@ -333,19 +410,20 @@ public class Settings { } public static boolean isGCPremiumMember() { - // Basic Member, Premium Member, ??? - return GCConstants.MEMBER_STATUS_PM.equalsIgnoreCase(Settings.getGCMemberStatus()); + final String memberStatus = Settings.getGCMemberStatus(); + return StringUtils.equalsIgnoreCase(memberStatus, GCConstants.MEMBER_STATUS_PREMIUM) || + StringUtils.equalsIgnoreCase(memberStatus, GCConstants.MEMBER_STATUS_CHARTER); } public static String getGCMemberStatus() { return getString(R.string.pref_memberstatus, ""); } - public static boolean setGCMemberStatus(final String memberStatus) { + public static void setGCMemberStatus(final String memberStatus) { if (StringUtils.isBlank(memberStatus)) { - return remove(R.string.pref_memberstatus); + remove(R.string.pref_memberstatus); } - return putString(R.string.pref_memberstatus, memberStatus); + putString(R.string.pref_memberstatus, memberStatus); } public static ImmutablePair<String, String> getTokenPair(final int tokenPublicPrefKey, final int tokenSecretPrefKey) { @@ -375,10 +453,7 @@ public class Settings { } public static boolean isGCvoteLogin() { - final String preUsername = getString(R.string.pref_username, null); - final String prePassword = getString(R.string.pref_pass_vote, null); - - return !StringUtils.isBlank(preUsername) && !StringUtils.isBlank(prePassword); + return getGCvoteLogin() != null; } public static ImmutablePair<String, String> getGCvoteLogin() { @@ -396,19 +471,33 @@ public class Settings { return getString(R.string.pref_signature, StringUtils.EMPTY); } - public static boolean setCookieStore(final String cookies) { + public static void setCookieStore(final String cookies) { if (StringUtils.isBlank(cookies)) { // erase cookies - return remove(R.string.pref_cookiestore); + remove(R.string.pref_cookiestore); } // save cookies - return putString(R.string.pref_cookiestore, cookies); + putString(R.string.pref_cookiestore, cookies); } public static String getCookieStore() { return getString(R.string.pref_cookiestore, null); } + public static void setUseGooglePlayServices(final boolean value) { + putBoolean(R.string.pref_googleplayservices, value); + } + + public static boolean useGooglePlayServices() { + // By defaut, enable play services starting from ICS. + return CgeoApplication.getInstance().isGooglePlayServicesAvailable() && + getBoolean(R.string.pref_googleplayservices, VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH); + } + + public static boolean useLowPowerMode() { + return getBoolean(R.string.pref_lowpowermode, false); + } + /** * @param cacheType * The cache type used for future filtering @@ -442,12 +531,11 @@ public class Settings { return getString(R.string.pref_mapfile, null); } - public static boolean setMapFile(final String mapFile) { - final boolean result = putString(R.string.pref_mapfile, mapFile); + public static void setMapFile(final String mapFile) { + putString(R.string.pref_mapfile, mapFile); if (mapFile != null) { setMapFileDirectory(new File(mapFile).getParent()); } - return result; } public static String getMapFileDirectory() { @@ -462,10 +550,9 @@ public class Settings { return null; } - public static boolean setMapFileDirectory(final String mapFileDirectory) { - final boolean result = putString(R.string.pref_mapDirectory, mapFileDirectory); + public static void setMapFileDirectory(final String mapFileDirectory) { + putString(R.string.pref_mapDirectory, mapFileDirectory); MapsforgeMapProvider.getInstance().updateOfflineMaps(); - return result; } public static boolean isValidMapFile() { @@ -506,10 +593,11 @@ public class Settings { /** * @return User selected date format on GC.com - * @see GCLogin#GC_CUSTOM_DATE_FORMATS */ public static String getGcCustomDate() { - return getString(R.string.pref_gccustomdate, null); + // We might have some users whose stored value is null, which is invalid. In this case, we use the default. + return StringUtils.defaultString(getString(R.string.pref_gccustomdate, GCConstants.DEFAULT_GC_DATE), + GCConstants.DEFAULT_GC_DATE); } public static boolean isExcludeMyCaches() { @@ -572,11 +660,15 @@ public class Settings { return getBoolean(R.string.pref_sigautoinsert, false); } - public static boolean isUseImperialUnits() { - return getBoolean(R.string.pref_units, getImperialUnitsDefault()); + public static void setUseImperialUnits(final boolean useImperialUnits) { + putBoolean(R.string.pref_units_imperial, useImperialUnits); + } + + public static boolean useImperialUnits() { + return getBoolean(R.string.pref_units_imperial, useImperialUnitsByDefault()); } - static boolean getImperialUnitsDefault() { + static boolean useImperialUnitsByDefault() { final String countryCode = Locale.getDefault().getCountry(); return "US".equals(countryCode) // USA || "LR".equals(countryCode) // Liberia @@ -592,21 +684,55 @@ public class Settings { } public static boolean isMapTrail() { - return getBoolean(R.string.pref_maptrail, true); + return getBoolean(R.string.pref_maptrail, false); } public static void setMapTrail(final boolean showTrail) { putBoolean(R.string.pref_maptrail, showTrail); } - public static int getMapZoom() { - return getInt(R.string.pref_lastmapzoom, 14); + /** + * Get last used zoom of the internal map. Differentiate between two use cases for a map of multiple caches (e.g. + * live map) and the map of a single cache (which is often zoomed in more deep). + */ + public static int getMapZoom(final MapMode mapMode) { + if (mapMode == MapMode.SINGLE || mapMode == MapMode.COORDS) { + return getCacheZoom(); + } + return getMapZoom(); } - public static void setMapZoom(final int mapZoomLevel) { + public static void setMapZoom(final MapMode mapMode, final int zoomLevel) { + if (mapMode == MapMode.SINGLE || mapMode == MapMode.COORDS) { + setCacheZoom(zoomLevel); + } + else { + setMapZoom(zoomLevel); + } + } + + /** + * @return zoom used for the (live) map + */ + private static int getMapZoom() { + return Math.max(getInt(R.string.pref_lastmapzoom, 14), INITIAL_MAP_ZOOM_LIMIT); + } + + private static void setMapZoom(final int mapZoomLevel) { putInt(R.string.pref_lastmapzoom, mapZoomLevel); } + /** + * @return zoom used for the map of a single cache + */ + private static int getCacheZoom() { + return Math.max(getInt(R.string.pref_cache_zoom, 14), INITIAL_MAP_ZOOM_LIMIT); + } + + private static void setCacheZoom(final int zoomLevel) { + putInt(R.string.pref_cache_zoom, zoomLevel); + } + public static GeoPointImpl getMapCenter() { return getMapProvider().getMapItemFactory() .getGeoPointBase(new Geopoint(getInt(R.string.pref_lastmaplat, 0) / 1e6, @@ -627,7 +753,7 @@ public class Settings { // mapSource = MapProviderFactory.getMapSource(id); if (mapSource != null) { // don't use offline maps if the map file is not valid - if ((!(mapSource instanceof OfflineMapSource)) || (isValidMapFile())) { + if (!(mapSource instanceof OfflineMapSource) || isValidMapFile()) { return mapSource; } } @@ -646,9 +772,7 @@ public class Settings { private static final int HISTORY_SIZE = 10; /** - * convert old preference ids for maps (based on constant values) into new hash based ids - * - * @return + * Convert old preference ids for maps (based on constant values) into new hash based ids. */ // private static int getConvertedMapId() { // final int id = Integer.parseInt(getString(R.string.pref_mapsource, @@ -675,7 +799,7 @@ public class Settings { // return id; // } - public static void setMapSource(final MapSource newMapSource) { + public static synchronized void setMapSource(final MapSource newMapSource) { putString(R.string.pref_mapsource, String.valueOf(newMapSource.getNumericalId())); if (newMapSource instanceof OfflineMapSource) { setMapFile(((OfflineMapSource) newMapSource).getFileName()); @@ -704,11 +828,11 @@ public class Settings { } public static boolean isUseCompass() { - return getBoolean(R.string.pref_usecompass, true); + return useCompass; } - public static void setUseCompass(final boolean useCompass) { - putBoolean(R.string.pref_usecompass, useCompass); + public static void setUseCompass(final boolean value) { + useCompass = value; } public static boolean isLightSkin() { @@ -716,13 +840,13 @@ public class Settings { } @NonNull - public static String getKeyConsumerPublic() { - return keyConsumerPublic; + public static String getTwitterKeyConsumerPublic() { + return TWITTER_KEY_CONSUMER_PUBLIC; } @NonNull - public static String getKeyConsumerSecret() { - return keyConsumerSecret; + public static String getTwitterKeyConsumerSecret() { + return TWITTER_KEY_CONSUMER_SECRET; } public static String getWebDeviceCode() { @@ -734,13 +858,14 @@ public class Settings { } public static String getWebDeviceName() { - return getString(R.string.pref_webDeviceName, android.os.Build.MODEL); + return getString(R.string.pref_webDeviceName, Build.MODEL); } /** * @return The cache type used for filtering or ALL if no filter is active. * Returns never null */ + @NonNull public static CacheType getCacheType() { return CacheType.getById(getString(R.string.pref_cachetype, CacheType.ALL.id)); } @@ -838,11 +963,11 @@ public class Settings { String.valueOf(NavigationAppsEnum.INTERNAL_MAP.id))); } - public static Strategy getLiveMapStrategy() { - return Strategy.getById(getInt(R.string.pref_livemapstrategy, Strategy.AUTO.id)); + public static LivemapStrategy getLiveMapStrategy() { + return LivemapStrategy.getById(getInt(R.string.pref_livemapstrategy, LivemapStrategy.AUTO.id)); } - public static void setLiveMapStrategy(final Strategy strategy) { + public static void setLiveMapStrategy(final LivemapStrategy strategy) { putInt(R.string.pref_livemapstrategy, strategy.id); } @@ -850,14 +975,6 @@ public class Settings { return Log.isDebug(); } - public static boolean getHideLiveMapHint() { - return getBoolean(R.string.pref_hidelivemaphint, false); - } - - public static void setHideLiveHint(final boolean hide) { - putBoolean(R.string.pref_hidelivemaphint, hide); - } - public static int getLiveMapHintShowCount() { return getInt(R.string.pref_livemaphintshowcount, 0); } @@ -986,9 +1103,7 @@ public class Settings { } /** - * remember date of last field note export - * - * @param date + * Remember date of last field note export. */ public static void setFieldnoteExportDate(final long date) { putLong(R.string.pref_fieldNoteExportDate, date); @@ -999,9 +1114,7 @@ public class Settings { } /** - * remember the state of the "Upload" checkbox in the field notes export dialog - * - * @param upload + * Remember the state of the "Upload" checkbox in the field notes export dialog. */ public static void setFieldNoteExportUpload(final boolean upload) { putBoolean(R.string.pref_fieldNoteExportUpload, upload); @@ -1012,9 +1125,7 @@ public class Settings { } /** - * remember the state of the "Only new" checkbox in the field notes export dialog - * - * @param onlyNew + * Remember the state of the "Only new" checkbox in the field notes export dialog. */ public static void setFieldNoteExportOnlyNew(final boolean onlyNew) { putBoolean(R.string.pref_fieldNoteExportOnlyNew, onlyNew); @@ -1054,7 +1165,32 @@ public class Settings { 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); + public static void setUseHardwareAcceleration(final boolean useHardwareAcceleration) { + putBoolean(R.string.pref_hardware_acceleration, useHardwareAcceleration); + } + + public static String getLastCacheLog() { + return getString(R.string.pref_last_cache_log, StringUtils.EMPTY); + } + + public static void setLastCacheLog(final String log) { + putString(R.string.pref_last_cache_log, log); + } + + public static String getLastTrackableLog() { + return getString(R.string.pref_last_trackable_log, StringUtils.EMPTY); + } + + public static void setLastTrackableLog(final String log) { + putString(R.string.pref_last_trackable_log, log); + } + + @Nullable + public static String getHomeLocation() { + return getString(R.string.pref_home_location, null); + } + + public static void setHomeLocation(@NonNull final String homeLocation) { + putString(R.string.pref_home_location, homeLocation); } } diff --git a/main/src/cgeo/geocaching/settings/SettingsActivity.java b/main/src/cgeo/geocaching/settings/SettingsActivity.java index df6e680..6e3ba0e 100644 --- a/main/src/cgeo/geocaching/settings/SettingsActivity.java +++ b/main/src/cgeo/geocaching/settings/SettingsActivity.java @@ -1,7 +1,5 @@ package cgeo.geocaching.settings; -import butterknife.ButterKnife; - import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.DataStore; import cgeo.geocaching.Intents; @@ -14,13 +12,19 @@ import cgeo.geocaching.connector.gc.GCConnector; import cgeo.geocaching.files.SimpleDirChooser; import cgeo.geocaching.maps.MapProviderFactory; import cgeo.geocaching.maps.interfaces.MapSource; +import cgeo.geocaching.network.AndroidBeam; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.utils.DatabaseBackupUtils; import cgeo.geocaching.utils.DebugUtils; import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.RxUtils; import org.apache.commons.lang3.StringUtils; import org.openintents.intents.FileManagerIntents; +import rx.functions.Action0; +import rx.schedulers.Schedulers; + import android.app.ProgressDialog; import android.app.backup.BackupManager; import android.content.ActivityNotFoundException; @@ -90,12 +94,14 @@ public class SettingsActivity extends PreferenceActivity { setTheme(Settings.isLightSkin() && Build.VERSION.SDK_INT > 10 ? R.style.settings_light : R.style.settings); super.onCreate(savedInstanceState); - initHardwareAccelerationPreferences(); + initDeviceSpecificPreferences(); + initUnitPreferences(); SettingsActivity.addPreferencesFromResource(this, R.xml.preferences); initPreferences(); final Intent intent = getIntent(); openInitialScreen(intent.getIntExtra(INTENT_OPEN_SCREEN, 0)); + AndroidBeam.disable(this); } private void openInitialScreen(final int initialScreen) { @@ -126,6 +132,7 @@ public class SettingsActivity extends PreferenceActivity { initDefaultNavigationPreferences(); initBackupButtons(); initDbLocationPreference(); + initGeoDirPreferences(); initDebugPreference(); initBasicMemberPreferences(); initSend2CgeoPreferences(); @@ -143,7 +150,6 @@ public class SettingsActivity extends PreferenceActivity { R.string.pref_ecusername, R.string.pref_ecpassword, R.string.pref_ec_icons }) { bindSummaryToStringValue(k); } - getPreference(R.string.pref_units).setDefaultValue(Settings.getImperialUnitsDefault()); } private void initNavigationMenuPreferences() { @@ -278,7 +284,7 @@ public class SettingsActivity extends PreferenceActivity { /** * Fire up a directory chooser on click on the preference. * - * @see #onActivityResult() for processing of the selected directory + * The result can be processed using {@link android.app.Activity#onActivityResult}. * * @param dct * type of directory to be selected @@ -297,7 +303,7 @@ public class SettingsActivity extends PreferenceActivity { dirChooser.putExtra(FileManagerIntents.EXTRA_BUTTON_TEXT, getString(android.R.string.ok)); startActivityForResult(dirChooser, dct.requestCode); - } catch (final android.content.ActivityNotFoundException ex) { + } catch (final ActivityNotFoundException ignored) { // OI file manager not available final Intent dirChooser = new Intent(this, SimpleDirChooser.class); dirChooser.putExtra(Intents.EXTRA_START_DIR, startDirectory); @@ -323,13 +329,14 @@ public class SettingsActivity extends PreferenceActivity { backup.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference preference) { - return DatabaseBackupUtils.createBackup(SettingsActivity.this, new Runnable() { + DatabaseBackupUtils.createBackup(SettingsActivity.this, new Runnable() { @Override public void run() { VALUE_CHANGE_LISTENER.onPreferenceChange(SettingsActivity.this.getPreference(R.string.pref_fakekey_preference_backup_info), ""); } }); + return true; } }); @@ -354,37 +361,41 @@ public class SettingsActivity extends PreferenceActivity { final Resources res = getResources(); final SettingsActivity activity = SettingsActivity.this; final ProgressDialog dialog = ProgressDialog.show(activity, res.getString(R.string.init_maintenance), res.getString(R.string.init_maintenance_directories), true, false); - new Thread() { + RxUtils.andThenOnUi(Schedulers.io(), new Action0() { @Override - public void run() { + public void call() { DataStore.removeObsoleteCacheDirectories(); - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - dialog.dismiss(); - } - }); } - }.start(); - + }, new Action0() { + @Override + public void call() { + dialog.dismiss(); + } + }); return true; - } - }); + } + }); final Preference memoryDumpPref = getPreference(R.string.pref_memory_dump); memoryDumpPref .setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override public boolean onPreferenceClick( - final Preference preference) { - DebugUtils.createMemoryDump(SettingsActivity.this); - return true; - } - }); + @Override + public boolean onPreferenceClick( + final Preference preference) { + DebugUtils.createMemoryDump(SettingsActivity.this); + return true; + } + }); } - public static void initHardwareAccelerationPreferences() { - // We have to ensure that the preference is initialized so that devices with hardware acceleration disabled - // get the appropriate value. + public static void initDeviceSpecificPreferences() { + // We have to ensure that those preferences are initialized so that devices with specific default values + // will get the appropriate ones. Settings.setUseHardwareAcceleration(Settings.useHardwareAcceleration()); + Settings.setUseGooglePlayServices(Settings.useGooglePlayServices()); + } + + private static void initUnitPreferences() { + Settings.setUseImperialUnits(Settings.useImperialUnits()); } private void initDbLocationPreference() { @@ -413,6 +424,28 @@ public class SettingsActivity extends PreferenceActivity { }); } + private void initGeoDirPreferences() { + final Sensors sensors = Sensors.getInstance(); + final Preference playServices = getPreference(R.string.pref_googleplayservices); + playServices.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + sensors.setupGeoDataObservables((Boolean) newValue, Settings.useLowPowerMode()); + return true; + } + }); + playServices.setEnabled(CgeoApplication.getInstance().isGooglePlayServicesAvailable()); + getPreference(R.string.pref_lowpowermode).setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + final Boolean useLowPower = (Boolean) newValue; + sensors.setupGeoDataObservables(Settings.useGooglePlayServices(), useLowPower); + sensors.setupDirectionObservable(useLowPower); + return true; + } + }); + } + void initBasicMemberPreferences() { getPreference(R.string.preference_screen_basicmembers) .setEnabled(!Settings.isGCPremiumMember()); @@ -570,7 +603,7 @@ public class SettingsActivity extends PreferenceActivity { final int mapSourceId = Integer.parseInt(stringValue); mapSource = MapProviderFactory.getMapSource(mapSourceId); } catch (final NumberFormatException e) { - Log.e("SettingsActivity.onPreferenceChange: bad source id '" + stringValue + "'"); + Log.e("SettingsActivity.onPreferenceChange: bad source id '" + stringValue + "'", e); mapSource = null; } // If there is no corresponding map source (because some map sources were @@ -676,8 +709,6 @@ public class SettingsActivity extends PreferenceActivity { /** * auto-care for the summary of the preference of string type with this key - * - * @param key */ private void bindSummaryToStringValue(final int key) { diff --git a/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java b/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java index 968dce5..d7c46e1 100644 --- a/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java +++ b/main/src/cgeo/geocaching/sorting/AbstractCacheComparator.java @@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils; * abstract super implementation for all cache comparators * */ -public abstract class AbstractCacheComparator implements CacheComparator { +abstract class AbstractCacheComparator implements CacheComparator { @Override public final int compare(final Geocache cache1, final Geocache cache2) { @@ -35,9 +35,11 @@ public abstract class AbstractCacheComparator implements CacheComparator { * Check necessary preconditions (like missing fields) before running the comparison itself. * Caches not filling the conditions will be placed last, sorted by Geocode. * - * The default returns <code>true</code> and can be overridden if needed in child classes. - * + * The default implementation returns <code>true</code> and can be overridden if needed in sub classes. + * * @param cache + * the cache to be sorted + * * @return <code>true</code> if the cache holds the necessary data to be compared meaningfully */ @SuppressWarnings("static-method") @@ -51,8 +53,6 @@ public abstract class AbstractCacheComparator implements CacheComparator { * A cache is smaller than another cache if it is desirable to show it first when presented to the user. * For example, a highly rated cache must be considered smaller than a poorly rated one. * - * @param cache1 - * @param cache2 * @return an integer < 0 if cache1 is less than cache2, 0 if they are equal, and > 0 if cache1 is greater than * cache2. */ diff --git a/main/src/cgeo/geocaching/sorting/DateComparator.java b/main/src/cgeo/geocaching/sorting/DateComparator.java index 7913941..347eb44 100644 --- a/main/src/cgeo/geocaching/sorting/DateComparator.java +++ b/main/src/cgeo/geocaching/sorting/DateComparator.java @@ -1,7 +1,7 @@ package cgeo.geocaching.sorting; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; +import cgeo.geocaching.sensors.Sensors; import java.util.ArrayList; import java.util.Date; @@ -9,21 +9,16 @@ import java.util.Date; /** * compares caches by date */ -public class DateComparator extends AbstractCacheComparator { +class DateComparator extends AbstractCacheComparator { @Override - protected int compareCaches(Geocache cache1, Geocache cache2) { + protected int compareCaches(final Geocache cache1, final Geocache cache2) { final Date date1 = cache1.getHiddenDate(); final Date date2 = cache2.getHiddenDate(); if (date1 != null && date2 != null) { final int dateDifference = date1.compareTo(date2); - // for equal dates, sort by distance if (dateDifference == 0) { - final ArrayList<Geocache> list = new ArrayList<>(); - list.add(cache1); - list.add(cache2); - final DistanceComparator distanceComparator = new DistanceComparator(CgeoApplication.getInstance().currentGeo().getCoords(), list); - return distanceComparator.compare(cache1, cache2); + return sortSameDate(cache1, cache2); } return dateDifference; } @@ -35,4 +30,13 @@ public class DateComparator extends AbstractCacheComparator { } return 0; } + + @SuppressWarnings("static-method") + protected int sortSameDate(final Geocache cache1, final Geocache cache2) { + final ArrayList<Geocache> list = new ArrayList<>(); + list.add(cache1); + list.add(cache2); + final DistanceComparator distanceComparator = new DistanceComparator(Sensors.getInstance().currentGeo().getCoords(), list); + return distanceComparator.compare(cache1, cache2); + } } diff --git a/main/src/cgeo/geocaching/sorting/DifficultyComparator.java b/main/src/cgeo/geocaching/sorting/DifficultyComparator.java index 459f38d..10463c8 100644 --- a/main/src/cgeo/geocaching/sorting/DifficultyComparator.java +++ b/main/src/cgeo/geocaching/sorting/DifficultyComparator.java @@ -6,10 +6,10 @@ import cgeo.geocaching.Geocache; * sorts caches by difficulty * */ -public class DifficultyComparator extends AbstractCacheComparator { +class DifficultyComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache) { + protected boolean canCompare(final Geocache cache) { return cache.getDifficulty() != 0.0; } diff --git a/main/src/cgeo/geocaching/sorting/DistanceComparator.java b/main/src/cgeo/geocaching/sorting/DistanceComparator.java index b3b751b..f400583 100644 --- a/main/src/cgeo/geocaching/sorting/DistanceComparator.java +++ b/main/src/cgeo/geocaching/sorting/DistanceComparator.java @@ -1,7 +1,7 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import java.util.ArrayList; import java.util.List; @@ -28,7 +28,8 @@ public class DistanceComparator extends AbstractCacheComparator { 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/sorting/EventDateComparator.java b/main/src/cgeo/geocaching/sorting/EventDateComparator.java index 197946a..efeeb5d 100644 --- a/main/src/cgeo/geocaching/sorting/EventDateComparator.java +++ b/main/src/cgeo/geocaching/sorting/EventDateComparator.java @@ -1,5 +1,7 @@ package cgeo.geocaching.sorting; +import cgeo.geocaching.Geocache; + /** * Compares caches by date. Used only for event caches. */ @@ -7,4 +9,16 @@ public class EventDateComparator extends DateComparator { final static public EventDateComparator singleton = new EventDateComparator(); + @Override + protected int sortSameDate(final Geocache left, final Geocache right) { + return compare(left.guessEventTimeMinutes(), right.guessEventTimeMinutes()); + } + + /** + * copy of {@link Integer#compare(int, int)}, as that is not available on lower API levels + * + */ + private static int compare(final int left, final int right) { + return left < right ? -1 : (left == right ? 0 : 1); + } } diff --git a/main/src/cgeo/geocaching/sorting/FindsComparator.java b/main/src/cgeo/geocaching/sorting/FindsComparator.java index 7f2ef50..a4c0686 100644 --- a/main/src/cgeo/geocaching/sorting/FindsComparator.java +++ b/main/src/cgeo/geocaching/sorting/FindsComparator.java @@ -2,17 +2,17 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; -public class FindsComparator extends AbstractCacheComparator { +class FindsComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache) { + protected boolean canCompare(final Geocache cache) { return cache.getLogCounts() != null; } @Override - protected int compareCaches(Geocache cache1, Geocache cache2) { - int finds1 = cache1.getFindsCount(); - int finds2 = cache2.getFindsCount(); + protected int compareCaches(final Geocache cache1, final Geocache cache2) { + final int finds1 = cache1.getFindsCount(); + final int finds2 = cache2.getFindsCount(); return finds2 - finds1; } diff --git a/main/src/cgeo/geocaching/sorting/GeocodeComparator.java b/main/src/cgeo/geocaching/sorting/GeocodeComparator.java index e700f13..30a2fc8 100644 --- a/main/src/cgeo/geocaching/sorting/GeocodeComparator.java +++ b/main/src/cgeo/geocaching/sorting/GeocodeComparator.java @@ -6,7 +6,7 @@ import cgeo.geocaching.Geocache; * sorts caches by geo code, therefore effectively sorting by cache age * */ -public class GeocodeComparator extends AbstractCacheComparator { +class GeocodeComparator extends AbstractCacheComparator { @Override protected boolean canCompare(final Geocache cache) { diff --git a/main/src/cgeo/geocaching/sorting/InventoryComparator.java b/main/src/cgeo/geocaching/sorting/InventoryComparator.java index 9d19b64..13eadcc 100644 --- a/main/src/cgeo/geocaching/sorting/InventoryComparator.java +++ b/main/src/cgeo/geocaching/sorting/InventoryComparator.java @@ -5,7 +5,7 @@ import cgeo.geocaching.Geocache; /** * sorts caches by number of items in inventory */ -public class InventoryComparator extends AbstractCacheComparator { +class InventoryComparator extends AbstractCacheComparator { @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { diff --git a/main/src/cgeo/geocaching/sorting/InverseComparator.java b/main/src/cgeo/geocaching/sorting/InverseComparator.java index 1dc76e2..0da351f 100644 --- a/main/src/cgeo/geocaching/sorting/InverseComparator.java +++ b/main/src/cgeo/geocaching/sorting/InverseComparator.java @@ -10,12 +10,12 @@ public class InverseComparator implements CacheComparator { private final CacheComparator originalComparator; - public InverseComparator(CacheComparator comparator) { + public InverseComparator(final CacheComparator comparator) { this.originalComparator = comparator; } @Override - public int compare(Geocache lhs, Geocache rhs) { + public int compare(final Geocache lhs, final Geocache rhs) { return originalComparator.compare(rhs, lhs); } diff --git a/main/src/cgeo/geocaching/sorting/NameComparator.java b/main/src/cgeo/geocaching/sorting/NameComparator.java index 2941b1c..ab7bbcb 100644 --- a/main/src/cgeo/geocaching/sorting/NameComparator.java +++ b/main/src/cgeo/geocaching/sorting/NameComparator.java @@ -1,22 +1,27 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; +import cgeo.geocaching.utils.TextUtils; import org.apache.commons.lang3.StringUtils; +import java.text.Collator; + /** * sorts caches by name * */ -public class NameComparator extends AbstractCacheComparator { +class NameComparator extends AbstractCacheComparator { + + private final Collator collator = TextUtils.getCollator(); @Override - protected boolean canCompare(Geocache cache) { + protected boolean canCompare(final Geocache cache) { return StringUtils.isNotBlank(cache.getName()); } @Override - protected int compareCaches(Geocache cache1, Geocache cache2) { - return cache1.getNameForSorting().compareToIgnoreCase(cache2.getNameForSorting()); + protected int compareCaches(final Geocache cache1, final Geocache cache2) { + return collator.compare(cache1.getNameForSorting(), cache2.getNameForSorting()); } } diff --git a/main/src/cgeo/geocaching/sorting/PopularityComparator.java b/main/src/cgeo/geocaching/sorting/PopularityComparator.java index 2dbee68..58b3b4c 100644 --- a/main/src/cgeo/geocaching/sorting/PopularityComparator.java +++ b/main/src/cgeo/geocaching/sorting/PopularityComparator.java @@ -6,7 +6,7 @@ import cgeo.geocaching.Geocache; * sorts caches by popularity (favorite count) * */ -public class PopularityComparator extends AbstractCacheComparator { +class PopularityComparator extends AbstractCacheComparator { @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { diff --git a/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java b/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java index 57a69ee..4c2d914 100644 --- a/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java +++ b/main/src/cgeo/geocaching/sorting/PopularityRatioComparator.java @@ -8,12 +8,12 @@ import cgeo.geocaching.Geocache; /** * sorts caches by popularity ratio (favorites per find in %). */ -public class PopularityRatioComparator extends AbstractCacheComparator { +class PopularityRatioComparator extends AbstractCacheComparator { @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { - int finds1 = cache1.getFindsCount(); - int finds2 = cache2.getFindsCount(); + final int finds1 = cache1.getFindsCount(); + final int finds2 = cache2.getFindsCount(); float ratio1 = 0.0f; if (finds1 != 0) { @@ -26,7 +26,8 @@ public class PopularityRatioComparator extends AbstractCacheComparator { if ((ratio2 - ratio1) > 0.0f) { return 1; - } else if ((ratio2 - ratio1) < 0.0f) { + } + if ((ratio2 - ratio1) < 0.0f) { return -1; } diff --git a/main/src/cgeo/geocaching/sorting/RatingComparator.java b/main/src/cgeo/geocaching/sorting/RatingComparator.java index 6f2c615..62854fe 100644 --- a/main/src/cgeo/geocaching/sorting/RatingComparator.java +++ b/main/src/cgeo/geocaching/sorting/RatingComparator.java @@ -6,7 +6,7 @@ import cgeo.geocaching.Geocache; * sorts caches by gcvote.com rating * */ -public class RatingComparator extends AbstractCacheComparator { +class RatingComparator extends AbstractCacheComparator { @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { diff --git a/main/src/cgeo/geocaching/sorting/SizeComparator.java b/main/src/cgeo/geocaching/sorting/SizeComparator.java index c8de586..8cb5178 100644 --- a/main/src/cgeo/geocaching/sorting/SizeComparator.java +++ b/main/src/cgeo/geocaching/sorting/SizeComparator.java @@ -6,15 +6,10 @@ import cgeo.geocaching.Geocache; * sorts caches by size * */ -public class SizeComparator extends AbstractCacheComparator { +class SizeComparator extends AbstractCacheComparator { @Override - protected boolean canCompare(Geocache cache) { - return cache.getSize() != null; - } - - @Override - protected int compareCaches(Geocache cache1, Geocache cache2) { + protected int compareCaches(final Geocache cache1, final Geocache cache2) { return cache2.getSize().comparable - cache1.getSize().comparable; } }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/sorting/SortActionProvider.java b/main/src/cgeo/geocaching/sorting/SortActionProvider.java index 6251984..e6db330 100644 --- a/main/src/cgeo/geocaching/sorting/SortActionProvider.java +++ b/main/src/cgeo/geocaching/sorting/SortActionProvider.java @@ -136,9 +136,7 @@ public class SortActionProvider extends ActionProvider implements OnMenuItemClic final CacheComparator comparator = cacheComparator.newInstance(); onClickListener.call(comparator); } - } catch (final InstantiationException e) { - Log.e("selectComparator", e); - } catch (final IllegalAccessException e) { + } catch (final InstantiationException | IllegalAccessException e) { Log.e("selectComparator", e); } } diff --git a/main/src/cgeo/geocaching/sorting/StateComparator.java b/main/src/cgeo/geocaching/sorting/StateComparator.java index 9488bd9..e4403f8 100644 --- a/main/src/cgeo/geocaching/sorting/StateComparator.java +++ b/main/src/cgeo/geocaching/sorting/StateComparator.java @@ -6,7 +6,7 @@ import cgeo.geocaching.Geocache; * sort caches by state (normal, disabled, archived) * */ -public class StateComparator extends AbstractCacheComparator { +class StateComparator extends AbstractCacheComparator { @Override protected int compareCaches(final Geocache cache1, final Geocache cache2) { diff --git a/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java b/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java index b718d3b..fbe3226 100644 --- a/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java +++ b/main/src/cgeo/geocaching/sorting/StorageTimeComparator.java @@ -2,10 +2,10 @@ package cgeo.geocaching.sorting; import cgeo.geocaching.Geocache; -public class StorageTimeComparator extends AbstractCacheComparator { +class StorageTimeComparator extends AbstractCacheComparator { @Override - protected int compareCaches(Geocache cache1, Geocache cache2) { + protected int compareCaches(final Geocache cache1, final Geocache cache2) { if (cache1.getUpdated() < cache2.getUpdated()) { return -1; } diff --git a/main/src/cgeo/geocaching/sorting/TerrainComparator.java b/main/src/cgeo/geocaching/sorting/TerrainComparator.java index 9bbb5f7..e882025 100644 --- a/main/src/cgeo/geocaching/sorting/TerrainComparator.java +++ b/main/src/cgeo/geocaching/sorting/TerrainComparator.java @@ -6,7 +6,7 @@ import cgeo.geocaching.Geocache; * sorts caches by terrain rating * */ -public class TerrainComparator extends AbstractCacheComparator { +class TerrainComparator extends AbstractCacheComparator { @Override protected boolean canCompare(final Geocache cache) { diff --git a/main/src/cgeo/geocaching/sorting/VoteComparator.java b/main/src/cgeo/geocaching/sorting/VoteComparator.java index cd4ad7e..5cf39d7 100644 --- a/main/src/cgeo/geocaching/sorting/VoteComparator.java +++ b/main/src/cgeo/geocaching/sorting/VoteComparator.java @@ -5,10 +5,10 @@ import cgeo.geocaching.Geocache; /** * sorts caches by the users own voting (if available at all) */ -public class VoteComparator extends AbstractCacheComparator { +class VoteComparator extends AbstractCacheComparator { @Override - protected int compareCaches(Geocache cache1, Geocache cache2) { + protected int compareCaches(final Geocache cache1, final Geocache cache2) { // if there is no vote available, put that cache at the end of the list return Float.compare(cache2.getMyVote(), cache1.getMyVote()); } diff --git a/main/src/cgeo/geocaching/speech/SpeechService.java b/main/src/cgeo/geocaching/speech/SpeechService.java index fbd2d7e..b22e061 100644 --- a/main/src/cgeo/geocaching/speech/SpeechService.java +++ b/main/src/cgeo/geocaching/speech/SpeechService.java @@ -1,10 +1,11 @@ package cgeo.geocaching.speech; +import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.activity.ActivityMixin; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; -import cgeo.geocaching.sensors.IGeoData; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; @@ -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; /** @@ -47,7 +47,7 @@ public class SpeechService extends Service implements OnInitListener { final GeoDirHandler geoDirHandler = new GeoDirHandler() { @Override - public void updateGeoDir(final IGeoData newGeo, final float newDirection) { + public void updateGeoDir(final GeoData newGeo, final float newDirection) { position = newGeo.getCoords(); direction = newDirection; // avoid any calculation, if the delay since the last output is not long enough @@ -152,11 +152,12 @@ 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 } + @SuppressWarnings("deprecation") private void speak(final String text) { if (!initialized) { return; @@ -168,7 +169,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/speech/TextFactory.java b/main/src/cgeo/geocaching/speech/TextFactory.java index eb780c6..3f1f142 100644 --- a/main/src/cgeo/geocaching/speech/TextFactory.java +++ b/main/src/cgeo/geocaching/speech/TextFactory.java @@ -2,8 +2,8 @@ package cgeo.geocaching.speech; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.IConversion; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.IConversion; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.AngleUtils; @@ -21,17 +21,17 @@ import java.util.Locale; * on unit expressions. */ public class TextFactory { - public static String getText(Geopoint position, Geopoint target, float direction) { + public static String getText(final Geopoint position, final Geopoint target, final float direction) { if (position == null || target == null) { return null; } return getDirection(position, target, direction) + ". " + getDistance(position, target); } - private static String getDistance(Geopoint position, Geopoint target) { + private static String getDistance(final Geopoint position, final Geopoint target) { final float kilometers = position.distanceTo(target); - if (Settings.isUseImperialUnits()) { + if (Settings.useImperialUnits()) { return getDistance(kilometers / IConversion.MILES_TO_KILOMETER, (int) (kilometers * 1000.0 * IConversion.METERS_TO_FEET), 3.0f, 0.2f, 300, @@ -44,9 +44,9 @@ public class TextFactory { R.plurals.tts_meters, R.string.tts_one_meter); } - private static String getDistance(float farDistance, int nearDistance, - float farFarAway, float farNearAway, int nearFarAway, - int farId, int farOneId, int nearId, int nearOneId) { + private static String getDistance(final float farDistance, final int nearDistance, + final float farFarAway, final float farNearAway, final int nearFarAway, + final int farId, final int farOneId, final int nearId, final int nearOneId) { if (farDistance >= farFarAway) { // example: "5 kilometers" - always without decimal digits final int quantity = Math.round(farDistance); @@ -84,15 +84,15 @@ public class TextFactory { return getQuantityString(nearId, quantity, String.valueOf(quantity)); } - private static String getString(int resourceId, Object... formatArgs) { + private static String getString(final int resourceId, final Object... formatArgs) { return CgeoApplication.getInstance().getString(resourceId, formatArgs); } - private static String getQuantityString(int resourceId, int quantity, Object... formatArgs) { + private static String getQuantityString(final int resourceId, final int quantity, final Object... formatArgs) { return CgeoApplication.getInstance().getResources().getQuantityString(resourceId, quantity, formatArgs); } - private static String getDirection(Geopoint position, Geopoint target, float direction) { + private static String getDirection(final Geopoint position, final Geopoint target, final float direction) { final int bearing = (int) position.bearingTo(target); final int degrees = (int) AngleUtils.normalize(bearing - direction); diff --git a/main/src/cgeo/geocaching/twitter/Twitter.java b/main/src/cgeo/geocaching/twitter/Twitter.java index c89c0b6..0e5ebfa 100644 --- a/main/src/cgeo/geocaching/twitter/Twitter.java +++ b/main/src/cgeo/geocaching/twitter/Twitter.java @@ -6,10 +6,11 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.LogEntry; import cgeo.geocaching.Trackable; import cgeo.geocaching.enumerations.LoadFlags; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter.Format; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter.Format; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.OAuth; +import cgeo.geocaching.network.OAuthTokens; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.Log; @@ -17,6 +18,7 @@ import cgeo.geocaching.utils.LogTemplateProvider; import cgeo.geocaching.utils.LogTemplateProvider.LogContext; import ch.boye.httpclientandroidlib.HttpResponse; + import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -25,7 +27,7 @@ public final class Twitter { private static final String HASH_PREFIX_WITH_BLANK = " #"; private static final int MAX_TWEET_SIZE = 140; - public static void postTweetCache(String geocode, final @Nullable LogEntry logEntry) { + public static void postTweetCache(final String geocode, final @Nullable LogEntry logEntry) { final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB); if (cache == null) { return; @@ -33,7 +35,7 @@ public final class Twitter { postTweet(CgeoApplication.getInstance(), getStatusMessage(cache, logEntry), null); } - public static void postTweetTrackable(String geocode, final @Nullable LogEntry logEntry) { + public static void postTweetTrackable(final String geocode, final @Nullable LogEntry logEntry) { final Trackable trackable = DataStore.loadTrackable(geocode); if (trackable == null) { return; @@ -48,7 +50,7 @@ public final class Twitter { try { final String status = shortenToMaxSize(statusIn); - Parameters parameters = new Parameters("status", status); + final Parameters parameters = new Parameters("status", status); if (coords != null) { parameters.put( "lat", coords.format(Format.LAT_DECDEGREE_RAW), @@ -56,7 +58,7 @@ public final class Twitter { "display_coordinates", "true"); } - OAuth.signOAuth("api.twitter.com", "/1.1/statuses/update.json", "POST", true, parameters, Settings.getTokenPublic(), Settings.getTokenSecret(), Settings.getKeyConsumerPublic(), Settings.getKeyConsumerSecret()); + OAuth.signOAuth("api.twitter.com", "/1.1/statuses/update.json", "POST", true, parameters, new OAuthTokens(Settings.getTokenPublic(), Settings.getTokenSecret()), Settings.getTwitterKeyConsumerPublic(), Settings.getTwitterKeyConsumerSecret()); final HttpResponse httpResponse = Network.postRequest("https://api.twitter.com/1.1/statuses/update.json", parameters); if (httpResponse != null) { if (httpResponse.getStatusLine().getStatusCode() == 200) { @@ -67,13 +69,13 @@ public final class Twitter { } else { Log.e("Tweet could not be posted. Reason: httpResponse Object is null"); } - } catch (Exception e) { + } catch (final Exception e) { Log.e("Twitter.postTweet", e); } } private static String shortenToMaxSize(final String status) { - String result = StringUtils.trim(status); + final String result = StringUtils.trim(status); if (StringUtils.length(result) > MAX_TWEET_SIZE) { return StringUtils.substring(result, 0, MAX_TWEET_SIZE - 1) + '…'; } @@ -98,7 +100,7 @@ public final class Twitter { } private static String appendHashTags(final String status) { - StringBuilder builder = new StringBuilder(status); + final StringBuilder builder = new StringBuilder(status); appendHashTag(builder, "cgeo"); appendHashTag(builder, "geocaching"); return builder.toString(); diff --git a/main/src/cgeo/geocaching/twitter/TwitterAuthorizationActivity.java b/main/src/cgeo/geocaching/twitter/TwitterAuthorizationActivity.java index 97332d3..6df417b 100644 --- a/main/src/cgeo/geocaching/twitter/TwitterAuthorizationActivity.java +++ b/main/src/cgeo/geocaching/twitter/TwitterAuthorizationActivity.java @@ -1,7 +1,7 @@ package cgeo.geocaching.twitter; import cgeo.geocaching.R; -import cgeo.geocaching.network.OAuthAuthorizationActivity; +import cgeo.geocaching.activity.OAuthAuthorizationActivity; import cgeo.geocaching.settings.Settings; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -15,8 +15,8 @@ public class TwitterAuthorizationActivity extends OAuthAuthorizationActivity { "/oauth/authorize", "/oauth/access_token", true, - Settings.getKeyConsumerPublic(), - Settings.getKeyConsumerSecret(), + Settings.getTwitterKeyConsumerPublic(), + Settings.getTwitterKeyConsumerSecret(), "callback://www.cgeo.org/twitter/"); @Override diff --git a/main/src/cgeo/geocaching/ui/AbstractCachingListViewPageViewCreator.java b/main/src/cgeo/geocaching/ui/AbstractCachingListViewPageViewCreator.java index 06fa1fa..40d077e 100644 --- a/main/src/cgeo/geocaching/ui/AbstractCachingListViewPageViewCreator.java +++ b/main/src/cgeo/geocaching/ui/AbstractCachingListViewPageViewCreator.java @@ -30,10 +30,10 @@ public abstract class AbstractCachingListViewPageViewCreator extends AbstractCac if (view == null) { return null; } - int position = view.getFirstVisiblePosition(); - View child = view.getChildAt(0); - int positionFromTop = (child == null) ? 0 : child.getTop(); - Bundle state = new Bundle(); + final int position = view.getFirstVisiblePosition(); + final View child = view.getChildAt(0); + final int positionFromTop = (child == null) ? 0 : child.getTop(); + final Bundle state = new Bundle(); state.putInt(STATE_POSITION, position); state.putInt(STATE_POSITION_FROM_TOP, positionFromTop); return state; @@ -44,12 +44,12 @@ public abstract class AbstractCachingListViewPageViewCreator extends AbstractCac * */ @Override - public void setViewState(@NonNull Bundle state) { + public void setViewState(@NonNull final Bundle state) { if (view == null) { return; } - int logViewPosition = state.getInt(STATE_POSITION); - int logViewPositionFromTop = state.getInt(STATE_POSITION_FROM_TOP); + final int logViewPosition = state.getInt(STATE_POSITION); + final int logViewPositionFromTop = state.getInt(STATE_POSITION_FROM_TOP); view.setSelectionFromTop(logViewPosition, logViewPositionFromTop); } diff --git a/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java b/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java index 306c686..60e8b2d 100644 --- a/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java +++ b/main/src/cgeo/geocaching/ui/AbstractCachingPageViewCreator.java @@ -14,7 +14,6 @@ import android.view.ViewGroup; /** * View creator which destroys the created view on every {@link #notifyDataSetChanged()}. * - * @param <ViewClass> */ public abstract class AbstractCachingPageViewCreator<ViewClass extends View> implements PageViewCreator { diff --git a/main/src/cgeo/geocaching/ui/AbstractImageAdapter.java b/main/src/cgeo/geocaching/ui/AbstractImageAdapter.java new file mode 100644 index 0000000..22ec603 --- /dev/null +++ b/main/src/cgeo/geocaching/ui/AbstractImageAdapter.java @@ -0,0 +1,66 @@ +package cgeo.geocaching.ui; + +import cgeo.geocaching.R; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; + +import java.util.List; + +public abstract class AbstractImageAdapter extends BaseAdapter { + private final Context context; + // references to our images + private final List<Integer> imageIds; + private final int imageWidth; + + public AbstractImageAdapter(final Context context, final GridView gridView, final List<Integer> imageIds) { + this.context = context; + this.imageIds = imageIds; + + final Drawable drawable = context.getResources().getDrawable(imageIds.get(0)); + imageWidth = drawable.getIntrinsicWidth(); + + // fix the column width, now that we know the images + gridView.setColumnWidth(getImageWidth()); + } + + @Override + public int getCount() { + return imageIds.size(); + } + + @Override + public Object getItem(final int position) { + return null; + } + + @Override + public long getItemId(final int position) { + return 0; + } + + // create a new ImageView for each item referenced by the Adapter + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + final ImageView imageView; + if (convertView == null) { // if it's not recycled, initialize some attributes + imageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.grid_image, null); + } else { + imageView = (ImageView) convertView; + } + + imageView.setImageResource(imageIds.get(position)); + return imageView; + } + + int getImageWidth() { + return imageWidth; + } + +}
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/ui/AbstractMenuActionProvider.java b/main/src/cgeo/geocaching/ui/AbstractMenuActionProvider.java new file mode 100644 index 0000000..fbea675 --- /dev/null +++ b/main/src/cgeo/geocaching/ui/AbstractMenuActionProvider.java @@ -0,0 +1,24 @@ +package cgeo.geocaching.ui; + +import android.content.Context; +import android.support.v4.view.ActionProvider; +import android.view.View; + +public abstract class AbstractMenuActionProvider extends ActionProvider { + + public AbstractMenuActionProvider(final Context context) { + super(context); + } + + @Override + public boolean hasSubMenu() { + return true; + } + + @Override + public View onCreateActionView() { + // must return null, otherwise the menu will not work + return null; + } + +} diff --git a/main/src/cgeo/geocaching/ui/AbstractUserClickListener.java b/main/src/cgeo/geocaching/ui/AbstractUserClickListener.java index f26cb53..1efa15e 100644 --- a/main/src/cgeo/geocaching/ui/AbstractUserClickListener.java +++ b/main/src/cgeo/geocaching/ui/AbstractUserClickListener.java @@ -24,7 +24,7 @@ abstract class AbstractUserClickListener implements View.OnClickListener { } @Override - public void onClick(View view) { + public void onClick(final View view) { if (view == null) { return; } @@ -45,8 +45,8 @@ abstract class AbstractUserClickListener implements View.OnClickListener { final AbstractActivity activity = (AbstractActivity) view.getContext(); final Resources res = activity.getResources(); - ArrayList<String> labels = new ArrayList<>(userActions.size()); - for (UserAction action : userActions) { + final ArrayList<String> labels = new ArrayList<>(userActions.size()); + for (final UserAction action : userActions) { labels.add(res.getString(action.displayResourceId)); } final CharSequence[] items = labels.toArray(new String[labels.size()]); @@ -55,7 +55,7 @@ abstract class AbstractUserClickListener implements View.OnClickListener { builder.setTitle(res.getString(R.string.user_menu_title) + " " + userName); builder.setItems(items, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int item) { + public void onClick(final DialogInterface dialog, final int item) { userActions.get(item).run(new Context(userName, activity)); } }); diff --git a/main/src/cgeo/geocaching/ui/AbstractViewHolder.java b/main/src/cgeo/geocaching/ui/AbstractViewHolder.java index b1cb719..0233bf0 100644 --- a/main/src/cgeo/geocaching/ui/AbstractViewHolder.java +++ b/main/src/cgeo/geocaching/ui/AbstractViewHolder.java @@ -11,7 +11,7 @@ import android.view.View; */ public abstract class AbstractViewHolder { - protected AbstractViewHolder(View view) { + protected AbstractViewHolder(final View view) { ButterKnife.inject(this, view); view.setTag(this); } diff --git a/main/src/cgeo/geocaching/ui/AddressListAdapter.java b/main/src/cgeo/geocaching/ui/AddressListAdapter.java index 81b6c23..901bffc 100644 --- a/main/src/cgeo/geocaching/ui/AddressListAdapter.java +++ b/main/src/cgeo/geocaching/ui/AddressListAdapter.java @@ -3,12 +3,13 @@ package cgeo.geocaching.ui; import butterknife.InjectView; import cgeo.geocaching.CacheListActivity; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.sensors.Sensors; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import android.app.Activity; import android.location.Address; @@ -23,7 +24,7 @@ import java.util.ArrayList; public class AddressListAdapter extends ArrayAdapter<Address> { final private LayoutInflater inflater; - final private Geopoint location; + @NonNull final private Geopoint location; protected static final class ViewHolder extends AbstractViewHolder { @InjectView(R.id.label) protected TextView label; @@ -37,7 +38,7 @@ public class AddressListAdapter extends ArrayAdapter<Address> { public AddressListAdapter(final Activity context) { super(context, 0); inflater = context.getLayoutInflater(); - location = CgeoApplication.getInstance().currentGeo().getCoords(); + location = Sensors.getInstance().currentGeo().getCoords(); } @Override @@ -72,7 +73,7 @@ public class AddressListAdapter extends ArrayAdapter<Address> { } private CharSequence getDistanceText(final Address address) { - if (location != null && address.hasLatitude() && address.hasLongitude()) { + if (address.hasLatitude() && address.hasLongitude()) { return Units.getDistanceFromKilometers(location.distanceTo(new Geopoint(address.getLatitude(), address.getLongitude()))); } diff --git a/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java b/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java index d4c2e10..57c9db0 100644 --- a/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java +++ b/main/src/cgeo/geocaching/ui/AnchorAwareLinkMovementMethod.java @@ -26,10 +26,10 @@ public class AnchorAwareLinkMovementMethod extends LinkMovementMethod { } @Override - public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + public boolean onTouchEvent(final TextView widget, final Spannable buffer, final MotionEvent event) { try { return super.onTouchEvent(widget, buffer, event); - } catch (Exception e) { + } catch (final Exception ignored) { // local links to anchors don't work } return false; diff --git a/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java b/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java index 40cd726..e2af419 100644 --- a/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java +++ b/main/src/cgeo/geocaching/ui/CacheDetailsCreator.java @@ -2,31 +2,29 @@ package cgeo.geocaching.ui; import butterknife.ButterKnife; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; +import cgeo.geocaching.ICoordinates; import cgeo.geocaching.R; import cgeo.geocaching.Waypoint; import cgeo.geocaching.connector.ConnectorFactory; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Units; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.utils.Formatter; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import android.annotation.SuppressLint; import android.app.Activity; import android.content.res.Resources; -import android.text.format.DateUtils; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; +import android.widget.RatingBar; import android.widget.RelativeLayout; import android.widget.TextView; import java.util.ArrayList; -import java.util.Date; import java.util.List; // TODO The suppression of this lint finding is bad. But to fix it, someone needs to rework the layout of the cache @@ -46,18 +44,20 @@ public final class CacheDetailsCreator { } /** - * @param nameId - * @param value - * @return the view containing the displayed string (i.e. the right side one from the pair of "label": "value") + * Create a "name: value" line. + * + * @param nameId the resource of the name field + * @param value the initial value + * @return a pair made of the whole "name: value" line (to be able to hide it for example) and of the value (to update it) */ - public TextView add(final int nameId, final CharSequence value) { + public ImmutablePair<RelativeLayout, TextView> add(final int nameId, final CharSequence value) { final RelativeLayout layout = (RelativeLayout) activity.getLayoutInflater().inflate(R.layout.cache_information_item, null, false); final TextView nameView = ButterKnife.findById(layout, R.id.name); nameView.setText(res.getString(nameId)); lastValueView = ButterKnife.findById(layout, R.id.value); lastValueView.setText(value); parentView.addView(layout); - return lastValueView; + return ImmutablePair.of(layout, lastValueView); } public TextView getValueView() { @@ -72,41 +72,29 @@ public final class CacheDetailsCreator { final RelativeLayout layout = (RelativeLayout) activity.getLayoutInflater().inflate(R.layout.cache_information_item, null, false); final TextView nameView = ButterKnife.findById(layout, R.id.name); lastValueView = ButterKnife.findById(layout, R.id.value); - final LinearLayout layoutStars = ButterKnife.findById(layout, R.id.stars); nameView.setText(activity.getResources().getString(nameId)); lastValueView.setText(String.format("%.1f", value) + ' ' + activity.getResources().getString(R.string.cache_rating_of) + " " + String.format("%d", max)); - createStarImages(layoutStars, value, max); + + final RatingBar layoutStars = ButterKnife.findById(layout, R.id.stars); + layoutStars.setRating(value); layoutStars.setVisibility(View.VISIBLE); parentView.addView(layout); return layout; } - private void createStarImages(final ViewGroup starsContainer, final float value, final int max) { - final LayoutInflater inflater = LayoutInflater.from(activity); - - for (int i = 0; i < max; i++) { - final ImageView star = (ImageView) inflater.inflate(R.layout.star_image, starsContainer, false); - if (value - i >= 0.75) { - star.setImageResource(R.drawable.star_on); - } else if (value - i >= 0.25) { - star.setImageResource(R.drawable.star_half); - } else { - star.setImageResource(R.drawable.star_off); - } - starsContainer.addView(star); - } - } - 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,19 +109,31 @@ public final class CacheDetailsCreator { } } + private static String getVisitedDate(final Geocache cache) { + final long visited = cache.getVisitedDate(); + return visited != 0 ? " (" + Formatter.formatShortDate(visited) + ")" : ""; + } + + private static Float distanceNonBlocking(final ICoordinates target) { + if (target.getCoords() == null) { + return null; + } + return Sensors.getInstance().currentGeo().getCoords().distanceTo(target); + } + 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); } } } public void addSize(final Geocache cache) { - if (null != cache.getSize() && cache.showSize()) { + if (cache.showSize()) { add(R.string.cache_size, cache.getSize().getL10n()); } } @@ -151,7 +151,7 @@ public final class CacheDetailsCreator { } public void addDistance(final Geocache cache, final TextView cacheDistanceView) { - Float distance = CgeoApplication.getInstance().distanceNonBlocking(cache); + Float distance = distanceNonBlocking(cache); if (distance == null) { if (cache.getDistance() != null) { distance = cache.getDistance(); @@ -170,7 +170,7 @@ public final class CacheDetailsCreator { } public void addDistance(final Waypoint wpt, final TextView waypointDistanceView) { - Float distance = CgeoApplication.getInstance().distanceNonBlocking(wpt); + final Float distance = distanceNonBlocking(wpt); String text = "--"; if (distance != null) { text = Units.getDistanceFromKilometers(distance); @@ -191,20 +191,12 @@ public final class CacheDetailsCreator { } public TextView addHiddenDate(final @NonNull Geocache cache) { - final Date hiddenDate = cache.getHiddenDate(); - if (hiddenDate == null) { + final String dateString = Formatter.formatHiddenDate(cache); + if (StringUtils.isEmpty(dateString)) { return null; } - final long time = hiddenDate.getTime(); - if (time > 0) { - String dateString = Formatter.formatFullDate(time); - if (cache.isEventCache()) { - dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString; - } - final TextView view = add(cache.isEventCache() ? R.string.cache_event : R.string.cache_hidden, dateString); - view.setId(R.id.date); - return view; - } - return null; + final TextView view = add(cache.isEventCache() ? R.string.cache_event : R.string.cache_hidden, dateString).right; + view.setId(R.id.date); + return view; } } diff --git a/main/src/cgeo/geocaching/ui/CacheListAdapter.java b/main/src/cgeo/geocaching/ui/CacheListAdapter.java index eaede2a..d9daa1d 100644 --- a/main/src/cgeo/geocaching/ui/CacheListAdapter.java +++ b/main/src/cgeo/geocaching/ui/CacheListAdapter.java @@ -3,14 +3,14 @@ package cgeo.geocaching.ui; import butterknife.InjectView; import cgeo.geocaching.CacheDetailActivity; -import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.enumerations.CacheListType; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.filter.IFilter; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.sensors.GeoData; +import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.sorting.CacheComparator; import cgeo.geocaching.sorting.DistanceComparator; @@ -18,7 +18,7 @@ import cgeo.geocaching.sorting.EventDateComparator; import cgeo.geocaching.sorting.InverseComparator; import cgeo.geocaching.sorting.VisitComparator; import cgeo.geocaching.utils.AngleUtils; -import cgeo.geocaching.utils.DateUtils; +import cgeo.geocaching.utils.CalendarUtils; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; @@ -27,9 +27,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.eclipse.jdt.annotation.NonNull; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; + import android.annotation.SuppressLint; import android.app.Activity; import android.content.res.Resources; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.text.Spannable; @@ -108,9 +112,10 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { @InjectView(R.id.distance) protected DistanceView distance; @InjectView(R.id.favorite) protected TextView favorite; @InjectView(R.id.info) protected TextView info; - @InjectView(R.id.inventory) protected ImageView inventory; + @InjectView(R.id.inventory) protected TextView inventory; @InjectView(R.id.direction) protected CompassMiniView direction; @InjectView(R.id.dirimg) protected ImageView dirImg; + public Geocache cache = null; public ViewHolder(final View view) { super(view); @@ -119,10 +124,8 @@ 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(); - } + final GeoData currentGeo = Sensors.getInstance().currentGeo(); + coords = currentGeo.getCoords(); this.res = activity.getResources(); this.list = list; this.cacheListType = cacheListType; @@ -157,7 +160,6 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { /** * change the sort order * - * @param comparator */ public void setComparator(final CacheComparator comparator) { cacheComparator = comparator; @@ -380,6 +382,7 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { } else { holder = (ViewHolder) v.getTag(); } + holder.cache = cache; final boolean lightSkin = Settings.isLightSkin(); @@ -411,7 +414,7 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { } Spannable spannable = null; - if (cache.isDisabled() || cache.isArchived() || DateUtils.isPastEvent(cache)) { // strike + if (cache.isDisabled() || cache.isArchived() || CalendarUtils.isPastEvent(cache)) { // strike spannable = Spannable.Factory.getInstance().newSpannable(cache.getName()); spannable.setSpan(new StrikethroughSpan(), 0, spannable.toString().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -430,7 +433,9 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { } holder.text.setCompoundDrawablesWithIntrinsicBounds(getCacheIcon(cache), null, null, null); - if (cache.getInventoryItems() > 0) { + final int inventorySize = cache.getInventoryItems(); + if (inventorySize > 0) { + holder.inventory.setText(Integer.toString(inventorySize)); holder.inventory.setVisibility(View.VISIBLE); } else { holder.inventory.setVisibility(View.GONE); @@ -459,9 +464,17 @@ public class CacheListAdapter extends ArrayAdapter<Geocache> { holder.direction.updateAzimuth(azimuth); holder.direction.updateHeading(cache.getDirection()); } else if (StringUtils.isNotBlank(cache.getDirectionImg())) { - holder.dirImg.setImageDrawable(DirectionImage.getDrawable(cache.getDirectionImg())); - holder.dirImg.setVisibility(View.VISIBLE); + holder.dirImg.setVisibility(View.INVISIBLE); holder.direction.setVisibility(View.GONE); + DirectionImage.fetchDrawable(cache.getDirectionImg()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<BitmapDrawable>() { + @Override + public void call(final BitmapDrawable bitmapDrawable) { + if (cache == holder.cache) { + holder.dirImg.setImageDrawable(bitmapDrawable); + holder.dirImg.setVisibility(View.VISIBLE); + } + } + }); } else { holder.dirImg.setVisibility(View.GONE); holder.direction.setVisibility(View.GONE); diff --git a/main/src/cgeo/geocaching/ui/CompassMiniView.java b/main/src/cgeo/geocaching/ui/CompassMiniView.java index 50823fd..d50d806 100644 --- a/main/src/cgeo/geocaching/ui/CompassMiniView.java +++ b/main/src/cgeo/geocaching/ui/CompassMiniView.java @@ -1,7 +1,7 @@ package cgeo.geocaching.ui; import cgeo.geocaching.R; -import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.AngleUtils; @@ -50,18 +50,19 @@ final public class CompassMiniView extends View { private static final float MINIMUM_ROTATION_DEGREES_FOR_REPAINT = 5; private float azimuthRelative; - public CompassMiniView(Context context) { + public CompassMiniView(final Context context) { super(context); } - public CompassMiniView(Context context, AttributeSet attrs) { + public CompassMiniView(final Context context, final AttributeSet attrs) { super(context, attrs); } @Override public void onAttachedToWindow() { if (instances++ == 0) { - compassArrow = BitmapFactory.decodeResource(getResources(), Settings.isLightSkin() ? R.drawable.compass_arrow_mini_black : R.drawable.compass_arrow_mini_white); + final int drawable = isInEditMode() || !Settings.isLightSkin() ? R.drawable.compass_arrow_mini_white : R.drawable.compass_arrow_mini_black; + compassArrow = BitmapFactory.decodeResource(getResources(), drawable); compassArrowWidth = compassArrow.getWidth(); compassArrowHeight = compassArrow.getWidth(); } @@ -79,12 +80,12 @@ final public class CompassMiniView extends View { targetCoords = point; } - public void updateAzimuth(float azimuth) { + public void updateAzimuth(final float azimuth) { this.azimuth = azimuth; updateDirection(); } - public void updateHeading(float heading) { + public void updateHeading(final float heading) { this.heading = heading; updateDirection(); } @@ -106,7 +107,7 @@ final public class CompassMiniView extends View { azimuthRelative = AngleUtils.normalize(azimuth - heading); // avoid updates on very small changes, which are not visible to the user - float change = Math.abs(azimuthRelative - lastDrawnAzimuth); + final float change = Math.abs(azimuthRelative - lastDrawnAzimuth); if (change < MINIMUM_ROTATION_DEGREES_FOR_REPAINT) { return; } @@ -119,7 +120,7 @@ final public class CompassMiniView extends View { } @Override - protected void onDraw(Canvas canvas) { + protected void onDraw(final Canvas canvas) { super.onDraw(canvas); lastDrawnAzimuth = azimuthRelative; @@ -137,11 +138,11 @@ final public class CompassMiniView extends View { } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } - private int measureWidth(int measureSpec) { + private int measureWidth(final int measureSpec) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); @@ -158,7 +159,7 @@ final public class CompassMiniView extends View { return result; } - private int measureHeight(int measureSpec) { + private int measureHeight(final int measureSpec) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); 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/CoordinatesFormatSwitcher.java b/main/src/cgeo/geocaching/ui/CoordinatesFormatSwitcher.java index 299256c..db299a2 100644 --- a/main/src/cgeo/geocaching/ui/CoordinatesFormatSwitcher.java +++ b/main/src/cgeo/geocaching/ui/CoordinatesFormatSwitcher.java @@ -1,7 +1,7 @@ package cgeo.geocaching.ui; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.GeopointFormatter; import android.view.View; import android.view.View.OnClickListener; @@ -13,7 +13,7 @@ import android.widget.TextView; */ public class CoordinatesFormatSwitcher implements OnClickListener { - private static GeopointFormatter.Format[] availableFormats = new GeopointFormatter.Format[] { + private static final GeopointFormatter.Format[] availableFormats = new GeopointFormatter.Format[] { GeopointFormatter.Format.LAT_LON_DECMINUTE, GeopointFormatter.Format.LAT_LON_DECSECOND, GeopointFormatter.Format.LAT_LON_DECDEGREE @@ -28,10 +28,10 @@ public class CoordinatesFormatSwitcher implements OnClickListener { } @Override - public void onClick(View view) { + public void onClick(final View view) { assert view instanceof TextView; position = (position + 1) % availableFormats.length; - TextView textView = (TextView) view; + final TextView textView = (TextView) view; // rotate coordinate formats on click textView.setText(coordinates.format(availableFormats[position])); } diff --git a/main/src/cgeo/geocaching/ui/DecryptTextClickListener.java b/main/src/cgeo/geocaching/ui/DecryptTextClickListener.java index e2e587e..30b0e5a 100644 --- a/main/src/cgeo/geocaching/ui/DecryptTextClickListener.java +++ b/main/src/cgeo/geocaching/ui/DecryptTextClickListener.java @@ -1,40 +1,37 @@ -package cgeo.geocaching.ui;
-
-import cgeo.geocaching.utils.CryptUtils;
-
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.text.Spannable;
-import android.view.View;
-import android.widget.TextView;
-
-public class DecryptTextClickListener implements View.OnClickListener {
-
- @NonNull private final TextView targetView;
-
- public DecryptTextClickListener(@NonNull final TextView targetView) {
- this.targetView = targetView;
- }
-
- @Override
- public final void onClick(final View view) {
- try {
- // do not run the click listener if a link was clicked
- if (targetView.getSelectionStart() != -1 || targetView.getSelectionEnd() != -1) {
- return;
- }
-
- CharSequence text = targetView.getText();
- if (text instanceof Spannable) {
- Spannable span = (Spannable) text;
- targetView.setText(CryptUtils.rot13(span));
- }
- else {
- String string = (String) text;
- targetView.setText(CryptUtils.rot13(string));
- }
- } catch (RuntimeException e) {
- // nothing
- }
- }
-}
+package cgeo.geocaching.ui; + +import cgeo.geocaching.utils.CryptUtils; + +import org.eclipse.jdt.annotation.NonNull; + +import android.text.Spannable; +import android.view.View; +import android.widget.TextView; + +public class DecryptTextClickListener implements View.OnClickListener { + + @NonNull private final TextView targetView; + + public DecryptTextClickListener(@NonNull final TextView targetView) { + this.targetView = targetView; + } + + @Override + public final void onClick(final View view) { + try { + // do not run the click listener if a link was clicked + if (targetView.getSelectionStart() != -1 || targetView.getSelectionEnd() != -1) { + return; + } + + final CharSequence text = targetView.getText(); + if (text instanceof Spannable) { + targetView.setText(CryptUtils.rot13((Spannable) text)); + } else { + targetView.setText(CryptUtils.rot13((String) text)); + } + } catch (final RuntimeException ignored) { + // nothing + } + } +} diff --git a/main/src/cgeo/geocaching/ui/DirectionImage.java b/main/src/cgeo/geocaching/ui/DirectionImage.java index cd7695e..e08ff51 100644 --- a/main/src/cgeo/geocaching/ui/DirectionImage.java +++ b/main/src/cgeo/geocaching/ui/DirectionImage.java @@ -3,22 +3,22 @@ package cgeo.geocaching.ui; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.network.HtmlImage; -import org.apache.commons.lang3.StringUtils; +import rx.Observable; import android.graphics.drawable.BitmapDrawable; public class DirectionImage { - static private HtmlImage htmlImage = new HtmlImage(HtmlImage.SHARED, false, StoredList.STANDARD_LIST_ID, false); + static final private HtmlImage HTML_IMAGE = new HtmlImage(HtmlImage.SHARED, false, StoredList.STANDARD_LIST_ID, false); /** * Retrieve the direction image corresponding to the direction code. * * @param directionCode one of the eight cardinal points - * @return a drawable with the arrow pointing into the right direction + * @return an observable containing zero or more drawables (the last one being the freshest image) */ - public static BitmapDrawable getDrawable(final String directionCode) { - return StringUtils.isNotBlank(directionCode) ? htmlImage.getDrawable("http://www.geocaching.com/images/icons/compass/" + directionCode + ".gif") : null; + public static Observable<BitmapDrawable> fetchDrawable(final String directionCode) { + return HTML_IMAGE.fetchDrawable("https://www.geocaching.com/images/icons/compass/" + directionCode + ".gif"); } } diff --git a/main/src/cgeo/geocaching/ui/DistanceView.java b/main/src/cgeo/geocaching/ui/DistanceView.java index a61fc4d..fb40ab4 100644 --- a/main/src/cgeo/geocaching/ui/DistanceView.java +++ b/main/src/cgeo/geocaching/ui/DistanceView.java @@ -1,7 +1,7 @@ package cgeo.geocaching.ui; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.Units; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Units; import org.eclipse.jdt.annotation.NonNull; @@ -12,15 +12,15 @@ import android.widget.TextView; public class DistanceView extends TextView { private Geopoint cacheCoords = null; - public DistanceView(Context context) { + public DistanceView(final Context context) { super(context); } - public DistanceView(Context context, AttributeSet attrs) { + public DistanceView(final Context context, final AttributeSet attrs) { super(context, attrs); } - public DistanceView(Context context, AttributeSet attrs, int defStyle) { + public DistanceView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } @@ -35,7 +35,7 @@ public class DistanceView extends TextView { setText(Units.getDistanceFromKilometers(coords.distanceTo(cacheCoords))); } - public void setDistance(Float distance) { + public void setDistance(final Float distance) { setText("~" + Units.getDistanceFromKilometers(distance)); } }
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/ui/FileSelectionListAdapter.java b/main/src/cgeo/geocaching/ui/FileSelectionListAdapter.java index e07bbc3..6f7f587 100644 --- a/main/src/cgeo/geocaching/ui/FileSelectionListAdapter.java +++ b/main/src/cgeo/geocaching/ui/FileSelectionListAdapter.java @@ -40,7 +40,7 @@ public class FileSelectionListAdapter extends ArrayAdapter<File> { View v = rowView; - ViewHolder holder; + final ViewHolder holder; if (v == null) { v = inflater.inflate(R.layout.mapfile_item, parent, false); holder = new ViewHolder(v); diff --git a/main/src/cgeo/geocaching/ui/ImagesList.java b/main/src/cgeo/geocaching/ui/ImagesList.java index 8bd4ac2..27a68c7 100644 --- a/main/src/cgeo/geocaching/ui/ImagesList.java +++ b/main/src/cgeo/geocaching/ui/ImagesList.java @@ -13,7 +13,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import rx.Subscription; -import rx.android.observables.AndroidObservable; +import rx.android.app.AppObservable; import rx.functions.Action0; import rx.functions.Action1; import rx.subscriptions.CompositeSubscription; @@ -102,11 +102,11 @@ public class ImagesList { imagesView = ButterKnife.findById(parentView, R.id.spoiler_list); - final HtmlImage imgGetter = new HtmlImage(geocode, true, offline ? StoredList.STANDARD_LIST_ID : StoredList.TEMPORARY_LIST_ID, false); + final HtmlImage imgGetter = new HtmlImage(geocode, true, offline ? StoredList.STANDARD_LIST_ID : StoredList.TEMPORARY_LIST.id, false); for (final Image img : images) { final LinearLayout rowView = (LinearLayout) inflater.inflate(R.layout.cache_image_item, imagesView, false); - assert(rowView != null); + assert rowView != null; if (StringUtils.isNotBlank(img.getTitle())) { final TextView titleView = ButterKnife.findById(rowView, R.id.title); @@ -121,8 +121,8 @@ public class ImagesList { } final ImageView imageView = (ImageView) inflater.inflate(R.layout.image_item, rowView, false); - assert(imageView != null); - subscriptions.add(AndroidObservable.bindActivity(activity, imgGetter.fetchDrawable(img.getUrl())).subscribe(new Action1<BitmapDrawable>() { + assert imageView != null; + subscriptions.add(AppObservable.bindActivity(activity, imgGetter.fetchDrawable(img.getUrl())).subscribe(new Action1<BitmapDrawable>() { @Override public void call(final BitmapDrawable image) { display(imageView, image, img, rowView); @@ -203,7 +203,7 @@ public class ImagesList { } private static File saveToTemporaryJPGFile(final BitmapDrawable image) throws FileNotFoundException { - final File file = LocalStorage.getStorageFile(null, "temp.jpg", false, true); + final File file = LocalStorage.getStorageFile(HtmlImage.SHARED, "temp.jpg", false, true); BufferedOutputStream stream = null; try { stream = new BufferedOutputStream(new FileOutputStream(file)); diff --git a/main/src/cgeo/geocaching/ui/IndexOutOfBoundsAvoidingTextView.java b/main/src/cgeo/geocaching/ui/IndexOutOfBoundsAvoidingTextView.java index a0c8b52..4727bf9 100644 --- a/main/src/cgeo/geocaching/ui/IndexOutOfBoundsAvoidingTextView.java +++ b/main/src/cgeo/geocaching/ui/IndexOutOfBoundsAvoidingTextView.java @@ -12,43 +12,43 @@ import android.widget.TextView; */ public class IndexOutOfBoundsAvoidingTextView extends TextView { - public IndexOutOfBoundsAvoidingTextView(Context context, AttributeSet attrs, int defStyle) { + public IndexOutOfBoundsAvoidingTextView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } - public IndexOutOfBoundsAvoidingTextView(Context context, AttributeSet attrs) { + public IndexOutOfBoundsAvoidingTextView(final Context context, final AttributeSet attrs) { super(context, attrs); } - public IndexOutOfBoundsAvoidingTextView(Context context) { + public IndexOutOfBoundsAvoidingTextView(final Context context) { super(context); } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { try{ super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } catch (IndexOutOfBoundsException e) { + } catch (final IndexOutOfBoundsException ignored) { setText(getText().toString()); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override - public void setGravity(int gravity){ + public void setGravity(final int gravity){ try{ super.setGravity(gravity); - } catch (IndexOutOfBoundsException e) { + } catch (final IndexOutOfBoundsException ignored) { setText(getText().toString()); super.setGravity(gravity); } } @Override - public void setText(CharSequence text, BufferType type) { + public void setText(final CharSequence text, final BufferType type) { try{ super.setText(text, type); - } catch (IndexOutOfBoundsException e) { + } catch (final IndexOutOfBoundsException ignored) { setText(text.toString()); } } diff --git a/main/src/cgeo/geocaching/ui/LoggingUI.java b/main/src/cgeo/geocaching/ui/LoggingUI.java index 8454474..e9656e8 100644 --- a/main/src/cgeo/geocaching/ui/LoggingUI.java +++ b/main/src/cgeo/geocaching/ui/LoggingUI.java @@ -17,7 +17,12 @@ import android.widget.ArrayAdapter; import java.util.ArrayList; import java.util.List; -public class LoggingUI extends AbstractUIFactory { +public final class LoggingUI extends AbstractUIFactory { + + private LoggingUI() { + // utility class + } + public static class LogTypeEntry { private final LogType logType; private final SpecialLogType specialLogType; @@ -79,7 +84,7 @@ public class LoggingUI extends AbstractUIFactory { final List<LogType> logTypes = cache.getPossibleLogTypes(); final ArrayList<LogTypeEntry> list = new ArrayList<>(); - for (LogType logType : logTypes) { + for (final LogType logType : logTypes) { list.add(new LogTypeEntry(logType, null, logType == currentLogType)); } if (cache.isLogOffline()) { @@ -94,7 +99,7 @@ public class LoggingUI extends AbstractUIFactory { builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int item) { + public void onClick(final DialogInterface dialog, final int item) { final LogTypeEntry logTypeEntry = adapter.getItem(item); if (logTypeEntry.logType == null) { switch (logTypeEntry.specialLogType) { @@ -116,7 +121,7 @@ public class LoggingUI extends AbstractUIFactory { } - public static void onPrepareOptionsMenu(Menu menu, Geocache cache) { + public static void onPrepareOptionsMenu(final Menu menu, final Geocache cache) { if (cache == null) { return; } @@ -127,7 +132,7 @@ public class LoggingUI extends AbstractUIFactory { itemOffline.setVisible(cache.supportsLogging() && Settings.getLogOffline()); } - public static void addMenuItems(Activity activity, Menu menu, Geocache cache) { + public static void addMenuItems(final Activity activity, final Menu menu, final Geocache cache) { activity.getMenuInflater().inflate(R.menu.logging_ui, menu); onPrepareOptionsMenu(menu, cache); } diff --git a/main/src/cgeo/geocaching/ui/NavigationActionProvider.java b/main/src/cgeo/geocaching/ui/NavigationActionProvider.java index 5840e27..ed4455d 100644 --- a/main/src/cgeo/geocaching/ui/NavigationActionProvider.java +++ b/main/src/cgeo/geocaching/ui/NavigationActionProvider.java @@ -9,6 +9,10 @@ import android.support.v4.view.ActionProvider; import android.view.LayoutInflater; import android.view.View; +/** + * Action provider showing the compass icon, and reacting to normal click (primary navigation) and long click (secondary + * navigation). + */ public class NavigationActionProvider extends ActionProvider { private final Context context; diff --git a/main/src/cgeo/geocaching/ui/OwnerActionsClickListener.java b/main/src/cgeo/geocaching/ui/OwnerActionsClickListener.java index acd43cb..9aa6bec 100644 --- a/main/src/cgeo/geocaching/ui/OwnerActionsClickListener.java +++ b/main/src/cgeo/geocaching/ui/OwnerActionsClickListener.java @@ -15,13 +15,13 @@ public class OwnerActionsClickListener extends AbstractUserClickListener { private final Geocache cache; - public OwnerActionsClickListener(Geocache cache) { + public OwnerActionsClickListener(final Geocache cache) { super(ConnectorFactory.getConnector(cache).getUserActions()); this.cache = cache; } @Override - protected String getUserName(View view) { + protected String getUserName(final View view) { // Use real owner name vice the one owner chose to display if (StringUtils.isNotBlank(cache.getOwnerUserId())) { return cache.getOwnerUserId(); diff --git a/main/src/cgeo/geocaching/ui/UrlPopup.java b/main/src/cgeo/geocaching/ui/UrlPopup.java index 5a8dba4..18d57d5 100644 --- a/main/src/cgeo/geocaching/ui/UrlPopup.java +++ b/main/src/cgeo/geocaching/ui/UrlPopup.java @@ -17,20 +17,20 @@ public class UrlPopup { } public void show(final String title, final String message, final String url, final String urlButtonTitle) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); + final AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setMessage(message) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(title) .setPositiveButton(R.string.err_none, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int id) { + public void onClick(final DialogInterface dialog, final int id) { dialog.cancel(); } }) .setNegativeButton(urlButtonTitle, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int id) { - Intent i = new Intent(Intent.ACTION_VIEW); + public void onClick(final DialogInterface dialog, final int id) { + final Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); context.startActivity(i); } diff --git a/main/src/cgeo/geocaching/ui/UserActionsClickListener.java b/main/src/cgeo/geocaching/ui/UserActionsClickListener.java index 19946bc..68c5493 100644 --- a/main/src/cgeo/geocaching/ui/UserActionsClickListener.java +++ b/main/src/cgeo/geocaching/ui/UserActionsClickListener.java @@ -12,16 +12,16 @@ import android.widget.TextView; */ public class UserActionsClickListener extends AbstractUserClickListener { - public UserActionsClickListener(Geocache cache) { + public UserActionsClickListener(final Geocache cache) { super(ConnectorFactory.getConnector(cache).getUserActions()); } - public UserActionsClickListener(Trackable trackable) { + public UserActionsClickListener(final Trackable trackable) { super(ConnectorFactory.getConnector(trackable).getUserActions()); } @Override - protected String getUserName(View view) { + protected String getUserName(final View view) { return ((TextView) view).getText().toString(); } } diff --git a/main/src/cgeo/geocaching/ui/WeakReferenceHandler.java b/main/src/cgeo/geocaching/ui/WeakReferenceHandler.java index d51e697..aa75db7 100644 --- a/main/src/cgeo/geocaching/ui/WeakReferenceHandler.java +++ b/main/src/cgeo/geocaching/ui/WeakReferenceHandler.java @@ -11,7 +11,6 @@ import java.lang.ref.WeakReference; * * Create static private subclasses of this handler class in your activity. * - * @param <ActivityType> */ public abstract class WeakReferenceHandler<ActivityType extends Activity> extends Handler { diff --git a/main/src/cgeo/geocaching/ui/WrappingGridView.java b/main/src/cgeo/geocaching/ui/WrappingGridView.java new file mode 100644 index 0000000..2c85887 --- /dev/null +++ b/main/src/cgeo/geocaching/ui/WrappingGridView.java @@ -0,0 +1,38 @@ +package cgeo.geocaching.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +/** + * GridView that will adjust its height to really use wrap_content. The standard GridView only shows one line of items. + * + * @see <a href="https://gist.github.com/runemart/9781609">https://gist.github.com/runemart/9781609</a> + * + */ +public class WrappingGridView extends GridView { + + public WrappingGridView(final Context context) { + super(context); + } + + public WrappingGridView(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + public WrappingGridView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + int heightSpec = heightMeasureSpec; + if (getLayoutParams().height == android.view.ViewGroup.LayoutParams.WRAP_CONTENT) { + // The two leftmost bits in the height measure spec have + // a special meaning, hence we can't use them to describe height. + heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + } + super.onMeasure(widthMeasureSpec, heightSpec); + } + +}
\ No newline at end of file diff --git a/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java b/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java index 9aee71a..578a15f 100644 --- a/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java +++ b/main/src/cgeo/geocaching/ui/dialog/CoordinatesInputDialog.java @@ -6,11 +6,13 @@ import cgeo.geocaching.Geocache; import cgeo.geocaching.R; import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.activity.Keyboard; -import cgeo.geocaching.geopoint.Geopoint; -import cgeo.geocaching.geopoint.GeopointFormatter; -import cgeo.geocaching.sensors.IGeoData; +import cgeo.geocaching.location.Geopoint; +import cgeo.geocaching.location.Geopoint.ParseException; +import cgeo.geocaching.location.GeopointFormatter; +import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.Settings.CoordInputFormatEnum; +import cgeo.geocaching.utils.ClipboardUtils; import cgeo.geocaching.utils.EditUtils; import org.apache.commons.lang3.StringUtils; @@ -21,6 +23,7 @@ import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.text.Editable; import android.text.TextWatcher; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -53,16 +56,14 @@ public class CoordinatesInputDialog extends DialogFragment { private static final String CACHECOORDS_ARG = "CACHECOORDS"; - public static CoordinatesInputDialog getInstance(final Geocache cache, final Geopoint gp, final IGeoData geo) { + public static CoordinatesInputDialog getInstance(final Geocache cache, final Geopoint gp, final GeoData geo) { final Bundle args = new Bundle(); if (gp != null) { args.putParcelable(GEOPOINT_ARG, gp); - } else if (geo != null && geo.getCoords() != null) { - args.putParcelable(GEOPOINT_ARG, geo.getCoords()); } else { - args.putParcelable(GEOPOINT_ARG, Geopoint.ZERO); + args.putParcelable(GEOPOINT_ARG, geo != null ? geo.getCoords() : Geopoint.ZERO); } if (geo !=null) { @@ -173,12 +174,27 @@ public class CoordinatesInputDialog extends DialogFragment { buttonCache.setVisibility(View.GONE); } + if (hasClipboardCoordinates()) { + final Button buttonClipboard = ButterKnife.findById(v, R.id.clipboard); + buttonClipboard.setOnClickListener(new ClipboardListener()); + buttonClipboard.setVisibility(View.VISIBLE); + } + final Button buttonDone = ButterKnife.findById(v, R.id.done); buttonDone.setOnClickListener(new InputDoneListener()); return v; } + @SuppressWarnings("unused") + private static boolean hasClipboardCoordinates() { + try { + new Geopoint(StringUtils.defaultString(ClipboardUtils.getText())); + } catch (final ParseException ignored) { + return false; + } + return true; + } private void updateGUI() { @@ -215,8 +231,10 @@ public class CoordinatesInputDialog extends DialogFragment { eLatDeg.setText(addZeros(gp.getLatDeg(), 2)); eLatMin.setText(addZeros(gp.getLatDegFrac(), 5)); + eLatMin.setGravity(Gravity.NO_GRAVITY); eLonDeg.setText(addZeros(gp.getLonDeg(), 3)); eLonMin.setText(addZeros(gp.getLonDegFrac(), 5)); + eLonMin.setGravity(Gravity.NO_GRAVITY); break; case Min: // DDD° MM.MMM getView().findViewById(R.id.coordTable).setVisibility(View.VISIBLE); @@ -238,9 +256,11 @@ public class CoordinatesInputDialog extends DialogFragment { eLatDeg.setText(addZeros(gp.getLatDeg(), 2)); eLatMin.setText(addZeros(gp.getLatMin(), 2)); + eLatMin.setGravity(Gravity.RIGHT); eLatSec.setText(addZeros(gp.getLatMinFrac(), 3)); eLonDeg.setText(addZeros(gp.getLonDeg(), 3)); eLonMin.setText(addZeros(gp.getLonMin(), 2)); + eLonMin.setGravity(Gravity.RIGHT); eLonSec.setText(addZeros(gp.getLonMinFrac(), 3)); break; case Sec: // DDD° MM SS.SSS @@ -263,10 +283,12 @@ public class CoordinatesInputDialog extends DialogFragment { eLatDeg.setText(addZeros(gp.getLatDeg(), 2)); eLatMin.setText(addZeros(gp.getLatMin(), 2)); + eLatMin.setGravity(Gravity.RIGHT); eLatSec.setText(addZeros(gp.getLatSec(), 2)); eLatSub.setText(addZeros(gp.getLatSecFrac(), 3)); eLonDeg.setText(addZeros(gp.getLonDeg(), 3)); eLonMin.setText(addZeros(gp.getLonMin(), 2)); + eLonMin.setGravity(Gravity.RIGHT); eLonSec.setText(addZeros(gp.getLonSec(), 2)); eLonSub.setText(addZeros(gp.getLonSecFrac(), 3)); break; @@ -391,16 +413,18 @@ public class CoordinatesInputDialog extends DialogFragment { final String lonDir = bLon.getText().toString(); final String latDeg = eLatDeg.getText().toString(); final String lonDeg = eLonDeg.getText().toString(); - final String latDegFrac = eLatMin.getText().toString(); - final String lonDegFrac = eLonMin.getText().toString(); + // right-pad decimal fraction + final String latDegFrac = padZerosRight(eLatMin.getText().toString(), 5); + final String lonDegFrac = padZerosRight(eLonMin.getText().toString(), 5); final String latMin = eLatMin.getText().toString(); final String lonMin = eLonMin.getText().toString(); final String latMinFrac = eLatSec.getText().toString(); final String lonMinFrac = eLonSec.getText().toString(); final String latSec = eLatSec.getText().toString(); final String lonSec = eLonSec.getText().toString(); - final String latSecFrac = eLatSub.getText().toString(); - final String lonSecFrac = eLonSub.getText().toString(); + // right-pad seconds fraction + final String latSecFrac = padZerosRight(eLatSub.getText().toString(), 3); + final String lonSecFrac = padZerosRight(eLonSub.getText().toString(), 3); switch (currentFormat) { case Deg: @@ -421,7 +445,7 @@ public class CoordinatesInputDialog extends DialogFragment { gp = current; return true; } - } catch (final Geopoint.ParseException e) { + } catch (final Geopoint.ParseException ignored) { // Signaled and returned below } if (signalError) { @@ -431,6 +455,10 @@ public class CoordinatesInputDialog extends DialogFragment { return false; } + private static String padZerosRight(final String value, final int len) { + return StringUtils.rightPad(value, len, '0'); + } + public int getMaxLengthFromCurrentField(final EditText editText) { if (editText == eLonDeg || editText == eLatSub || editText == eLonSub) { return 3; @@ -503,6 +531,17 @@ public class CoordinatesInputDialog extends DialogFragment { } } + private class ClipboardListener implements View.OnClickListener { + + @Override + public void onClick(final View v) { + try { + gp = new Geopoint(StringUtils.defaultString(ClipboardUtils.getText())); + updateGUI(); + } catch (final ParseException ignored) { + } + } + } private class InputDoneListener implements View.OnClickListener { diff --git a/main/src/cgeo/geocaching/ui/dialog/DateDialog.java b/main/src/cgeo/geocaching/ui/dialog/DateDialog.java index 1046f81..15c9556 100644 --- a/main/src/cgeo/geocaching/ui/dialog/DateDialog.java +++ b/main/src/cgeo/geocaching/ui/dialog/DateDialog.java @@ -22,11 +22,11 @@ public class DateDialog extends DialogFragment { private Calendar date; public static DateDialog getInstance(final Calendar date) { - final DateDialog dd = new DateDialog(); + final DateDialog dateDialog = new DateDialog(); final Bundle args = new Bundle(); args.putSerializable("date", date); - dd.setArguments(args); - return dd; + dateDialog.setArguments(args); + return dateDialog; } @Override diff --git a/main/src/cgeo/geocaching/ui/dialog/Dialogs.java b/main/src/cgeo/geocaching/ui/dialog/Dialogs.java index 21e1a82..3729677 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 @@ -401,7 +408,6 @@ public final class Dialogs { /** * Move the cursor to the end of the input field. * - * @param input */ public static void moveCursorToEnd(final EditText input) { input.setSelection(input.getText().length(), input.getText().length()); @@ -410,4 +416,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/dialog/LiveMapInfoDialogBuilder.java b/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java index 9858c28..d4720bf 100644 --- a/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java +++ b/main/src/cgeo/geocaching/ui/dialog/LiveMapInfoDialogBuilder.java @@ -15,7 +15,7 @@ import android.view.View; public class LiveMapInfoDialogBuilder { - public static AlertDialog create(Activity activity) { + public static AlertDialog create(final Activity activity) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity); final Context themedContext; @@ -32,7 +32,7 @@ public class LiveMapInfoDialogBuilder { builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { + public void onClick(final DialogInterface dialog, final int which) { dialog.dismiss(); CgeoApplication.getInstance().setLiveMapHintShownInThisSession(); } diff --git a/main/src/cgeo/geocaching/ui/logs/CacheLogsViewCreator.java b/main/src/cgeo/geocaching/ui/logs/CacheLogsViewCreator.java index 7e49c97..076c412 100644 --- a/main/src/cgeo/geocaching/ui/logs/CacheLogsViewCreator.java +++ b/main/src/cgeo/geocaching/ui/logs/CacheLogsViewCreator.java @@ -2,6 +2,7 @@ package cgeo.geocaching.ui.logs; import cgeo.geocaching.CacheDetailActivity; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.DataStore; import cgeo.geocaching.Geocache; import cgeo.geocaching.LogEntry; import cgeo.geocaching.R; @@ -24,28 +25,33 @@ import java.util.Map.Entry; public class CacheLogsViewCreator extends LogsViewCreator { private final boolean allLogs; private final Resources res = CgeoApplication.getInstance().getResources(); + private final CacheDetailActivity cacheDetailActivity; - public CacheLogsViewCreator(CacheDetailActivity cacheDetailActivity, boolean allLogs) { + public CacheLogsViewCreator(final CacheDetailActivity cacheDetailActivity, final boolean allLogs) { super(cacheDetailActivity); + this.cacheDetailActivity = cacheDetailActivity; this.allLogs = allLogs; } - /** - * May return null! - * - * @return - */ private Geocache getCache() { - if (this.activity instanceof CacheDetailActivity) { - CacheDetailActivity details = (CacheDetailActivity) this.activity; - return details.getCache(); - } - return null; + return cacheDetailActivity.getCache(); } @Override protected List<LogEntry> getLogs() { - return allLogs ? getCache().getLogs() : getCache().getFriendsLogs(); + final Geocache cache = getCache(); + final List<LogEntry> logs = allLogs ? cache.getLogs() : cache.getFriendsLogs(); + return addOwnOfflineLog(cache, logs); + } + + private List<LogEntry> addOwnOfflineLog(final Geocache cache, final List<LogEntry> logsIn) { + final LogEntry log = DataStore.loadLogOffline(cache.getGeocode()); + final ArrayList<LogEntry> logs = new ArrayList<>(logsIn); + if (log != null) { + log.author = res.getString(R.string.log_your_saved_log); + logs.add(0, log); + } + return logs; } @Override @@ -56,7 +62,7 @@ public class CacheLogsViewCreator extends LogsViewCreator { final List<Entry<LogType, Integer>> sortedLogCounts = new ArrayList<>(logCounts.size()); for (final Entry<LogType, Integer> entry : logCounts.entrySet()) { // it may happen that the label is unknown -> then avoid any output for this type - if (entry.getKey() != LogType.PUBLISH_LISTING && entry.getKey().getL10n() != null && entry.getValue() != 0) { + if (entry.getKey() != LogType.PUBLISH_LISTING && entry.getValue() != 0) { sortedLogCounts.add(entry); } } @@ -66,7 +72,7 @@ public class CacheLogsViewCreator extends LogsViewCreator { Collections.sort(sortedLogCounts, new Comparator<Entry<LogType, Integer>>() { @Override - public int compare(Entry<LogType, Integer> logCountItem1, Entry<LogType, Integer> logCountItem2) { + public int compare(final Entry<LogType, Integer> logCountItem1, final Entry<LogType, Integer> logCountItem2) { return logCountItem1.getKey().compareTo(logCountItem2.getKey()); } }); @@ -84,7 +90,7 @@ public class CacheLogsViewCreator extends LogsViewCreator { } @Override - protected void fillCountOrLocation(LogViewHolder holder, final LogEntry log) { + protected void fillCountOrLocation(final LogViewHolder holder, final LogEntry log) { // finds count if (log.found == -1) { holder.countOrLocation.setVisibility(View.GONE); @@ -95,6 +101,21 @@ public class CacheLogsViewCreator extends LogsViewCreator { } @Override + protected void fillViewHolder(final View convertView, final LogViewHolder holder, final LogEntry log) { + super.fillViewHolder(convertView, holder, log); + if (isOfflineLog(log)) { + holder.author.setOnClickListener(new EditOfflineLogListener(getCache(), cacheDetailActivity)); + holder.text.setOnClickListener(new EditOfflineLogListener(getCache(), cacheDetailActivity)); + holder.marker.setVisibility(View.VISIBLE); + holder.marker.setImageResource(R.drawable.mark_orange); + } + } + + private boolean isOfflineLog(final LogEntry log) { + return log.author.equals(activity.getString(R.string.log_your_saved_log)); + } + + @Override protected boolean isValid() { return getCache() != null; } diff --git a/main/src/cgeo/geocaching/ui/logs/EditOfflineLogListener.java b/main/src/cgeo/geocaching/ui/logs/EditOfflineLogListener.java new file mode 100644 index 0000000..2e0f154 --- /dev/null +++ b/main/src/cgeo/geocaching/ui/logs/EditOfflineLogListener.java @@ -0,0 +1,25 @@ +package cgeo.geocaching.ui.logs; + +import cgeo.geocaching.CacheDetailActivity; +import cgeo.geocaching.Geocache; + +import android.view.View; +import android.view.View.OnClickListener; + +class EditOfflineLogListener implements OnClickListener { + + private final Geocache cache; + private final CacheDetailActivity activity; + + public EditOfflineLogListener(final Geocache cache, final CacheDetailActivity activity) { + this.cache = cache; + this.activity = activity; + } + + @Override + public void onClick(final View v) { + activity.setNeedsRefresh(); + cache.logVisit(activity); + } + +} diff --git a/main/src/cgeo/geocaching/ui/logs/LogViewHolder.java b/main/src/cgeo/geocaching/ui/logs/LogViewHolder.java index 16f5537..302f86c 100644 --- a/main/src/cgeo/geocaching/ui/logs/LogViewHolder.java +++ b/main/src/cgeo/geocaching/ui/logs/LogViewHolder.java @@ -20,7 +20,7 @@ public class LogViewHolder extends AbstractViewHolder { private int position; - public LogViewHolder(View rowView) { + public LogViewHolder(final View rowView) { super(rowView); } diff --git a/main/src/cgeo/geocaching/ui/logs/LogsViewCreator.java b/main/src/cgeo/geocaching/ui/logs/LogsViewCreator.java index 23caf79..a6fd5aa 100644 --- a/main/src/cgeo/geocaching/ui/logs/LogsViewCreator.java +++ b/main/src/cgeo/geocaching/ui/logs/LogsViewCreator.java @@ -4,8 +4,7 @@ import cgeo.geocaching.ImagesActivity; import cgeo.geocaching.LogEntry; import cgeo.geocaching.R; import cgeo.geocaching.activity.AbstractActionBarActivity; -import cgeo.geocaching.list.StoredList; -import cgeo.geocaching.network.HtmlImage; +import cgeo.geocaching.network.SmileyImage; import cgeo.geocaching.ui.AbstractCachingListViewPageViewCreator; import cgeo.geocaching.ui.AnchorAwareLinkMovementMethod; import cgeo.geocaching.ui.DecryptTextClickListener; @@ -69,7 +68,7 @@ public abstract class LogsViewCreator extends AbstractCachingListViewPageViewCre return view; } - protected void fillViewHolder(final View convertView, final LogViewHolder holder, final LogEntry log) { + protected void fillViewHolder(@SuppressWarnings("unused") final View convertView, final LogViewHolder holder, final LogEntry log) { if (log.date > 0) { holder.date.setText(Formatter.formatShortDateVerbally(log.date)); holder.date.setVisibility(View.VISIBLE); @@ -87,7 +86,7 @@ public abstract class LogsViewCreator extends AbstractCachingListViewPageViewCre if (TextUtils.containsHtml(logText)) { logText = log.getDisplayText(); final UnknownTagsHandler unknownTagsHandler = new UnknownTagsHandler(); - holder.text.setText(Html.fromHtml(logText, new HtmlImage(getGeocode(), false, StoredList.STANDARD_LIST_ID, false, holder.text), + holder.text.setText(Html.fromHtml(logText, new SmileyImage(getGeocode(), holder.text), unknownTagsHandler), TextView.BufferType.SPANNABLE); } else { holder.text.setText(logText, TextView.BufferType.SPANNABLE); @@ -117,12 +116,10 @@ public abstract class LogsViewCreator extends AbstractCachingListViewPageViewCre holder.marker.setVisibility(View.GONE); } - if (null == convertView) { - holder.author.setOnClickListener(createUserActionsListener()); - holder.text.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); - holder.text.setOnClickListener(new DecryptTextClickListener(holder.text)); - activity.registerForContextMenu(holder.text); - } + holder.author.setOnClickListener(createUserActionsListener()); + holder.text.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); + holder.text.setOnClickListener(new DecryptTextClickListener(holder.text)); + activity.registerForContextMenu(holder.text); } abstract protected UserActionsClickListener createUserActionsListener(); diff --git a/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java b/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java index 300f510..ef8f5cc 100644 --- a/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java +++ b/main/src/cgeo/geocaching/ui/logs/TrackableLogsViewCreator.java @@ -15,14 +15,15 @@ import java.util.List; public class TrackableLogsViewCreator extends LogsViewCreator { - private final Trackable trackable; + private Trackable trackable; + private final TrackableActivity trackableActivity; /** - * @param trackableActivity */ - public TrackableLogsViewCreator(TrackableActivity trackableActivity, final Trackable trackable) { + public TrackableLogsViewCreator(final TrackableActivity trackableActivity) { super(trackableActivity); - this.trackable = trackable; + this.trackableActivity = trackableActivity; + trackable = trackableActivity.getTrackable(); } @Override @@ -32,6 +33,7 @@ public class TrackableLogsViewCreator extends LogsViewCreator { @Override protected List<LogEntry> getLogs() { + trackable = trackableActivity.getTrackable(); return trackable.getLogs(); } @@ -41,19 +43,20 @@ public class TrackableLogsViewCreator extends LogsViewCreator { } @Override - protected void fillCountOrLocation(LogViewHolder holder, final LogEntry log) { - if (StringUtils.isBlank(log.cacheName)) { - holder.countOrLocation.setVisibility(View.GONE); - } else { + protected void fillCountOrLocation(final LogViewHolder holder, final LogEntry log) { + if (StringUtils.isNotBlank(log.cacheName)) { holder.countOrLocation.setText(Html.fromHtml(log.cacheName)); + holder.countOrLocation.setVisibility(View.VISIBLE); final String cacheGuid = log.cacheGuid; final String cacheName = log.cacheName; holder.countOrLocation.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View arg0) { + public void onClick(final View arg0) { CacheDetailActivity.startActivityGuid(activity, cacheGuid, Html.fromHtml(cacheName).toString()); } }); + } else { + holder.countOrLocation.setVisibility(View.GONE); } } diff --git a/main/src/cgeo/geocaching/utils/AngleUtils.java b/main/src/cgeo/geocaching/utils/AngleUtils.java index fdd9a9d..5ab2c75 100644 --- a/main/src/cgeo/geocaching/utils/AngleUtils.java +++ b/main/src/cgeo/geocaching/utils/AngleUtils.java @@ -1,7 +1,17 @@ package cgeo.geocaching.utils; +import cgeo.geocaching.CgeoApplication; + +import android.content.Context; +import android.view.Surface; +import android.view.WindowManager; + public final class AngleUtils { + private static class WindowManagerHolder { + public static final WindowManager WINDOW_MANAGER = (WindowManager) CgeoApplication.getInstance().getSystemService(Context.WINDOW_SERVICE); + } + private AngleUtils() { // Do not instantiate } @@ -27,4 +37,37 @@ public final class AngleUtils { public static float normalize(final float angle) { return (angle >= 0 ? angle : (360 - ((-angle) % 360))) % 360; } + + public static int getRotationOffset() { + switch (WindowManagerHolder.WINDOW_MANAGER.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + default: + return 0; + } + } + + /** + * Take the phone rotation (through a given activity) in account and adjust the direction. + * + * @param direction the unadjusted direction in degrees, in the [0, 360[ range + * @return the adjusted direction in degrees, in the [0, 360[ range + */ + public static float getDirectionNow(final float direction) { + return normalize(direction + getRotationOffset()); + } + + /** + * Reverse the phone rotation (through a given activity) in account and adjust the direction. + * + * @param direction the unadjusted direction in degrees, in the [0, 360[ range + * @return the adjusted direction in degrees, in the [0, 360[ range + */ + public static float reverseDirectionNow(final float direction) { + return normalize(direction - getRotationOffset()); + } } diff --git a/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java b/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java index 3d2b2b1..c2edd24 100644 --- a/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java +++ b/main/src/cgeo/geocaching/utils/AsyncTaskWithProgress.java @@ -14,8 +14,6 @@ import android.os.AsyncTask; * automatically derived from the number of {@code Params} given to the task in {@link #execute(Object...)}. * </p> * - * @param <Params> - * @param <Result> */ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Params, Integer, Result> { @@ -28,9 +26,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa /** * Creates an AsyncTask with progress dialog. * - * @param activity - * @param progressTitle - * @param progressMessage */ public AsyncTaskWithProgress(final Activity activity, final String progressTitle, final String progressMessage) { this(activity, progressTitle, progressMessage, false); @@ -39,8 +34,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa /** * Creates an AsyncTask with progress dialog. * - * @param activity - * @param progressTitle */ public AsyncTaskWithProgress(final Activity activity, final String progressTitle) { this(activity, progressTitle, null); @@ -49,9 +42,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa /** * Creates an AsyncTask with progress dialog. * - * @param activity - * @param progressTitle - * @param progressMessage */ public AsyncTaskWithProgress(final Activity activity, final String progressTitle, final String progressMessage, final boolean indeterminate) { this.activity = activity; @@ -63,8 +53,6 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa /** * Creates an AsyncTask with progress dialog. * - * @param activity - * @param progressTitle */ public AsyncTaskWithProgress(final Activity activity, final String progressTitle, final boolean indeterminate) { this(activity, progressTitle, null, indeterminate); @@ -102,6 +90,8 @@ public abstract class AsyncTaskWithProgress<Params, Result> extends AsyncTask<Pa * This method should typically be overridden by sub classes instead of {@link #onPostExecute(Object)}. * * @param result + * The result of the operation computed by {@link #doInBackground(Object...)}. + * */ protected void onPostExecuteInternal(final Result result) { // empty by default diff --git a/main/src/cgeo/geocaching/utils/BundleUtils.java b/main/src/cgeo/geocaching/utils/BundleUtils.java index 9c4255b..e61e45e 100644 --- a/main/src/cgeo/geocaching/utils/BundleUtils.java +++ b/main/src/cgeo/geocaching/utils/BundleUtils.java @@ -7,8 +7,8 @@ import android.os.Bundle; public class BundleUtils { @NonNull - public static String getString(Bundle bundle, @NonNull String key, @NonNull String defaultValue) { - String res = bundle.getString(key); + public static String getString(final Bundle bundle, @NonNull final String key, @NonNull final String defaultValue) { + final String res = bundle.getString(key); if (res != null) { return res; } diff --git a/main/src/cgeo/geocaching/utils/DateUtils.java b/main/src/cgeo/geocaching/utils/CalendarUtils.java index 9aa4222..ed3b18c 100644 --- a/main/src/cgeo/geocaching/utils/DateUtils.java +++ b/main/src/cgeo/geocaching/utils/CalendarUtils.java @@ -5,13 +5,13 @@ import cgeo.geocaching.Geocache; import java.util.Calendar; import java.util.Date; -public final class DateUtils { +public final class CalendarUtils { - private DateUtils() { + private CalendarUtils() { // utility class } - public static int daysSince(long date) { + public static int daysSince(final long date) { final Calendar logDate = Calendar.getInstance(); logDate.setTimeInMillis(date); logDate.set(Calendar.SECOND, 0); @@ -24,12 +24,27 @@ public final class DateUtils { return (int) Math.round((today.getTimeInMillis() - logDate.getTimeInMillis()) / 86400000d); } + public static int daysSince(final Calendar date) { + return daysSince(date.getTimeInMillis()); + } + public static boolean isPastEvent(final Geocache cache) { if (!cache.isEventCache()) { return false; } final Date hiddenDate = cache.getHiddenDate(); - return hiddenDate != null && DateUtils.daysSince(hiddenDate.getTime()) > 0; + return hiddenDate != null && CalendarUtils.daysSince(hiddenDate.getTime()) > 0; + } + + /** + * Return whether the given date is *more* than 1 day away. We allow 1 day to be "present time" to compensate for + * potential timezone issues. + * + * @param date + * the date + */ + public static boolean isFuture(final Calendar date) { + return daysSince(date) < -1; } } diff --git a/main/src/cgeo/geocaching/utils/CancellableHandler.java b/main/src/cgeo/geocaching/utils/CancellableHandler.java index 3ed233a..7b7aa6f 100644 --- a/main/src/cgeo/geocaching/utils/CancellableHandler.java +++ b/main/src/cgeo/geocaching/utils/CancellableHandler.java @@ -17,7 +17,7 @@ public abstract class CancellableHandler extends Handler { public static final int DONE = -1000; protected static final int UPDATE_LOAD_PROGRESS_DETAIL = 42186; private volatile boolean cancelled = false; - private static CompositeSubscription subscriptions = new CompositeSubscription(); + private final CompositeSubscription subscriptions = new CompositeSubscription(); private static class CancelHolder { final Object payload; diff --git a/main/src/cgeo/geocaching/utils/CheckerUtils.java b/main/src/cgeo/geocaching/utils/CheckerUtils.java new file mode 100644 index 0000000..39ef078 --- /dev/null +++ b/main/src/cgeo/geocaching/utils/CheckerUtils.java @@ -0,0 +1,35 @@ +package cgeo.geocaching.utils; + +import cgeo.geocaching.Geocache; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import android.util.Patterns; + +import java.util.regex.Matcher; + +public final class CheckerUtils { + private static final String[] CHECKERS = new String[] { "geocheck.org", "geochecker.com", "certitudes.org" }; + + private CheckerUtils() { + // utility class + } + + @Nullable + public static String getCheckerUrl(@NonNull final Geocache cache) { + final String description = cache.getDescription(); + final Matcher matcher = Patterns.WEB_URL.matcher(description); + while (matcher.find()) { + final String url = matcher.group(); + for (final String checker : CHECKERS) { + if (StringUtils.containsIgnoreCase(url, checker)) { + return StringEscapeUtils.unescapeHtml4(url); + } + } + } + return null; + } +} diff --git a/main/src/cgeo/geocaching/utils/ClipboardUtils.java b/main/src/cgeo/geocaching/utils/ClipboardUtils.java index 77250f3..fb30886 100644 --- a/main/src/cgeo/geocaching/utils/ClipboardUtils.java +++ b/main/src/cgeo/geocaching/utils/ClipboardUtils.java @@ -2,6 +2,8 @@ package cgeo.geocaching.utils; import cgeo.geocaching.CgeoApplication; +import org.eclipse.jdt.annotation.Nullable; + import android.content.Context; /** @@ -9,7 +11,6 @@ import android.content.Context; * This class uses the deprecated function ClipboardManager.setText(CharSequence). * API 11 introduced setPrimaryClip(ClipData) */ -@SuppressWarnings("deprecation") public final class ClipboardUtils { private ClipboardUtils() { @@ -22,10 +23,24 @@ public final class ClipboardUtils { * @param text * The text to place in the clipboard. */ + @SuppressWarnings("deprecation") public static void copyToClipboard(final CharSequence text) { // fully qualified name used here to avoid buggy deprecation warning (of javac) on the import statement final android.text.ClipboardManager clipboard = (android.text.ClipboardManager) CgeoApplication.getInstance().getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setText(text); } + /** + * get clipboard content + * + */ + @SuppressWarnings("deprecation") + @Nullable + public static String getText() { + // fully qualified name used here to avoid buggy deprecation warning (of javac) on the import statement + final android.text.ClipboardManager clipboard = (android.text.ClipboardManager) CgeoApplication.getInstance().getSystemService(Context.CLIPBOARD_SERVICE); + final CharSequence text = clipboard.getText(); + return text != null ? text.toString() : null; + } + } diff --git a/main/src/cgeo/geocaching/utils/CryptUtils.java b/main/src/cgeo/geocaching/utils/CryptUtils.java index 815c2f4..4aec509 100644 --- a/main/src/cgeo/geocaching/utils/CryptUtils.java +++ b/main/src/cgeo/geocaching/utils/CryptUtils.java @@ -1,6 +1,5 @@ package cgeo.geocaching.utils; - import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; @@ -23,28 +22,29 @@ public final class CryptUtils { // utility class } - private static char[] base64map1 = new char[64]; - private static byte[] base64map2 = new byte[128]; + private static final byte[] EMPTY = {}; + private static final char[] BASE64MAP1 = new char[64]; + private static final byte[] BASE64MAP2 = new byte[128]; static { int i = 0; for (char c = 'A'; c <= 'Z'; c++) { - base64map1[i++] = c; + BASE64MAP1[i++] = c; } for (char c = 'a'; c <= 'z'; c++) { - base64map1[i++] = c; + BASE64MAP1[i++] = c; } for (char c = '0'; c <= '9'; c++) { - base64map1[i++] = c; + BASE64MAP1[i++] = c; } - base64map1[i++] = '+'; - base64map1[i++] = '/'; + BASE64MAP1[i++] = '+'; + BASE64MAP1[i++] = '/'; - for (i = 0; i < base64map2.length; i++) { - base64map2[i] = -1; + for (i = 0; i < BASE64MAP2.length; i++) { + BASE64MAP2[i] = -1; } for (i = 0; i < 64; i++) { - base64map2[base64map1[i]] = (byte) i; + BASE64MAP2[BASE64MAP1[i]] = (byte) i; } } @@ -58,7 +58,7 @@ public final class CryptUtils { } else if (result == ']') { plaintext = false; } else if (!plaintext) { - int capitalized = result & 32; + final int capitalized = result & 32; result &= ~capitalized; result = ((result >= 'A') && (result <= 'Z') ? ((result - 'A' + 13) % 26 + 'A') : result) | capitalized; @@ -68,50 +68,44 @@ public final class CryptUtils { } @NonNull - public static String rot13(String text) { + public static String rot13(final String text) { if (text == null) { return StringUtils.EMPTY; } final StringBuilder result = new StringBuilder(); - Rot13Encryption rot13 = new Rot13Encryption(); + final Rot13Encryption rot13 = new Rot13Encryption(); final int length = text.length(); for (int index = 0; index < length; index++) { - char c = text.charAt(index); + final char c = text.charAt(index); result.append(rot13.getNextEncryptedCharacter(c)); } return result.toString(); } - public static String md5(String text) { + public static String md5(final String text) { try { final MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(text.getBytes(CharEncoding.UTF_8), 0, text.length()); return new BigInteger(1, digest.digest()).toString(16); - } catch (NoSuchAlgorithmException e) { - Log.e("CryptUtils.md5", e); - } catch (UnsupportedEncodingException e) { + } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { Log.e("CryptUtils.md5", e); } return StringUtils.EMPTY; } - public static byte[] hashHmac(String text, String salt) { - byte[] macBytes = {}; + public static byte[] hashHmac(final String text, final String salt) { try { final SecretKeySpec secretKeySpec = new SecretKeySpec(salt.getBytes(CharEncoding.UTF_8), "HmacSHA1"); final Mac mac = Mac.getInstance("HmacSHA1"); mac.init(secretKeySpec); - macBytes = mac.doFinal(text.getBytes(CharEncoding.UTF_8)); - } catch (GeneralSecurityException e) { - Log.e("CryptUtils.hashHmac", e); - } catch (UnsupportedEncodingException e) { + return mac.doFinal(text.getBytes(CharEncoding.UTF_8)); + } catch (GeneralSecurityException | UnsupportedEncodingException e) { Log.e("CryptUtils.hashHmac", e); + return EMPTY; } - - return macBytes; } public static CharSequence rot13(final Spannable span) { @@ -119,37 +113,37 @@ public final class CryptUtils { // a SpannableStringBuilder instead of the pure text and we must replace each character inline. // Otherwise we loose all the images, colors and so on... final SpannableStringBuilder buffer = new SpannableStringBuilder(span); - Rot13Encryption rot13 = new Rot13Encryption(); + final Rot13Encryption rot13 = new Rot13Encryption(); final int length = span.length(); for (int index = 0; index < length; index++) { - char c = span.charAt(index); + final char c = span.charAt(index); buffer.replace(index, index + 1, String.valueOf(rot13.getNextEncryptedCharacter(c))); } return buffer; } - public static String base64Encode(byte[] in) { - int iLen = in.length; - int oDataLen = (iLen * 4 + 2) / 3; // output length without padding - int oLen = ((iLen + 2) / 3) * 4; // output length including padding - char[] out = new char[oLen]; + public static String base64Encode(final byte[] in) { + final int iLen = in.length; + final int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + final int oLen = ((iLen + 2) / 3) * 4; // output length including padding + final char[] out = new char[oLen]; int ip = 0; int op = 0; while (ip < iLen) { - int i0 = in[ip++] & 0xff; - int i1 = ip < iLen ? in[ip++] & 0xff : 0; - int i2 = ip < iLen ? in[ip++] & 0xff : 0; - int o0 = i0 >>> 2; - int o1 = ((i0 & 3) << 4) | (i1 >>> 4); - int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); - int o3 = i2 & 0x3F; - out[op++] = base64map1[o0]; - out[op++] = base64map1[o1]; - out[op] = op < oDataLen ? base64map1[o2] : '='; + final int i0 = in[ip++] & 0xff; + final int i1 = ip < iLen ? in[ip++] & 0xff : 0; + final int i2 = ip < iLen ? in[ip++] & 0xff : 0; + final int o0 = i0 >>> 2; + final int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + final int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + final int o3 = i2 & 0x3F; + out[op++] = BASE64MAP1[o0]; + out[op++] = BASE64MAP1[o1]; + out[op] = op < oDataLen ? BASE64MAP1[o2] : '='; op++; - out[op] = op < oDataLen ? base64map1[o3] : '='; + out[op] = op < oDataLen ? BASE64MAP1[o3] : '='; op++; } diff --git a/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java b/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java index d8aff74..a65a9fb 100644 --- a/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java +++ b/main/src/cgeo/geocaching/utils/DatabaseBackupUtils.java @@ -6,9 +6,18 @@ import cgeo.geocaching.R; import cgeo.geocaching.ui.dialog.Dialogs; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.schedulers.Schedulers; import android.app.Activity; import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.res.Resources; import java.io.File; @@ -21,70 +30,107 @@ public class DatabaseBackupUtils { } /** - * restore the database in a new thread, showing a progress window + * After confirming to overwrite the existing caches on the devices, restore the database in a new thread, showing a + * progress window * * @param activity * calling activity */ public static void restoreDatabase(final Activity activity) { + if (!hasBackup()) { + return; + } + final int caches = DataStore.getAllCachesCount(); + if (caches == 0) { + restoreDatabaseInternal(activity); + } + else { + Dialogs.confirm(activity, R.string.init_backup_restore, activity.getString(R.string.restore_confirm_overwrite, activity.getResources().getQuantityString(R.plurals.cache_counts, caches, caches)), new OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + restoreDatabaseInternal(activity); + } + }); + + } + } + + private static void restoreDatabaseInternal(final Activity activity) { final Resources res = activity.getResources(); final ProgressDialog dialog = ProgressDialog.show(activity, res.getString(R.string.init_backup_restore), res.getString(R.string.init_restore_running), true, false); final AtomicBoolean restoreSuccessful = new AtomicBoolean(false); - new Thread() { + RxUtils.andThenOnUi(Schedulers.io(), new Action0() { @Override - public void run() { + public void call() { restoreSuccessful.set(DataStore.restoreDatabaseInternal()); - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - dialog.dismiss(); - boolean restored = restoreSuccessful.get(); - String message = restored ? res.getString(R.string.init_restore_success) : res.getString(R.string.init_restore_failed); - Dialogs.message(activity, R.string.init_backup_restore, message); - if (activity instanceof MainActivity) { - ((MainActivity) activity).updateCacheCounter(); - } - } - }); } - }.start(); + }, new Action0() { + @Override + public void call() { + dialog.dismiss(); + final boolean restored = restoreSuccessful.get(); + final String message = restored ? res.getString(R.string.init_restore_success) : res.getString(R.string.init_restore_failed); + Dialogs.message(activity, R.string.init_backup_restore, message); + if (activity instanceof MainActivity) { + ((MainActivity) activity).updateCacheCounter(); + } + } + }); } - public static boolean createBackup(final Activity activity, final Runnable runAfterwards) { + /** + * Create a backup after confirming to overwrite the existing backup. + * + */ + public static void createBackup(final Activity activity, final Runnable runAfterwards) { // avoid overwriting an existing backup with an empty database // (can happen directly after reinstalling the app) if (DataStore.getAllCachesCount() == 0) { Dialogs.message(activity, R.string.init_backup, R.string.init_backup_unnecessary); - return false; + return; } + if (hasBackup()) { + Dialogs.confirm(activity, R.string.init_backup, activity.getString(R.string.backup_confirm_overwrite, getBackupDateTime()), new OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + createBackupInternal(activity, runAfterwards); + } + }); + } + else { + createBackupInternal(activity, runAfterwards); + } + } + + private static void createBackupInternal(final Activity activity, final Runnable runAfterwards) { final ProgressDialog dialog = ProgressDialog.show(activity, activity.getString(R.string.init_backup), activity.getString(R.string.init_backup_running), true, false); - new Thread() { + RxUtils.andThenOnUi(Schedulers.io(), new Func0<String>() { + @Override + public String call() { + return DataStore.backupDatabaseInternal(); + } + }, new Action1<String>() { @Override - public void run() { - final String backupFileName = DataStore.backupDatabaseInternal(); - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - dialog.dismiss(); - Dialogs.message(activity, - R.string.init_backup_backup, - backupFileName != null - ? activity.getString(R.string.init_backup_success) - + "\n" + backupFileName - : activity.getString(R.string.init_backup_failed)); - if (runAfterwards != null) { - runAfterwards.run(); - } - } - }); + public void call(final String backupFileName) { + dialog.dismiss(); + Dialogs.message(activity, + R.string.init_backup_backup, + backupFileName != null + ? activity.getString(R.string.init_backup_success) + + "\n" + backupFileName + : activity.getString(R.string.init_backup_failed)); + if (runAfterwards != null) { + runAfterwards.run(); + } } - }.start(); - return true; + }); } + @Nullable public static File getRestoreFile() { final File fileSourceFile = DataStore.getBackupFileInternal(); return fileSourceFile.exists() && fileSourceFile.length() > 0 ? fileSourceFile : null; @@ -94,6 +140,7 @@ public class DatabaseBackupUtils { return getRestoreFile() != null; } + @NonNull public static String getBackupDateTime() { final File restoreFile = getRestoreFile(); if (restoreFile == null) { diff --git a/main/src/cgeo/geocaching/utils/DebugUtils.java b/main/src/cgeo/geocaching/utils/DebugUtils.java index 07aac64..1f95e7c 100644 --- a/main/src/cgeo/geocaching/utils/DebugUtils.java +++ b/main/src/cgeo/geocaching/utils/DebugUtils.java @@ -22,15 +22,14 @@ public class DebugUtils { public static void createMemoryDump(final @NonNull Context context) { try { - final Date now = new Date(); final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyy-MM-dd_hh-mm", Locale.US); - File file = FileUtils.getUniqueNamedFile(Environment.getExternalStorageDirectory().getPath() - + File.separatorChar + "cgeo_dump_" + fileNameDateFormat.format(now) + ".hprof"); + final File file = FileUtils.getUniqueNamedFile(new File(Environment.getExternalStorageDirectory(), + "cgeo_dump_" + fileNameDateFormat.format(new Date()) + ".hprof")); android.os.Debug.dumpHprofData(file.getPath()); Toast.makeText(context, context.getString(R.string.init_memory_dumped, file.getAbsolutePath()), Toast.LENGTH_LONG).show(); ShareUtils.share(context, file, R.string.init_memory_dump); - } catch (IOException e) { + } catch (final IOException e) { Log.e("createMemoryDump", e); } } diff --git a/main/src/cgeo/geocaching/utils/EditUtils.java b/main/src/cgeo/geocaching/utils/EditUtils.java index 0bfe2ea..455ce4d 100644 --- a/main/src/cgeo/geocaching/utils/EditUtils.java +++ b/main/src/cgeo/geocaching/utils/EditUtils.java @@ -17,7 +17,7 @@ public final class EditUtils { editText.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + public boolean onEditorAction(final TextView v, final int actionId, final KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_GO) { runnable.run(); return true; @@ -30,7 +30,7 @@ public final class EditUtils { editText.setOnKeyListener(new View.OnKeyListener() { @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { + public boolean onKey(final View v, final int keyCode, final KeyEvent event) { // If the event is a key-down event on the "enter" button if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { runnable.run(); @@ -42,7 +42,7 @@ public final class EditUtils { } - public static void disableSuggestions(EditText edit) { + public static void disableSuggestions(final EditText edit) { edit.setInputType(edit.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_FILTER); diff --git a/main/src/cgeo/geocaching/utils/FileUtils.java b/main/src/cgeo/geocaching/utils/FileUtils.java index 979820c..778b9c7 100644 --- a/main/src/cgeo/geocaching/utils/FileUtils.java +++ b/main/src/cgeo/geocaching/utils/FileUtils.java @@ -4,6 +4,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jdt.annotation.NonNull; import android.os.Handler; import android.os.Message; @@ -23,14 +24,18 @@ import java.util.List; */ public final class FileUtils { + private static final int MAX_DIRECTORY_SCAN_DEPTH = 30; private static final String FILE_PROTOCOL = "file://"; private FileUtils() { // utility class } - public static void listDir(List<File> result, File directory, FileSelector chooser, Handler feedBackHandler) { + public static void listDir(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler) { + listDirInternally(result, directory, chooser, feedBackHandler, 0); + } + private static void listDirInternally(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler, final int depths) { if (directory == null || !directory.isDirectory() || !directory.canRead() || result == null || chooser == null) { @@ -40,7 +45,7 @@ public final class FileUtils { final File[] files = directory.listFiles(); if (ArrayUtils.isNotEmpty(files)) { - for (File file : files) { + for (final File file : files) { if (chooser.shouldEnd()) { return; } @@ -63,12 +68,32 @@ public final class FileUtils { feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name)); } - listDir(result, file, chooser, feedBackHandler); // go deeper + if (depths < MAX_DIRECTORY_SCAN_DEPTH) { + listDirInternally(result, file, chooser, feedBackHandler, depths + 1); // go deeper + } } } } } + public static boolean deleteDirectory(@NonNull final File dir) { + final File[] files = dir.listFiles(); + + // Although we are called on an existing directory, it might have been removed concurrently + // in the meantime, for example by the user or by another cleanup task. + if (files != null) { + for (final File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + delete(file); + } + } + } + + return delete(dir); + } + public static abstract class FileSelector { public abstract boolean isSelected(File file); @@ -86,18 +111,20 @@ public final class FileUtils { * </ul> * which does not yet exist. */ - public static File getUniqueNamedFile(final String baseNameAndPath) { - String extension = StringUtils.substringAfterLast(baseNameAndPath, "."); - String pathName = StringUtils.substringBeforeLast(baseNameAndPath, "."); - int number = 1; - while (new File(getNumberedFileName(pathName, extension, number)).exists()) { - number++; + public static File getUniqueNamedFile(final File file) { + if (!file.exists()) { + return file; } - return new File(getNumberedFileName(pathName, extension, number)); - } - - private static String getNumberedFileName(String pathName, String extension, int number) { - return pathName + (number > 1 ? "_" + Integer.toString(number) : "") + "." + extension; + final String baseNameAndPath = file.getPath(); + final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_"; + final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, "."); + for (int i = 1; i < Integer.MAX_VALUE; i++) { + final File numbered = new File(prefix + i + extension); + if (!numbered.exists()) { + return numbered; + } + } + throw new IllegalStateException("Unable to generate a non-existing file name"); } /** @@ -129,7 +156,7 @@ public final class FileUtils { * @return <code>true</code> if the directory was created, <code>false</code> on failure or if the directory already * existed. */ - public static boolean mkdirs(File file) { + public static boolean mkdirs(final File file) { final boolean success = file.mkdirs() || file.isDirectory(); // mkdirs returns false on existing directories if (!success) { Log.e("Could not make directories " + file.getAbsolutePath()); @@ -137,7 +164,7 @@ public final class FileUtils { return success; } - public static boolean writeFileUTF16(File file, String content) { + public static boolean writeFileUTF16(final File file, final String content) { // TODO: replace by some apache.commons IOUtils or FileUtils code Writer fileWriter = null; BufferedOutputStream buffer = null; @@ -177,7 +204,7 @@ public final class FileUtils { /** * Local file name when {@link #isFileUrl(String)} is <tt>true</tt>. - * + * * @return the local file */ public static File urlToFile(final String url) { diff --git a/main/src/cgeo/geocaching/utils/Formatter.java b/main/src/cgeo/geocaching/utils/Formatter.java index 3068cd4..2127d59 100644 --- a/main/src/cgeo/geocaching/utils/Formatter.java +++ b/main/src/cgeo/geocaching/utils/Formatter.java @@ -17,6 +17,7 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; public abstract class Formatter { @@ -33,7 +34,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatTime(long date) { + public static String formatTime(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME); } @@ -45,7 +46,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatDate(long date) { + public static String formatDate(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE); } @@ -58,7 +59,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatFullDate(long date) { + public static String formatFullDate(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR); } @@ -71,11 +72,15 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatShortDate(long date) { - DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context); + public static String formatShortDate(final long date) { + final DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(context); return dateFormat.format(date); } + private static String formatShortDateIncludingWeekday(final long time) { + return DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY) + ", " + formatShortDate(time); + } + /** * Generate a numeric date string according to system-wide settings (locale, date format) * such as "10/20/2010". Today and yesterday will be presented as strings "today" and "yesterday". @@ -84,8 +89,8 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatShortDateVerbally(long date) { - int diff = cgeo.geocaching.utils.DateUtils.daysSince(date); + public static String formatShortDateVerbally(final long date) { + final int diff = CalendarUtils.daysSince(date); switch (diff) { case 0: return CgeoApplication.getInstance().getString(R.string.log_today); @@ -104,7 +109,7 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatShortDateTime(long date) { + public static String formatShortDateTime(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL); } @@ -116,11 +121,11 @@ public abstract class Formatter { * milliseconds since the epoch * @return the formatted string */ - public static String formatDateTime(long date) { + public static String formatDateTime(final long date) { return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); } - public static String formatCacheInfoLong(Geocache cache, CacheListType cacheListType) { + public static String formatCacheInfoLong(final Geocache cache, final CacheListType cacheListType) { final ArrayList<String> infos = new ArrayList<>(); if (StringUtils.isNotBlank(cache.getGeocode())) { infos.add(cache.getGeocode()); @@ -137,18 +142,18 @@ public abstract class Formatter { return StringUtils.join(infos, Formatter.SEPARATOR); } - public static String formatCacheInfoShort(Geocache cache) { + public static String formatCacheInfoShort(final Geocache cache) { final ArrayList<String> infos = new ArrayList<>(); addShortInfos(cache, infos); return StringUtils.join(infos, Formatter.SEPARATOR); } - private static void addShortInfos(Geocache cache, final ArrayList<String> infos) { + private static void addShortInfos(final Geocache cache, final ArrayList<String> infos) { if (cache.hasDifficulty()) { - infos.add("D " + String.format("%.1f", cache.getDifficulty())); + infos.add("D " + formatDT(cache.getDifficulty())); } if (cache.hasTerrain()) { - infos.add("T " + String.format("%.1f", cache.getTerrain())); + infos.add("T " + formatDT(cache.getTerrain())); } // don't show "not chosen" for events and virtuals, that should be the normal case @@ -157,12 +162,16 @@ public abstract class Formatter { } else if (cache.isEventCache()) { final Date hiddenDate = cache.getHiddenDate(); if (hiddenDate != null) { - infos.add(Formatter.formatShortDate(hiddenDate.getTime())); + infos.add(Formatter.formatShortDateIncludingWeekday(hiddenDate.getTime())); } } } - public static String formatCacheInfoHistory(Geocache cache) { + private static String formatDT(final float value) { + return String.format(Locale.getDefault(), "%.1f", value); + } + + public static String formatCacheInfoHistory(final Geocache cache) { final ArrayList<String> infos = new ArrayList<>(3); infos.add(StringUtils.upperCase(cache.getGeocode())); infos.add(Formatter.formatDate(cache.getVisitedDate())); @@ -170,9 +179,9 @@ public abstract class Formatter { return StringUtils.join(infos, Formatter.SEPARATOR); } - public static String formatWaypointInfo(Waypoint waypoint) { + public static String formatWaypointInfo(final Waypoint waypoint) { final List<String> infos = new ArrayList<>(3); - WaypointType waypointType = waypoint.getWaypointType(); + final WaypointType waypointType = waypoint.getWaypointType(); if (waypointType != WaypointType.OWN && waypointType != null) { infos.add(waypointType.getL10n()); } @@ -188,4 +197,42 @@ public abstract class Formatter { } return StringUtils.join(infos, Formatter.SEPARATOR); } + + public static String formatDaysAgo(final long date) { + final int days = CalendarUtils.daysSince(date); + switch (days) { + case 0: + return CgeoApplication.getInstance().getString(R.string.log_today); + case 1: + return CgeoApplication.getInstance().getString(R.string.log_yesterday); + default: + return CgeoApplication.getInstance().getResources().getQuantityString(R.plurals.days_ago, days, days); + } + } + + /** + * Formatting of the hidden date of a cache + * + * @return {@code null} or hidden date of the cache (or event date of the cache) in human readable format + */ + public static String formatHiddenDate(final Geocache cache) { + final Date hiddenDate = cache.getHiddenDate(); + if (hiddenDate == null) { + return null; + } + final long time = hiddenDate.getTime(); + if (time <= 0) { + return null; + } + String dateString = Formatter.formatFullDate(time); + if (cache.isEventCache()) { + dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString; + } + return dateString; + } + + public static String formatMapSubtitle(final Geocache cache) { + return "D " + formatDT(cache.getDifficulty()) + SEPARATOR + "T " + formatDT(cache.getTerrain()) + SEPARATOR + cache.getGeocode(); + } + } diff --git a/main/src/cgeo/geocaching/utils/HtmlUtils.java b/main/src/cgeo/geocaching/utils/HtmlUtils.java index 51c4d6e..ab6e8fe 100644 --- a/main/src/cgeo/geocaching/utils/HtmlUtils.java +++ b/main/src/cgeo/geocaching/utils/HtmlUtils.java @@ -21,10 +21,8 @@ public final class HtmlUtils { * Extract the text from a HTML based string. This is similar to what HTML.fromHtml(...) does, but this method also * removes the embedded images instead of replacing them by a small rectangular representation character. * - * @param html - * @return */ - public static String extractText(CharSequence html) { + public static String extractText(final CharSequence html) { if (StringUtils.isBlank(html)) { return StringUtils.EMPTY; } @@ -32,13 +30,13 @@ public final class HtmlUtils { // recognize images in textview HTML contents if (html instanceof Spanned) { - Spanned text = (Spanned) html; - Object[] styles = text.getSpans(0, text.length(), Object.class); - ArrayList<Pair<Integer, Integer>> removals = new ArrayList<>(); - for (Object style : styles) { + final Spanned text = (Spanned) html; + final Object[] styles = text.getSpans(0, text.length(), Object.class); + final ArrayList<Pair<Integer, Integer>> removals = new ArrayList<>(); + for (final Object style : styles) { if (style instanceof ImageSpan) { - int start = text.getSpanStart(style); - int end = text.getSpanEnd(style); + final int start = text.getSpanStart(style); + final int end = text.getSpanEnd(style); removals.add(Pair.of(start, end)); } } @@ -47,12 +45,12 @@ public final class HtmlUtils { Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() { @Override - public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) { + public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) { return rhs.getRight().compareTo(lhs.getRight()); } }); result = text.toString(); - for (Pair<Integer, Integer> removal : removals) { + for (final Pair<Integer, Integer> removal : removals) { result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight()); } } @@ -60,4 +58,15 @@ public final class HtmlUtils { // now that images are gone, do a normal html to text conversion return Html.fromHtml(result).toString().trim(); } + + public static String removeExtraParagraph(final String htmlIn) { + final String html = StringUtils.trim(htmlIn); + if (StringUtils.startsWith(html, "<p>") && StringUtils.endsWith(html, "</p>")) { + final String paragraph = StringUtils.substring(html, "<p>".length(), html.length() - "</p>".length()).trim(); + if (extractText(paragraph).equals(paragraph)) { + return paragraph; + } + } + return html; + } } diff --git a/main/src/cgeo/geocaching/utils/ImageUtils.java b/main/src/cgeo/geocaching/utils/ImageUtils.java index 739ecc4..71d5e39 100644 --- a/main/src/cgeo/geocaching/utils/ImageUtils.java +++ b/main/src/cgeo/geocaching/utils/ImageUtils.java @@ -1,16 +1,20 @@ package cgeo.geocaching.utils; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.Image; import cgeo.geocaching.R; import cgeo.geocaching.compatibility.Compatibility; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import rx.Observable; +import rx.Scheduler.Worker; import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action0; import rx.functions.Action1; import android.content.res.Resources; @@ -25,6 +29,8 @@ import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.net.Uri; import android.os.Environment; +import android.text.Html; +import android.text.Html.ImageGetter; import android.util.Base64; import android.util.Base64InputStream; import android.widget.TextView; @@ -36,8 +42,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Locale; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; public final class ImageUtils { private static final int[] ORIENTATIONS = new int[] { @@ -49,6 +61,10 @@ public final class ImageUtils { private static final int[] ROTATION = new int[] { 90, 180, 270 }; private static final int MAX_DISPLAY_IMAGE_XY = 800; + // Images whose URL contains one of those patterns will not be available on the Images tab + // for opening into an external application. + private final static String[] NO_EXTERNAL = new String[] { "geocheck.org" }; + private ImageUtils() { // Do not let this class be instantiated, this is a utility class. } @@ -61,7 +77,7 @@ public final class ImageUtils { * @return BitmapDrawable The scaled image */ public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) { - Point displaySize = Compatibility.getDisplaySize(); + final Point displaySize = Compatibility.getDisplaySize(); final int maxWidth = displaySize.x - 25; final int maxHeight = displaySize.y - 25; return scaleBitmapTo(image, maxWidth, maxHeight); @@ -76,7 +92,7 @@ public final class ImageUtils { */ @Nullable public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) { - Point displaySize = Compatibility.getDisplaySize(); + final Point displaySize = Compatibility.getDisplaySize(); // Restrict image size to 800 x 800 to prevent OOM on tablets final int maxWidth = Math.min(displaySize.x - 25, MAX_DISPLAY_IMAGE_XY); final int maxHeight = Math.min(displaySize.y - 25, MAX_DISPLAY_IMAGE_XY); @@ -128,12 +144,12 @@ public final class ImageUtils { */ public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) { try { - FileOutputStream out = new FileOutputStream(pathOfOutputImage); - BufferedOutputStream bos = new BufferedOutputStream(out); + final FileOutputStream out = new FileOutputStream(pathOfOutputImage); + final BufferedOutputStream bos = new BufferedOutputStream(out); bitmap.compress(format, quality, bos); bos.flush(); bos.close(); - } catch (IOException e) { + } catch (final IOException e) { Log.e("ImageHelper.storeBitmap", e); } } @@ -152,7 +168,7 @@ public final class ImageUtils { if (maxXY <= 0) { return filePath; } - Bitmap image = readDownsampledImage(filePath, maxXY, maxXY); + final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY); if (image == null) { return null; } @@ -184,7 +200,7 @@ public final class ImageUtils { try { final ExifInterface exif = new ExifInterface(filePath); orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - } catch (IOException e) { + } catch (final IOException e) { Log.e("ImageUtils.readDownsampledImage", e); } final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options(); @@ -233,7 +249,7 @@ public final class ImageUtils { } // Create a media file name - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg"); } @@ -254,7 +270,7 @@ public final class ImageUtils { * @return <tt>true</tt> if the URL contains at least one of the patterns, <tt>false</tt> otherwise */ public static boolean containsPattern(final String url, final String[] patterns) { - for (String entry : patterns) { + for (final String entry : patterns) { if (StringUtils.containsIgnoreCase(url, entry)) { return true; } @@ -282,7 +298,7 @@ public final class ImageUtils { /** * Decode a base64-encoded string and save the result into a stream. - * + * * @param inString * the encoded string * @param out @@ -303,42 +319,158 @@ public final class ImageUtils { } /** + * Add images present in the HTML description to the existing collection. + * @param images a collection of images + * @param geocode the common title for images in the description + * @param htmlText the HTML description to be parsed, can be repeated + */ + public static void addImagesFromHtml(final Collection<Image> images, final String geocode, final String... htmlText) { + final Set<String> urls = new LinkedHashSet<>(); + for (final Image image : images) { + urls.add(image.getUrl()); + } + for (final String text: htmlText) { + Html.fromHtml(StringUtils.defaultString(text), new ImageGetter() { + @Override + public Drawable getDrawable(final String source) { + if (!urls.contains(source) && canBeOpenedExternally(source)) { + images.add(new Image(source, StringUtils.defaultString(geocode))); + urls.add(source); + } + return null; + } + }, null); + } + } + + /** * Container which can hold a drawable (initially an empty one) and get a newer version when it * becomes available. It also invalidates the view the container belongs to, so that it is * redrawn properly. + * <p/> + * When a new version of the drawable is available, it is put into a queue and, if needed (no other elements + * waiting in the queue), a refresh is launched on the UI thread. This refresh will empty the queue (including + * elements arrived in the meantime) and ensures that the view is uploaded only once all the queued requests have + * been handled. */ - @SuppressWarnings("deprecation") - public final static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> { + public static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> { + final private static Object lock = new Object(); // Used to lock the queue to determine if a refresh needs to be scheduled + final private static LinkedBlockingQueue<ImmutablePair<ContainerDrawable, Drawable>> REDRAW_QUEUE = new LinkedBlockingQueue<>(); + final private static Set<TextView> VIEWS = new HashSet<>(); // Modified only on the UI thread + final private static Worker UI_WORKER = AndroidSchedulers.mainThread().createWorker(); + final private static Action0 REDRAW_QUEUED_DRAWABLES = new Action0() { + @Override + public void call() { + redrawQueuedDrawables(); + } + }; + private Drawable drawable; - final private TextView view; + final protected TextView view; - public ContainerDrawable(@NonNull final TextView view) { + @SuppressWarnings("deprecation") + public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { this.view = view; drawable = null; setBounds(0, 0, 0, 0); - } - - public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { - this(view); - updateFrom(drawableObservable); + drawableObservable.subscribe(this); } @Override - public void draw(final Canvas canvas) { + public final void draw(final Canvas canvas) { if (drawable != null) { drawable.draw(canvas); } } @Override - public void call(final Drawable newDrawable) { + public final void call(final Drawable newDrawable) { + final boolean needsRedraw; + synchronized (lock) { + // Check for emptyness inside the call to match the behaviour in redrawQueuedDrawables(). + needsRedraw = REDRAW_QUEUE.isEmpty(); + REDRAW_QUEUE.add(ImmutablePair.of(this, newDrawable)); + } + if (needsRedraw) { + UI_WORKER.schedule(REDRAW_QUEUED_DRAWABLES); + } + } + + /** + * Update the container with the new drawable. Called on the UI thread. + * + * @param newDrawable the new drawable + * @return the view to update + */ + protected TextView updateDrawable(final Drawable newDrawable) { setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight()); drawable = newDrawable; - view.setText(view.getText()); + return view; + } + + private static void redrawQueuedDrawables() { + if (!REDRAW_QUEUE.isEmpty()) { + // Add a small margin so that drawables arriving between the beginning of the allocation and the draining + // of the queue might be absorbed without reallocation. + final ArrayList<ImmutablePair<ContainerDrawable, Drawable>> toRedraw = new ArrayList<>(REDRAW_QUEUE.size() + 16); + synchronized (lock) { + // Empty the queue inside the lock to match the check done in call(). + REDRAW_QUEUE.drainTo(toRedraw); + } + for (final ImmutablePair<ContainerDrawable, Drawable> redrawable : toRedraw) { + VIEWS.add(redrawable.left.updateDrawable(redrawable.right)); + } + for (final TextView view : VIEWS) { + view.setText(view.getText()); + } + VIEWS.clear(); + } + } + + } + + /** + * Image that automatically scales to fit a line of text in the containing {@link TextView}. + */ + public final static class LineHeightContainerDrawable extends ContainerDrawable { + public LineHeightContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { + super(view, drawableObservable); } - public void updateFrom(final Observable<? extends Drawable> drawableObservable) { - drawableObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(this); + @Override + protected TextView updateDrawable(final Drawable newDrawable) { + super.updateDrawable(newDrawable); + setBounds(ImageUtils.scaleImageToLineHeight(newDrawable, view)); + return view; } } + + public static boolean canBeOpenedExternally(final String source) { + return !containsPattern(source, NO_EXTERNAL); + } + + public static Rect scaleImageToLineHeight(final Drawable drawable, final TextView view) { + final int lineHeight = (int) (view.getLineHeight() * 0.8); + final int width = drawable.getIntrinsicWidth() * lineHeight / drawable.getIntrinsicHeight(); + return new Rect(0, 0, width, lineHeight); + } + + public static Bitmap convertToBitmap(final Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + // handle solid colors, which have no width + int width = drawable.getIntrinsicWidth(); + width = width > 0 ? width : 1; + int height = drawable.getIntrinsicHeight(); + height = height > 0 ? height : 1; + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } } diff --git a/main/src/cgeo/geocaching/utils/JsonUtils.java b/main/src/cgeo/geocaching/utils/JsonUtils.java new file mode 100644 index 0000000..492e137 --- /dev/null +++ b/main/src/cgeo/geocaching/utils/JsonUtils.java @@ -0,0 +1,20 @@ +package cgeo.geocaching.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + +public class JsonUtils { + + private static final ObjectMapper mapper = new ObjectMapper(); + public static final ObjectReader reader = mapper.reader(); + public static final ObjectWriter writer = mapper.writer(); + + public static final JsonNodeFactory factory = new JsonNodeFactory(true); + + private JsonUtils() { + // Do not instantiate + } + +} diff --git a/main/src/cgeo/geocaching/utils/LazyInitializedList.java b/main/src/cgeo/geocaching/utils/LazyInitializedList.java index b0e2e46..866acad 100644 --- a/main/src/cgeo/geocaching/utils/LazyInitializedList.java +++ b/main/src/cgeo/geocaching/utils/LazyInitializedList.java @@ -49,7 +49,7 @@ public abstract class LazyInitializedList<ElementType> extends AbstractList<Elem } @Override - public void add(int index, final ElementType element) { + public void add(final int index, final ElementType element) { getUnderlyingList().add(index, element); } diff --git a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java index 6122532..aecfaf1 100644 --- a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java +++ b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedMap.java @@ -30,7 +30,7 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { final int initialCapacity; final float loadFactor; - protected LeastRecentlyUsedMap(int maxEntries, int initialCapacity, float loadFactor, OperationModes opMode) { + protected LeastRecentlyUsedMap(final int maxEntries, final int initialCapacity, final float loadFactor, final OperationModes opMode) { super(initialCapacity, loadFactor, (opMode==OperationModes.LRU_CACHE)); this.initialCapacity = initialCapacity; this.loadFactor = loadFactor; @@ -38,12 +38,12 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { this.opMode = opMode; } - protected LeastRecentlyUsedMap(int maxEntries, OperationModes opMode) { + protected LeastRecentlyUsedMap(final int maxEntries, final OperationModes opMode) { this(maxEntries, 16, 0.75f, opMode); } @Override - public V put(K key, V value) { + public V put(final K key, final V value) { // in case the underlying Map is not running with accessOrder==true, the map won't notice any changes // of existing keys, so for the normal BOUNDED mode we remove and put the value to get its order updated. if (opMode == OperationModes.BOUNDED && containsKey(key)) { @@ -57,7 +57,7 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { } @Override - protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) { return size() > maxEntries; } @@ -66,9 +66,9 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { } @Override - public V remove(Object key) { + public V remove(final Object key) { - V removed = super.remove(key); + final V removed = super.remove(key); if (removed != null && removeHandler != null) { removeHandler.onRemove(removed); @@ -84,18 +84,18 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { * @param removeHandler * The new handler to receive notifications or null to remove a handler */ - public void setRemoveHandler(RemoveHandler<V> removeHandler) { + public void setRemoveHandler(final RemoveHandler<V> removeHandler) { this.removeHandler = removeHandler; } public static class LruCache<K, V> extends LeastRecentlyUsedMap<K, V> { private static final long serialVersionUID = 9028478916221334454L; - public LruCache(int maxEntries, int initialCapacity, float loadFactor) { + public LruCache(final int maxEntries, final int initialCapacity, final float loadFactor) { super(maxEntries, initialCapacity, loadFactor, OperationModes.LRU_CACHE); } - public LruCache(int maxEntries) { + public LruCache(final int maxEntries) { super(maxEntries, OperationModes.LRU_CACHE); } } @@ -104,11 +104,11 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { private static final long serialVersionUID = -1476389304214398315L; - public Bounded(int maxEntries, int initialCapacity, float loadFactor) { + public Bounded(final int maxEntries, final int initialCapacity, final float loadFactor) { super(maxEntries, initialCapacity, loadFactor, OperationModes.BOUNDED); } - public Bounded(int maxEntries) { + public Bounded(final int maxEntries) { super(maxEntries, OperationModes.BOUNDED); } } @@ -117,7 +117,6 @@ public abstract class LeastRecentlyUsedMap<K, V> extends LinkedHashMap<K, V> { * Interface for handlers that wish to get notified when items are * removed from the LRUMap * - * @param <V> */ public interface RemoveHandler<V> { diff --git a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java index a69f427..c139136 100644 --- a/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java +++ b/main/src/cgeo/geocaching/utils/LeastRecentlyUsedSet.java @@ -20,21 +20,18 @@ import java.util.List; * access has to be guarded externally or the synchronized getAsList method can be used * to get a clone for iteration. */ -public class LeastRecentlyUsedSet<E> extends AbstractSet<E> - implements Cloneable, java.io.Serializable { +public class LeastRecentlyUsedSet<E> extends AbstractSet<E> { - private static final long serialVersionUID = -1942301031191419547L; - - private transient LeastRecentlyUsedMap<E, Object> map; + private final LeastRecentlyUsedMap<E, Object> map; private static final Object PRESENT = new Object(); - public LeastRecentlyUsedSet(int maxEntries, int initialCapacity, float loadFactor) { + public LeastRecentlyUsedSet(final int maxEntries, final int initialCapacity, final float loadFactor) { // because we don't use any Map.get() methods from the Set, BOUNDED and LRU_CACHE have the exact same Behaviour // So we use LRU_CACHE mode because it should perform a bit better (as it doesn't re-add explicitly) map = new LeastRecentlyUsedMap.LruCache<>(maxEntries, initialCapacity, loadFactor); } - public LeastRecentlyUsedSet(int maxEntries) { + public LeastRecentlyUsedSet(final int maxEntries) { map = new LeastRecentlyUsedMap.LruCache<>(maxEntries); } @@ -79,7 +76,7 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E> * @see HashSet */ @Override - public synchronized boolean contains(Object o) { + public synchronized boolean contains(final Object o) { return map.containsKey(o); } @@ -90,7 +87,7 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E> * @see HashSet */ @Override - public synchronized boolean add(E e) { + public synchronized boolean add(final E e) { if (e == null) { throw new IllegalArgumentException("LeastRecentlyUsedSet cannot take null element"); } @@ -104,7 +101,7 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E> * @see HashSet */ @Override - public synchronized boolean remove(Object o) { + public synchronized boolean remove(final Object o) { return map.remove(o) == PRESENT; } @@ -132,26 +129,6 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E> } /** - * (synchronized) Clone of the set - * Copy of the HashSet code if clone() - * - * @see HashSet - */ - @Override - @SuppressWarnings("unchecked") - public Object clone() throws CloneNotSupportedException { - try { - synchronized (this) { - final LeastRecentlyUsedSet<E> newSet = (LeastRecentlyUsedSet<E>) super.clone(); - newSet.map = (LeastRecentlyUsedMap<E, Object>) map.clone(); - return newSet; - } - } catch (CloneNotSupportedException e) { - throw new InternalError(); - } - } - - /** * Creates a clone as a list in a synchronized fashion. * * @return List based clone of the set @@ -160,56 +137,4 @@ public class LeastRecentlyUsedSet<E> extends AbstractSet<E> return new ArrayList<>(this); } - /** - * Serialization version of HashSet with the additional parameters for the custom Map - * - * @see HashSet - */ - private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException { - // Write out any hidden serialization magic - s.defaultWriteObject(); - - // Write out HashMap capacity and load factor - s.writeInt(map.initialCapacity); - s.writeFloat(map.loadFactor); - s.writeInt(map.getMaxEntries()); - - // Write out size - s.writeInt(map.size()); - - // Write out all elements in the proper order. - for (final E e : map.keySet()) { - s.writeObject(e); - } - } - - /** - * Serialization version of HashSet with the additional parameters for the custom Map - * - * @see HashSet - */ - @SuppressWarnings("unchecked") - private void readObject(java.io.ObjectInputStream s) - throws java.io.IOException, ClassNotFoundException { - // Read in any hidden serialization magic - s.defaultReadObject(); - - // Read in HashMap capacity and load factor and create backing HashMap - final int capacity = s.readInt(); - final float loadFactor = s.readFloat(); - final int maxEntries = s.readInt(); - - map = new LeastRecentlyUsedMap.LruCache<>(maxEntries, capacity, loadFactor); - - // Read in size - final int size = s.readInt(); - - // Read in all elements in the proper order. - for (int i = 0; i < size; i++) { - E e = (E) s.readObject(); - map.put(e, PRESENT); - } - } - } diff --git a/main/src/cgeo/geocaching/utils/Log.java b/main/src/cgeo/geocaching/utils/Log.java index f338a8e..861faaa 100644 --- a/main/src/cgeo/geocaching/utils/Log.java +++ b/main/src/cgeo/geocaching/utils/Log.java @@ -37,62 +37,65 @@ public final class Log { /** * Save a copy of the debug flag from the settings for performance reasons. * - * @param isDebug */ public static void setDebug(final boolean isDebug) { Log.isDebug = isDebug; } + private static String addThreadInfo(final String msg) { + return new StringBuilder("[").append(Thread.currentThread().getName()).append("] ").append(msg).toString(); + } + public static void v(final String msg) { if (isDebug) { - android.util.Log.v(TAG, msg); + android.util.Log.v(TAG, addThreadInfo(msg)); } } public static void v(final String msg, final Throwable t) { if (isDebug) { - android.util.Log.v(TAG, msg, t); + android.util.Log.v(TAG, addThreadInfo(msg), t); } } public static void d(final String msg) { if (isDebug) { - android.util.Log.d(TAG, msg); + android.util.Log.d(TAG, addThreadInfo(msg)); } } public static void d(final String msg, final Throwable t) { if (isDebug) { - android.util.Log.d(TAG, msg, t); + android.util.Log.d(TAG, addThreadInfo(msg), t); } } public static void i(final String msg) { if (isDebug) { - android.util.Log.i(TAG, msg); + android.util.Log.i(TAG, addThreadInfo(msg)); } } public static void i(final String msg, final Throwable t) { if (isDebug) { - android.util.Log.i(TAG, msg, t); + android.util.Log.i(TAG, addThreadInfo(msg), t); } } public static void w(final String msg) { - android.util.Log.w(TAG, msg); + android.util.Log.w(TAG, addThreadInfo(msg)); } public static void w(final String msg, final Throwable t) { - android.util.Log.w(TAG, msg, t); + android.util.Log.w(TAG, addThreadInfo(msg), t); } public static void e(final String msg) { - android.util.Log.e(TAG, msg); + android.util.Log.e(TAG, addThreadInfo(msg)); } public static void e(final String msg, final Throwable t) { - android.util.Log.e(TAG, msg, t); + android.util.Log.e(TAG, addThreadInfo(msg), t); } /** @@ -116,7 +119,7 @@ public final class Log { Writer writer = null; try { writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), CharEncoding.UTF_8)); - writer.write(msg); + writer.write(addThreadInfo(msg)); } catch (final IOException e) { Log.e("logToFile: cannot write to " + file, e); } finally { diff --git a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java index ff4013c..1db3d5b 100644 --- a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java +++ b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java @@ -215,7 +215,7 @@ public final class LogTemplateProvider { } final Geocache cache = context.getCache(); if (cache != null) { - return cache.getUrl(); + return StringUtils.defaultString(cache.getUrl()); } return StringUtils.EMPTY; } diff --git a/main/src/cgeo/geocaching/utils/MapUtils.java b/main/src/cgeo/geocaching/utils/MapUtils.java index 5120ca5..f41247c 100644 --- a/main/src/cgeo/geocaching/utils/MapUtils.java +++ b/main/src/cgeo/geocaching/utils/MapUtils.java @@ -154,7 +154,6 @@ public final class MapUtils { } private static int calculateResolution(final Drawable marker) { - final int resolution = marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0; - return resolution; + return marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0; } } diff --git a/main/src/cgeo/geocaching/utils/MatcherWrapper.java b/main/src/cgeo/geocaching/utils/MatcherWrapper.java index c99d3c4..733a18e 100644 --- a/main/src/cgeo/geocaching/utils/MatcherWrapper.java +++ b/main/src/cgeo/geocaching/utils/MatcherWrapper.java @@ -2,6 +2,8 @@ package cgeo.geocaching.utils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.eclipse.jdt.annotation.NonNull; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -13,7 +15,7 @@ import java.util.regex.Pattern; public class MatcherWrapper { private final Matcher matcher; - public MatcherWrapper(Pattern pattern, String input) { + public MatcherWrapper(@NonNull final Pattern pattern, @NonNull final String input) { this.matcher = pattern.matcher(input); } @@ -24,14 +26,14 @@ public class MatcherWrapper { return matcher.find(); } - public boolean find(int start) { + public boolean find(final int start) { return matcher.find(start); } /** * see {@link Matcher#group(int)} */ - public String group(int index) { + public String group(final int index) { return newString(matcher.group(index)); } @@ -43,11 +45,9 @@ public class MatcherWrapper { * <p> * Do not change this method, even if Findbugs and other tools will report a violation for that line! * - * @param input - * @return */ @SuppressFBWarnings("DM_STRING_CTOR") - private static String newString(String input) { + private static String newString(final String input) { if (input == null) { return null; } @@ -78,7 +78,7 @@ public class MatcherWrapper { /** * see {@link Matcher#replaceAll(String)} */ - public String replaceAll(String replacement) { + public String replaceAll(final String replacement) { return newString(matcher.replaceAll(replacement)); } @@ -92,7 +92,7 @@ public class MatcherWrapper { /** * see {@link Matcher#replaceFirst(String)} */ - public String replaceFirst(String replacement) { + public String replaceFirst(final String replacement) { return newString(matcher.replaceFirst(replacement)); } } diff --git a/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java b/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java index 1401542..0c6365c 100644 --- a/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java +++ b/main/src/cgeo/geocaching/utils/OOMDumpingUncaughtExceptionHandler.java @@ -11,14 +11,12 @@ public class OOMDumpingUncaughtExceptionHandler implements UncaughtExceptionHand private boolean defaultReplaced = false; public static boolean activateHandler() { - final OOMDumpingUncaughtExceptionHandler handler = new OOMDumpingUncaughtExceptionHandler(); return handler.activate(); } private boolean activate() { - defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); // replace default handler if that has not been done already @@ -34,10 +32,8 @@ public class OOMDumpingUncaughtExceptionHandler implements UncaughtExceptionHand } public static boolean resetToDefault() { - - boolean defaultResetted = false; - final UncaughtExceptionHandler unspecificHandler = Thread.getDefaultUncaughtExceptionHandler(); + boolean defaultResetted = unspecificHandler != null; if (unspecificHandler instanceof OOMDumpingUncaughtExceptionHandler) { final OOMDumpingUncaughtExceptionHandler handler = (OOMDumpingUncaughtExceptionHandler) unspecificHandler; @@ -48,7 +44,6 @@ public class OOMDumpingUncaughtExceptionHandler implements UncaughtExceptionHand } private boolean reset() { - final boolean resetted = defaultReplaced; if (defaultReplaced) { diff --git a/main/src/cgeo/geocaching/utils/ProcessUtils.java b/main/src/cgeo/geocaching/utils/ProcessUtils.java index d80674b..6a57cbf 100644 --- a/main/src/cgeo/geocaching/utils/ProcessUtils.java +++ b/main/src/cgeo/geocaching/utils/ProcessUtils.java @@ -3,7 +3,10 @@ package cgeo.geocaching.utils; import cgeo.geocaching.CgeoApplication; import org.apache.commons.collections4.CollectionUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import android.app.Activity; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -21,10 +24,8 @@ public final class ProcessUtils { /** * Preferred method to detect the availability of an external app * - * @param packageName - * @return */ - public static boolean isLaunchable(final String packageName) { + public static boolean isLaunchable(@Nullable final String packageName) { return getLaunchIntent(packageName) != null; } @@ -33,17 +34,15 @@ public final class ProcessUtils { * This function is relatively costly, so if you know that the package in question has * a launch intent, use isLaunchable() instead. * - * @param packageName - * @return */ - public static boolean isInstalled(final String packageName) { + public static boolean isInstalled(@NonNull final String packageName) { return isLaunchable(packageName) || hasPackageInstalled(packageName); } /** * This will find installed applications even without launch intent (e.g. the streetview plugin). */ - private static boolean hasPackageInstalled(final String packageName) { + private static boolean hasPackageInstalled(@NonNull final String packageName) { final List<PackageInfo> packs = CgeoApplication.getInstance().getPackageManager().getInstalledPackages(0); for (final PackageInfo packageInfo : packs) { if (packageName.equals(packageInfo.packageName)) { @@ -56,7 +55,8 @@ public final class ProcessUtils { /** * This will find applications, which can be launched. */ - public static Intent getLaunchIntent(final String packageName) { + @Nullable + public static Intent getLaunchIntent(@Nullable final String packageName) { if (packageName == null) { return null; } @@ -65,12 +65,12 @@ public final class ProcessUtils { // This can throw an exception where the exception type is only defined on API Level > 3 // therefore surround with try-catch return packageManager.getLaunchIntentForPackage(packageName); - } catch (final Exception e) { + } catch (final Exception ignored) { return null; } } - public static boolean isIntentAvailable(final String intent) { + public static boolean isIntentAvailable(@NonNull final String intent) { return isIntentAvailable(intent, null); } @@ -79,16 +79,16 @@ public final class ProcessUtils { * method queries the package manager for installed packages that can * respond to an intent with the specified action. If no suitable package is * found, this method returns false. - * + * * @param action * The Intent action to check for availability. * @param uri * The Intent URI to check for availability. - * + * * @return True if an Intent with the specified action can be sent and * responded to, false otherwise. */ - public static boolean isIntentAvailable(final String action, final Uri uri) { + public static boolean isIntentAvailable(@NonNull final String action, @Nullable final Uri uri) { final PackageManager packageManager = CgeoApplication.getInstance().getPackageManager(); final Intent intent; if (uri == null) { @@ -98,7 +98,23 @@ public final class ProcessUtils { } final List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - return CollectionUtils.isNotEmpty(list); + final List<ResolveInfo> servicesList = packageManager.queryIntentServices(intent, + PackageManager.MATCH_DEFAULT_ONLY); + return CollectionUtils.isNotEmpty(list) || CollectionUtils.isNotEmpty(servicesList); + } + + @SuppressWarnings("deprecation") + public static void openMarket(final Activity activity, @NonNull final String packageName) { + try { + final String url = "market://details?id=" + packageName; + final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + marketIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activity.startActivity(marketIntent); + + } catch (final RuntimeException ignored) { + // market not available, fall back to browser + activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=" + packageName))); + } } } diff --git a/main/src/cgeo/geocaching/utils/RxUtils.java b/main/src/cgeo/geocaching/utils/RxUtils.java index 241ba78..08cc3e7 100644 --- a/main/src/cgeo/geocaching/utils/RxUtils.java +++ b/main/src/cgeo/geocaching/utils/RxUtils.java @@ -1,22 +1,56 @@ package cgeo.geocaching.utils; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Scheduler; +import rx.Scheduler.Worker; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func0; +import rx.functions.Func1; +import rx.internal.util.RxThreadFactory; import rx.observables.BlockingObservable; +import rx.observers.Subscribers; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; +import rx.subscriptions.Subscriptions; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; public class RxUtils { - // Utility class, not to be instanciated - private RxUtils() {} + private RxUtils() { + // Utility class, not to be instantiated + } public final static Scheduler computationScheduler = Schedulers.computation(); - public static final Scheduler networkScheduler = Schedulers.from(new ThreadPoolExecutor(10, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>())); + public static final Scheduler networkScheduler = Schedulers.from(Executors.newFixedThreadPool(10, new RxThreadFactory("network-"))); + + public static final Scheduler refreshScheduler = Schedulers.from(Executors.newFixedThreadPool(3, new RxThreadFactory("refresh-"))); + + private static final HandlerThread looperCallbacksThread = + new HandlerThread("looper callbacks", Process.THREAD_PRIORITY_DEFAULT); + + static { + looperCallbacksThread.start(); + } + + public static final Looper looperCallbacksLooper = looperCallbacksThread.getLooper(); + public static final Scheduler looperCallbacksScheduler = AndroidSchedulers.handlerThread(new Handler(looperCallbacksLooper)); + public static final Worker looperCallbacksWorker = looperCallbacksScheduler.createWorker(); public static <T> void waitForCompletion(final BlockingObservable<T> observable) { observable.lastOrDefault(null); @@ -25,4 +59,139 @@ public class RxUtils { public static void waitForCompletion(final Observable<?>... observables) { waitForCompletion(Observable.merge(observables).toBlocking()); } + + /** + * Subscribe function whose subscription and unsubscription take place on a looper thread. + * + * @param <T> + * the type of the observable + */ + public static abstract class LooperCallbacks<T> implements OnSubscribe<T> { + + final AtomicInteger counter = new AtomicInteger(0); + final long stopDelay; + final TimeUnit stopDelayUnit; + final protected PublishSubject<T> subject = PublishSubject.create(); + + public LooperCallbacks(final long stopDelay, final TimeUnit stopDelayUnit) { + this.stopDelay = stopDelay; + this.stopDelayUnit = stopDelayUnit; + } + + public LooperCallbacks() { + this(0, TimeUnit.SECONDS); + } + + @Override + final public void call(final Subscriber<? super T> subscriber) { + subscriber.add(subject.subscribe(Subscribers.from(subscriber))); + looperCallbacksWorker.schedule(new Action0() { + @Override + public void call() { + if (counter.getAndIncrement() == 0) { + onStart(); + } + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + looperCallbacksWorker.schedule(new Action0() { + @Override + public void call() { + if (counter.decrementAndGet() == 0) { + onStop(); + } + } + }, stopDelay, stopDelayUnit); + } + })); + } + }); + } + + abstract protected void onStart(); + + abstract protected void onStop(); + } + + public static<T> Observable<T> rememberLast(final Observable<T> observable, final T initialValue) { + final AtomicReference<T> lastValue = new AtomicReference<>(initialValue); + return observable.doOnNext(new Action1<T>() { + @Override + public void call(final T value) { + lastValue.set(value); + } + }).startWith(Observable.defer(new Func0<Observable<T>>() { + @Override + public Observable<T> call() { + final T last = lastValue.get(); + return last != null ? Observable.just(last) : Observable.<T>empty(); + } + })).replay(1).refCount(); + } + + public static <T> void andThenOnUi(final Scheduler scheduler, final Func0<T> background, final Action1<T> foreground) { + scheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + final T value = background.call(); + AndroidSchedulers.mainThread().createWorker().schedule(new Action0() { + @Override + public void call() { + foreground.call(value); + } + }); + } + }); + } + + public static void andThenOnUi(final Scheduler scheduler, final Action0 background, final Action0 foreground) { + scheduler.createWorker().schedule(new Action0() { + @Override + public void call() { + background.call(); + AndroidSchedulers.mainThread().createWorker().schedule(foreground); + } + }); + } + + /** + * Cache the last value of observables so that every key is associated to only one of them. + * + * @param <K> the type of the key + * @param <V> the type of the value + */ + public static class ObservableCache<K, V> { + + final private Func1<K, Observable<V>> func; + final private Map<K, Observable<V>> cached = new HashMap<>(); + + /** + * Create a new observables cache. + * + * @param func the function transforming a key into an observable + */ + public ObservableCache(final Func1<K, Observable<V>> func) { + this.func = func; + } + + /** + * Get the observable corresponding to a key. If the key has not already been + * seen, the function passed to the constructor will be called to build the observable + * <p/> + * If the observable has already emitted values, only the last one will be remembered. + * + * @param key the key + * @return the observable corresponding to the key + */ + public synchronized Observable<V> get(final K key) { + if (cached.containsKey(key)) { + return cached.get(key); + } + final Observable<V> value = func.call(key).replay(1).refCount(); + cached.put(key, value); + return value; + } + + } + } diff --git a/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java b/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java index eee71ba..0743692 100644 --- a/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java +++ b/main/src/cgeo/geocaching/utils/SimpleCancellableHandler.java @@ -5,6 +5,7 @@ import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.activity.Progress; import android.content.res.Resources; +import android.os.Bundle; import android.os.Message; import java.lang.ref.WeakReference; @@ -21,7 +22,7 @@ public class SimpleCancellableHandler extends CancellableHandler { @Override protected void handleRegularMessage(final Message msg) { - AbstractActivity activity = activityRef.get(); + final AbstractActivity activity = activityRef.get(); if (activity != null && msg.getData() != null && msg.getData().getString(MESSAGE_TEXT) != null) { activity.showToast(msg.getData().getString(MESSAGE_TEXT)); } @@ -30,37 +31,37 @@ public class SimpleCancellableHandler extends CancellableHandler { @Override protected void handleCancel(final Object extra) { - AbstractActivity activity = activityRef.get(); + final AbstractActivity activity = activityRef.get(); if (activity != null) { activity.showToast((String) extra); } dismissProgress(); } - protected final void showToast(int resId) { - AbstractActivity activity = activityRef.get(); + protected final void showToast(final int resId) { + final AbstractActivity activity = activityRef.get(); if (activity != null) { - Resources res = activity.getResources(); + final Resources res = activity.getResources(); activity.showToast(res.getText(resId).toString()); } } protected final void dismissProgress() { - Progress progressDialog = progressDialogRef.get(); + final Progress progressDialog = progressDialogRef.get(); if (progressDialog != null) { progressDialog.dismiss(); } } protected final void setProgressMessage(final String txt) { - Progress progressDialog = progressDialogRef.get(); + final Progress progressDialog = progressDialogRef.get(); if (progressDialog != null) { progressDialog.setMessage(txt); } } protected final void finishActivity() { - AbstractActivity activity = activityRef.get(); + final AbstractActivity activity = activityRef.get(); if (activity != null) { activity.finish(); } @@ -68,7 +69,7 @@ public class SimpleCancellableHandler extends CancellableHandler { } protected void updateStatusMsg(final int resId, final String msg) { - CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get()); + final CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get()); if (activity != null) { setProgressMessage(activity.getResources().getString(resId) + "\n\n" @@ -76,4 +77,15 @@ public class SimpleCancellableHandler extends CancellableHandler { } } + public void sendTextMessage(final int what, final int resId) { + final CacheDetailActivity activity = ((CacheDetailActivity) activityRef.get()); + if (activity != null) { + final Message msg = obtainMessage(what); + final Bundle bundle = new Bundle(); + bundle.putString(SimpleCancellableHandler.MESSAGE_TEXT, activity.getResources().getString(resId)); + msg.setData(bundle); + msg.sendToTarget(); + } + } + } diff --git a/main/src/cgeo/geocaching/utils/StartableHandlerThread.java b/main/src/cgeo/geocaching/utils/StartableHandlerThread.java deleted file mode 100644 index 91ab1d0..0000000 --- a/main/src/cgeo/geocaching/utils/StartableHandlerThread.java +++ /dev/null @@ -1,80 +0,0 @@ -package cgeo.geocaching.utils; - -import org.eclipse.jdt.annotation.NonNull; -import rx.Subscriber; -import rx.functions.Action0; -import rx.subscriptions.Subscriptions; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; - -/** - * Derivated class of {@link android.os.HandlerThread} with an exposed handler and a start/stop mechanism - * based on subscriptions. - */ - -public class StartableHandlerThread extends HandlerThread { - - private final static int START = 1; - private final static int STOP = 2; - - static public interface Callback { - public void start(final Context context, final Handler handler); - public void stop(); - } - - // The handler and the thread are intimely linked, there will be no leak. - @SuppressLint("HandlerLeak") - private class StartableHandler extends Handler { - public StartableHandler() { - super(StartableHandlerThread.this.getLooper()); - } - - @Override - public void handleMessage(final Message message) { - if (callback != null) { - switch (message.what) { - case START: - callback.start((Context) message.obj, this); - break; - case STOP: - callback.stop(); - break; - } - } - } - } - - private Handler handler; - private Callback callback; - - public StartableHandlerThread(@NonNull final String name, final int priority, final Callback callback) { - super(name, priority); - this.callback = callback; - } - - public StartableHandlerThread(@NonNull final String name, final int priority) { - this(name, priority, null); - } - - public synchronized Handler getHandler() { - if (handler == null) { - handler = new StartableHandler(); - } - return handler; - } - - public void start(final Subscriber<?> subscriber, final Context context) { - getHandler().obtainMessage(START, context).sendToTarget(); - subscriber.add(Subscriptions.create(new Action0() { - @Override - public void call() { - getHandler().sendEmptyMessage(STOP); - } - })); - } - -} diff --git a/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java b/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java index 7848d1a..5963e2e 100644 --- a/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java +++ b/main/src/cgeo/geocaching/utils/SynchronizedDateFormat.java @@ -13,7 +13,7 @@ public class SynchronizedDateFormat { format = new SimpleDateFormat(pattern, locale); } - public SynchronizedDateFormat(String pattern, TimeZone timeZone, Locale locale) { + public SynchronizedDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) { format = new SimpleDateFormat(pattern, locale); format.setTimeZone(timeZone); } diff --git a/main/src/cgeo/geocaching/utils/TextUtils.java b/main/src/cgeo/geocaching/utils/TextUtils.java index 77aa167..1f14f8d 100644 --- a/main/src/cgeo/geocaching/utils/TextUtils.java +++ b/main/src/cgeo/geocaching/utils/TextUtils.java @@ -8,6 +8,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.eclipse.jdt.annotation.Nullable; import java.nio.charset.Charset; +import java.text.Collator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; @@ -27,11 +28,11 @@ public final class TextUtils { } /** - * Searches for the pattern p in the data. If the pattern is not found defaultValue is returned + * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned * * @param data * Data to search in - * @param p + * @param pattern * Pattern to search for * @param trim * Set to true if the group found should be trim'ed @@ -44,37 +45,38 @@ public final class TextUtils { * @return defaultValue or the n-th group if the pattern matches (trimmed if wanted) */ @SuppressFBWarnings("DM_STRING_CTOR") - public static String getMatch(@Nullable final String data, final Pattern p, final boolean trim, final int group, final String defaultValue, final boolean last) { + public static String getMatch(@Nullable final String data, final Pattern pattern, final boolean trim, final int group, final String defaultValue, final boolean last) { if (data != null) { - - String result = null; - final Matcher matcher = p.matcher(data); - + final Matcher matcher = pattern.matcher(data); if (matcher.find()) { - result = matcher.group(group); - } - if (null != result) { - final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(result); - result = remover.replaceAll(" "); + String result = matcher.group(group); + while (last && matcher.find()) { + result = matcher.group(group); + } - return trim ? new String(result).trim() : new String(result); - // Java copies the whole page String, when matching with regular expressions - // later this would block the garbage collector, as we only need tiny parts of the page - // see http://developer.android.com/reference/java/lang/String.html#backing_array - // Thus the creating of a new String via String constructor is necessary here!! + if (result != null) { + final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(result); + result = remover.replaceAll(" "); - // And BTW: You cannot even see that effect in the debugger, but must use a separate memory profiler! + // Some versions of Java copy the whole page String, when matching with regular expressions + // later this would block the garbage collector, as we only need tiny parts of the page + // see http://developer.android.com/reference/java/lang/String.html#backing_array + // Thus the creating of a new String via String constructor is voluntary here!! + // And BTW: You cannot even see that effect in the debugger, but must use a separate memory profiler! + return trim ? new String(result).trim() : new String(result); + } } } + return defaultValue; } /** - * Searches for the pattern p in the data. If the pattern is not found defaultValue is returned + * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned * * @param data * Data to search in - * @param p + * @param pattern * Pattern to search for * @param trim * Set to true if the group found should be trim'ed @@ -82,38 +84,33 @@ public final class TextUtils { * Value to return if the pattern is not found * @return defaultValue or the first group if the pattern matches (trimmed if wanted) */ - public static String getMatch(final String data, final Pattern p, final boolean trim, final String defaultValue) { - return TextUtils.getMatch(data, p, trim, 1, defaultValue, false); + public static String getMatch(final String data, final Pattern pattern, final boolean trim, final String defaultValue) { + return TextUtils.getMatch(data, pattern, trim, 1, defaultValue, false); } /** - * Searches for the pattern p in the data. If the pattern is not found defaultValue is returned + * Searches for the pattern pattern in the data. If the pattern is not found defaultValue is returned * * @param data * Data to search in - * @param p + * @param pattern * Pattern to search for * @param defaultValue * Value to return if the pattern is not found * @return defaultValue or the first group if the pattern matches (trimmed) */ - public static String getMatch(@Nullable final String data, final Pattern p, final String defaultValue) { - return TextUtils.getMatch(data, p, true, 1, defaultValue, false); + public static String getMatch(@Nullable final String data, final Pattern pattern, final String defaultValue) { + return TextUtils.getMatch(data, pattern, true, 1, defaultValue, false); } /** - * Searches for the pattern p in the data. + * Searches for the pattern pattern in the data. * - * @param data - * @param p - * @return true if data contains the pattern p + * @return true if data contains the pattern pattern */ - public static boolean matches(final String data, final Pattern p) { - if (data == null) { - return false; - } + public static boolean matches(final String data, final Pattern pattern) { // matcher is faster than String.contains() and more flexible - it takes patterns instead of fixed texts - return p.matcher(data).find(); + return data != null && pattern.matcher(data).find(); } @@ -165,8 +162,6 @@ public final class TextUtils { * Remove all control characters (which are not valid in XML or HTML), as those should not appear in cache texts * anyway * - * @param input - * @return */ public static String removeControlCharacters(final String input) { final Matcher remover = PATTERN_REMOVE_NONPRINTABLE.matcher(input); @@ -182,7 +177,19 @@ public final class TextUtils { */ public static long checksum(final String input) { final CRC32 checksum = new CRC32(); - checksum.update(input.getBytes()); + checksum.update(input.getBytes(CHARSET_UTF8)); return checksum.getValue(); } + + /** + * Build a Collator instance appropriate for comparing strings using the default locale while ignoring the casing. + * + * @return a collator + */ + public static Collator getCollator() { + final Collator collator = Collator.getInstance(); + collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION); + collator.setStrength(Collator.TERTIARY); + return collator; + } } diff --git a/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java b/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java index 3cb4f16..d518ac8 100644 --- a/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java +++ b/main/src/cgeo/geocaching/utils/UnknownTagsHandler.java @@ -22,8 +22,8 @@ public class UnknownTagsHandler implements TagHandler { private ListType listType = ListType.Unordered; @Override - public void handleTag(boolean opening, String tag, Editable output, - XMLReader xmlReader) { + public void handleTag(final boolean opening, final String tag, final Editable output, + final XMLReader xmlReader) { if (tag.equalsIgnoreCase("strike") || tag.equals("s")) { handleStrike(opening, output); } else if (tag.equalsIgnoreCase("table")) { @@ -41,7 +41,7 @@ public class UnknownTagsHandler implements TagHandler { } } - private void handleStrike(boolean opening, Editable output) { + private void handleStrike(final boolean opening, final Editable output) { final int length = output.length(); if (opening) { strikePos = length; @@ -61,7 +61,7 @@ public class UnknownTagsHandler implements TagHandler { problematicDetected = true; } - private void handleTd(boolean opening, Editable output) { + private void handleTd(final boolean opening, final Editable output) { // insert bar for each table column, see https://en.wikipedia.org/wiki/Box-drawing_characters if (opening) { if (countCells++ > 0) { @@ -70,7 +70,7 @@ public class UnknownTagsHandler implements TagHandler { } } - private void handleTr(boolean opening, Editable output) { + private void handleTr(final boolean opening, final Editable output) { // insert new line for each table row if (opening) { output.append('\n'); @@ -80,7 +80,7 @@ public class UnknownTagsHandler implements TagHandler { // Ordered lists are handled in a simple manner. They are rendered as Arabic numbers starting at 1 // with no handling for alpha or Roman numbers or arbitrary numbering. - private void handleOl(boolean opening) { + private void handleOl(final boolean opening) { if (opening) { listIndex = 1; listType = ListType.Ordered; @@ -89,7 +89,7 @@ public class UnknownTagsHandler implements TagHandler { } } - private void handleLi(boolean opening, Editable output) { + private void handleLi(final boolean opening, final Editable output) { if (opening) { if (listType == ListType.Ordered) { output.append("\n ").append(String.valueOf(listIndex++)).append(". "); diff --git a/main/src/cgeo/geocaching/utils/XmlUtils.java b/main/src/cgeo/geocaching/utils/XmlUtils.java index c36fb53..004fd1b 100644 --- a/main/src/cgeo/geocaching/utils/XmlUtils.java +++ b/main/src/cgeo/geocaching/utils/XmlUtils.java @@ -17,7 +17,6 @@ public final class XmlUtils { * @param prefix an XML prefix, see {@link XmlSerializer#startTag(String, String)} * @param tag an XML tag * @param text some text to insert, or <tt>null</tt> to omit completely this tag - * @throws IOException */ public static void simpleText(final XmlSerializer serializer, final String prefix, final String tag, final String text) throws IOException { if (text != null) { @@ -34,7 +33,6 @@ public final class XmlUtils { * @param prefix an XML prefix, see {@link XmlSerializer#startTag(String, String)} shared by all tags * @param tagAndText an XML tag, the corresponding text, another XML tag, the corresponding text. <tt>null</tt> texts * will be omitted along with their respective tag. - * @throws IOException */ public static void multipleTexts(final XmlSerializer serializer, final String prefix, final String... tagAndText) throws IOException { for (int i = 0; i < tagAndText.length; i += 2) { |