aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamuel Tardieu <sam@rfc1149.net>2015-08-29 12:59:10 +0200
committerSamuel Tardieu <sam@rfc1149.net>2015-08-29 12:59:47 +0200
commit0b86d40eeb5e9ae1c5ee63b5fc8361f69f5d29af (patch)
tree2a9c3961ca8022662ba58428f4dc2f96b05e61ac
parent1bd7c325ce8d296ef50f0f57a29c9497d042f1d3 (diff)
downloadcgeo-0b86d40eeb5e9ae1c5ee63b5fc8361f69f5d29af.zip
cgeo-0b86d40eeb5e9ae1c5ee63b5fc8361f69f5d29af.tar.gz
cgeo-0b86d40eeb5e9ae1c5ee63b5fc8361f69f5d29af.tar.bz2
Revert 5 latest commits due to minor CRLF EOL issues
They will be resubmitted with proper EOL settings. Revert "images can have null description, fix tests" This reverts commit 1bd7c325ce8d296ef50f0f57a29c9497d042f1d3. Revert "refactoring: more null annotations" This reverts commit 45e9235419f5a3d1232e4c557f6a85534d822d3f. Revert "fix nullable violations" This reverts commit 8ad70af3f005579769b8198741d589f0a1c3230f. Revert "Android lint: recycle parcel after use" This reverts commit 7bcf3ce15868f6dfa052f9255c393ceda74527d5. Revert "fix warnings: unused imports, static declarations" This reverts commit 5df4801a2f4a5a4905a6bad19a27288b8ba3a40d.
-rw-r--r--main/src/cgeo/geocaching/Image.java728
-rw-r--r--main/src/cgeo/geocaching/ImageSelectActivity.java663
-rw-r--r--main/src/cgeo/geocaching/connector/ILoggingManager.java84
-rw-r--r--main/src/cgeo/geocaching/connector/gc/GCParser.java4086
-rw-r--r--main/src/cgeo/geocaching/connector/trackable/GeokretyLoggingManager.java171
-rw-r--r--main/src/cgeo/geocaching/location/DistanceParser.java102
-rw-r--r--main/src/cgeo/geocaching/settings/Settings.java2413
-rw-r--r--main/src/cgeo/geocaching/utils/CryptUtils.java309
-rw-r--r--main/src/cgeo/geocaching/utils/EnvironmentUtils.java58
-rw-r--r--main/src/cgeo/geocaching/utils/FileUtils.java427
-rw-r--r--main/src/cgeo/geocaching/utils/Formatter.java492
-rw-r--r--main/src/cgeo/geocaching/utils/HtmlUtils.java149
-rw-r--r--main/src/cgeo/geocaching/utils/ImageUtils.java976
-rw-r--r--main/src/cgeo/geocaching/utils/LogTemplateProvider.java551
-rw-r--r--main/src/cgeo/geocaching/utils/MapUtils.java580
-rw-r--r--main/src/cgeo/geocaching/utils/MatcherWrapper.java6
-rw-r--r--tests/src/cgeo/geocaching/ImageTest.java379
-rw-r--r--tests/src/cgeo/test/Compare.java130
18 files changed, 6128 insertions, 6176 deletions
diff --git a/main/src/cgeo/geocaching/Image.java b/main/src/cgeo/geocaching/Image.java
index 32c51fd..0612e85 100644
--- a/main/src/cgeo/geocaching/Image.java
+++ b/main/src/cgeo/geocaching/Image.java
@@ -1,364 +1,364 @@
-package cgeo.geocaching;
-
-import cgeo.geocaching.activity.ActivityMixin;
-import cgeo.geocaching.utils.FileUtils;
-import cgeo.geocaching.utils.Log;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.builder.HashCodeBuilder;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-
-import android.app.Activity;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.io.File;
-
-/**
- * Represent an Image along with Title and Description.
- * Mostly used for representing image in/from logs.
- */
-public class Image implements Parcelable {
-
- /**
- * Static empty image, linked to nothing.
- */
- public static final Image NONE = new Image(Uri.EMPTY, null, null);
-
- /**
- * Image builder class.
- */
- public static class Builder {
- @NonNull private Uri uri;
- @Nullable private String title;
- @Nullable private String description;
-
- /**
- * Create a new Image.
- *
- */
- public Builder() {
- uri = Uri.EMPTY;
- title = null;
- description = null;
- }
-
- /**
- * Set image url from String.
- *
- */
- @NonNull
- public Image build() {
- return new Image(uri, title, description);
- }
-
- /**
- * Set image url from String.
- *
- * @param url
- * The image url from String
- */
- @NonNull
- public Builder setUrl(@NonNull final String url) {
- uri = Uri.parse(url);
- return this;
- }
-
- /**
- * Set image from Uri.
- *
- * @param uri
- * The image url from Uri
- */
- @NonNull
- public Builder setUrl(@NonNull final Uri uri) {
- this.uri = uri;
- return this;
- }
-
- /**
- * Set image from File.
- *
- * @param file
- * The image url from File
- */
- @NonNull
- public Builder setUrl(@NonNull final File file) {
- uri = Uri.fromFile(file);
- return this;
- }
-
- /**
- * Set image from Image.
- *
- * @param image
- * The image url from Image
- */
- @NonNull
- public Builder setUrl(@NonNull final Image image) {
- uri = image.uri;
- return this;
- }
-
- /**
- * Set image title.
- *
- * @param title
- * The image title
- */
- @NonNull
- public Builder setTitle(@NonNull final String title) {
- this.title = title;
- return this;
- }
-
- /**
- * Set image description.
- *
- * @param description
- * The image description
- */
- @NonNull
- public Builder setDescription(@Nullable final String description) {
- this.description = description;
- return this;
- }
- }
-
-
- @NonNull final Uri uri;
- @Nullable final String title;
- @Nullable final String description;
-
- /**
- * Create a new Image from Url.
- *
- * @param uri
- * The image uri
- * @param title
- * The image title
- * @param description
- * The image description
- */
- private Image(@NonNull final Uri uri, @Nullable final String title, @Nullable final String description) {
- this.uri = uri;
- this.title = title;
- this.description = description;
- }
-
- private Image(@NonNull final Parcel in) {
- uri = in.readParcelable(Uri.class.getClassLoader());
- title = in.readString();
- description = in.readString();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeParcelable(uri, 0);
- dest.writeString(title);
- dest.writeString(description);
- }
-
- public static final Parcelable.Creator<Image> CREATOR = new Parcelable.Creator<Image>() {
- @Override
- public Image createFromParcel(final Parcel in) {
- return new Image(in);
- }
-
- @Override
- public Image[] newArray(final int size) {
- return new Image[size];
- }
- };
-
- /**
- * Get image title.
- *
- * @return
- * the image title
- */
- @Nullable
- public String getTitle() {
- return title;
- }
-
- /**
- * Get image description.
- *
- * @return
- * the image description
- */
- @Nullable
- public String getDescription() {
- return description;
- }
-
- /**
- * Get the image Url.
- *
- * @return
- * the image url
- */
- @NonNull
- public String getUrl() {
- return uri.toString();
- }
-
- /**
- * Get the image Uri.
- *
- * @return
- * the image uri
- */
- @NonNull
- public Uri getUri() {
- return uri;
- }
-
- /**
- * Get the image filesystem path.
- *
- * @return
- * the image url path
- */
- @NonNull
- public String getPath() {
- return isLocalFile() ? uri.getPath() : "";
- }
-
- /**
- * Get the image as File.
- * If file is not local, return Null
- *
- * @return
- * the image File
- */
- @Nullable
- public File getFile() {
- if (isLocalFile()) {
- return new File(uri.toString());
- }
- return null;
- }
-
- /**
- * Check if image has a title.
- *
- * @return
- * True if the image has a title
- */
- public boolean hasTitle() {
- return StringUtils.isNotBlank(title);
- }
-
- /**
- * Check if the image has a description.
- *
- * @return
- * True if the image has a description
- */
- public boolean hasDescription() {
- return StringUtils.isNotBlank(description);
- }
-
- /**
- * Open the image in an external activity.
- * Do nothing if image url is empty.
- *
- * @param fromActivity
- * The calling activity
- */
- public void openInBrowser(final Activity fromActivity) {
- if (isEmpty()) {
- return;
- }
- final Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
- try {
- fromActivity.startActivity(browserIntent);
- } catch (final ActivityNotFoundException e) {
- Log.e("Cannot find suitable activity", e);
- ActivityMixin.showToast(fromActivity, R.string.err_application_no);
- }
- }
-
- /**
- * Check if the URL represents a file on the local file system.
- *
- * @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
- */
- public boolean isLocalFile() {
- return FileUtils.isFileUrl(uri.toString());
- }
-
- /**
- * Local file name when {@link #isLocalFile()} is <tt>true</tt>.
- *
- * @return the local file
- */
- public File localFile() {
- return FileUtils.urlToFile(uri.toString());
- }
-
- /**
- * Check if the image exists locally.
- * Return False if Image is not local.
- * Todo: May check if we have a cached Image for remote Uri
- *
- * @return
- * True if image exists on local filesystem
- */
- public boolean existsLocal() {
- if (!isLocalFile()) {
- return false;
- }
- return new File(getPath()).exists();
- }
-
- /**
- * Check if the image Uri is Empty.
- *
- * @return
- * True if Uri is Empty or blank
- */
- public boolean isEmpty() {
- return uri.equals(Uri.EMPTY) || StringUtils.isBlank(uri.toString());
- }
-
- /**
- * Compare two Images.
- *
- * @param o
- * The Object to compare
- * @return
- * True if all fields match
- */
- @Override
- public boolean equals(final Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- final Image image = (Image) o;
-
- return uri.equals(image.uri)
- && StringUtils.equals(title, image.title)
- && StringUtils.equals(description, image.description);
- }
-
- @Override
- public int hashCode() {
- return new HashCodeBuilder().append(uri).append(title).append(description).build();
- }
-}
+package cgeo.geocaching;
+
+import cgeo.geocaching.activity.ActivityMixin;
+import cgeo.geocaching.utils.FileUtils;
+import cgeo.geocaching.utils.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.File;
+
+/**
+ * Represent an Image along with Title and Description.
+ * Mostly used for representing image in/from logs.
+ */
+public class Image implements Parcelable {
+
+ /**
+ * Static empty image, linked to nothing.
+ */
+ public static final Image NONE = new Image(Uri.EMPTY, null, null);
+
+ /**
+ * Image builder class.
+ */
+ public static class Builder {
+ @NonNull private Uri uri;
+ @Nullable private String title;
+ @Nullable private String description;
+
+ /**
+ * Create a new Image.
+ *
+ */
+ public Builder() {
+ uri = Uri.EMPTY;
+ title = null;
+ description = null;
+ }
+
+ /**
+ * Set image url from String.
+ *
+ */
+ @NonNull
+ public Image build() {
+ return new Image(uri, title, description);
+ }
+
+ /**
+ * Set image url from String.
+ *
+ * @param url
+ * The image url from String
+ */
+ @NonNull
+ public Builder setUrl(@NonNull final String url) {
+ uri = Uri.parse(url);
+ return this;
+ }
+
+ /**
+ * Set image from Uri.
+ *
+ * @param uri
+ * The image url from Uri
+ */
+ @NonNull
+ public Builder setUrl(@NonNull final Uri uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /**
+ * Set image from File.
+ *
+ * @param file
+ * The image url from File
+ */
+ @NonNull
+ public Builder setUrl(@NonNull final File file) {
+ uri = Uri.fromFile(file);
+ return this;
+ }
+
+ /**
+ * Set image from Image.
+ *
+ * @param image
+ * The image url from Image
+ */
+ @NonNull
+ public Builder setUrl(@NonNull final Image image) {
+ uri = image.uri;
+ return this;
+ }
+
+ /**
+ * Set image title.
+ *
+ * @param title
+ * The image title
+ */
+ @NonNull
+ public Builder setTitle(@NonNull final String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Set image description.
+ *
+ * @param description
+ * The image description
+ */
+ @NonNull
+ public Builder setDescription(@NonNull final String description) {
+ this.description = description;
+ return this;
+ }
+ }
+
+
+ @NonNull final Uri uri;
+ @Nullable final String title;
+ @Nullable final String description;
+
+ /**
+ * Create a new Image from Url.
+ *
+ * @param uri
+ * The image uri
+ * @param title
+ * The image title
+ * @param description
+ * The image description
+ */
+ private Image(@NonNull final Uri uri, @Nullable final String title, @Nullable final String description) {
+ this.uri = uri;
+ this.title = title;
+ this.description = description;
+ }
+
+ private Image(@NonNull final Parcel in) {
+ uri = in.readParcelable(Uri.class.getClassLoader());
+ title = in.readString();
+ description = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ dest.writeParcelable(uri, 0);
+ dest.writeString(title);
+ dest.writeString(description);
+ }
+
+ public static final Parcelable.Creator<Image> CREATOR = new Parcelable.Creator<Image>() {
+ @Override
+ public Image createFromParcel(final Parcel in) {
+ return new Image(in);
+ }
+
+ @Override
+ public Image[] newArray(final int size) {
+ return new Image[size];
+ }
+ };
+
+ /**
+ * Get image title.
+ *
+ * @return
+ * the image title
+ */
+ @Nullable
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Get image description.
+ *
+ * @return
+ * the image description
+ */
+ @Nullable
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Get the image Url.
+ *
+ * @return
+ * the image url
+ */
+ @NonNull
+ public String getUrl() {
+ return uri.toString();
+ }
+
+ /**
+ * Get the image Uri.
+ *
+ * @return
+ * the image uri
+ */
+ @NonNull
+ public Uri getUri() {
+ return uri;
+ }
+
+ /**
+ * Get the image filesystem path.
+ *
+ * @return
+ * the image url path
+ */
+ @NonNull
+ public String getPath() {
+ return isLocalFile() ? uri.getPath() : "";
+ }
+
+ /**
+ * Get the image as File.
+ * If file is not local, return Null
+ *
+ * @return
+ * the image File
+ */
+ @Nullable
+ public File getFile() {
+ if (isLocalFile()) {
+ return new File(uri.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Check if image has a title.
+ *
+ * @return
+ * True if the image has a title
+ */
+ public boolean hasTitle() {
+ return StringUtils.isNotBlank(title);
+ }
+
+ /**
+ * Check if the image has a description.
+ *
+ * @return
+ * True if the image has a description
+ */
+ public boolean hasDescription() {
+ return StringUtils.isNotBlank(description);
+ }
+
+ /**
+ * Open the image in an external activity.
+ * Do nothing if image url is empty.
+ *
+ * @param fromActivity
+ * The calling activity
+ */
+ public void openInBrowser(final Activity fromActivity) {
+ if (isEmpty()) {
+ return;
+ }
+ final Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
+ try {
+ fromActivity.startActivity(browserIntent);
+ } catch (final ActivityNotFoundException e) {
+ Log.e("Cannot find suitable activity", e);
+ ActivityMixin.showToast(fromActivity, R.string.err_application_no);
+ }
+ }
+
+ /**
+ * Check if the URL represents a file on the local file system.
+ *
+ * @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
+ */
+ public boolean isLocalFile() {
+ return FileUtils.isFileUrl(uri.toString());
+ }
+
+ /**
+ * Local file name when {@link #isLocalFile()} is <tt>true</tt>.
+ *
+ * @return the local file
+ */
+ public File localFile() {
+ return FileUtils.urlToFile(uri.toString());
+ }
+
+ /**
+ * Check if the image exists locally.
+ * Return False if Image is not local.
+ * Todo: May check if we have a cached Image for remote Uri
+ *
+ * @return
+ * True if image exists on local filesystem
+ */
+ public boolean existsLocal() {
+ if (!isLocalFile()) {
+ return false;
+ }
+ return new File(getPath()).exists();
+ }
+
+ /**
+ * Check if the image Uri is Empty.
+ *
+ * @return
+ * True if Uri is Empty or blank
+ */
+ public boolean isEmpty() {
+ return uri.equals(Uri.EMPTY) || StringUtils.isBlank(uri.toString());
+ }
+
+ /**
+ * Compare two Images.
+ *
+ * @param o
+ * The Object to compare
+ * @return
+ * True if all fields match
+ */
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final Image image = (Image) o;
+
+ return uri.equals(image.uri)
+ && StringUtils.equals(title, image.title)
+ && StringUtils.equals(description, image.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder().append(uri).append(title).append(description).build();
+ }
+}
diff --git a/main/src/cgeo/geocaching/ImageSelectActivity.java b/main/src/cgeo/geocaching/ImageSelectActivity.java
index fefae9d..b6e4a29 100644
--- a/main/src/cgeo/geocaching/ImageSelectActivity.java
+++ b/main/src/cgeo/geocaching/ImageSelectActivity.java
@@ -1,335 +1,328 @@
-package cgeo.geocaching;
-
-import cgeo.geocaching.activity.AbstractActionBarActivity;
-import cgeo.geocaching.files.LocalStorage;
-import cgeo.geocaching.settings.Settings;
-import cgeo.geocaching.ui.dialog.Dialogs;
-import cgeo.geocaching.utils.ImageUtils;
-import cgeo.geocaching.utils.Log;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.Nullable;
-
-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;
-import android.provider.MediaStore;
-import android.provider.MediaStore.MediaColumns;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.Spinner;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import butterknife.Bind;
-import butterknife.ButterKnife;
-
-public class ImageSelectActivity extends AbstractActionBarActivity {
-
- @Bind(R.id.caption) protected EditText captionView;
- @Bind(R.id.description) protected EditText descriptionView;
- @Bind(R.id.logImageScale) protected Spinner scaleView;
- @Bind(R.id.camera) protected Button cameraButton;
- @Bind(R.id.stored) protected Button storedButton;
- @Bind(R.id.save) protected Button saveButton;
- @Bind(R.id.cancel) protected Button clearButton;
- @Bind(R.id.image_preview) protected ImageView imagePreview;
-
- private static final String SAVED_STATE_IMAGE = "cgeo.geocaching.saved_state_image";
- private static final String SAVED_STATE_IMAGE_SCALE = "cgeo.geocaching.saved_state_image_scale";
-
- private static final int SELECT_NEW_IMAGE = 1;
- private static final int SELECT_STORED_IMAGE = 2;
-
- // Data to be saved while reconfiguring
- private Image image;
- private int scaleChoiceIndex;
-
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- onCreate(savedInstanceState, R.layout.imageselect_activity);
- ButterKnife.bind(this);
-
- scaleChoiceIndex = Settings.getLogImageScale();
-
- // Get parameters from intent and basic cache information from database
- final Bundle extras = getIntent().getExtras();
- if (extras != null) {
- image = extras.getParcelable(Intents.EXTRA_IMAGE);
- scaleChoiceIndex = extras.getInt(Intents.EXTRA_SCALE, scaleChoiceIndex);
- }
-
- // Restore previous state
- if (savedInstanceState != null) {
- image = savedInstanceState.getParcelable(SAVED_STATE_IMAGE);
- scaleChoiceIndex = savedInstanceState.getInt(SAVED_STATE_IMAGE_SCALE);
- }
-
- if (image == null) {
- image = Image.NONE;
- }
-
- cameraButton.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(final View view) {
- selectImageFromCamera();
- }
- });
-
- storedButton.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(final View view) {
- selectImageFromStorage();
- }
- });
-
- if (image.hasTitle()) {
- captionView.setText(image.getTitle());
- Dialogs.moveCursorToEnd(captionView);
- }
-
- if (image.hasDescription()) {
- descriptionView.setText(image.getDescription());
- Dialogs.moveCursorToEnd(captionView);
- }
-
- scaleView.setSelection(scaleChoiceIndex);
- scaleView.setOnItemSelectedListener(new OnItemSelectedListener() {
- @Override
- 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(final AdapterView<?> arg0) {
- }
- });
-
- saveButton.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(final View v) {
- saveImageInfo(true);
- }
- });
-
- clearButton.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(final View v) {
- saveImageInfo(false);
- }
- });
-
- loadImagePreview();
- }
-
- @Override
- protected void onSaveInstanceState(final Bundle outState) {
- super.onSaveInstanceState(outState);
- syncEditTexts();
- outState.putParcelable(SAVED_STATE_IMAGE, image);
- outState.putInt(SAVED_STATE_IMAGE_SCALE, scaleChoiceIndex);
- }
-
- public void saveImageInfo(final boolean saveInfo) {
- if (saveInfo) {
- new AsyncTask<Void, Void, String>() {
- @Override
- protected String doInBackground(final Void... params) {
- return writeScaledImage(image.getPath());
- }
-
- @Override
- protected void onPostExecute(final String filename) {
- if (filename != null) {
- image = new Image.Builder().setUrl(filename).build();
- final Intent intent = new Intent();
- syncEditTexts();
- intent.putExtra(Intents.EXTRA_IMAGE, image);
- 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();
- }
- }
-
- private void syncEditTexts() {
- image = new Image.Builder()
- .setUrl(image.uri)
- .setTitle(captionView.getText().toString())
- .setDescription(descriptionView.getText().toString())
- .build();
-
- scaleChoiceIndex = scaleView.getSelectedItemPosition();
- }
-
- private void selectImageFromCamera() {
- // create Intent to take a picture and return control to the calling application
- final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-
- final Uri imageUri = ImageUtils.getOutputImageFileUri();
- if (imageUri == null) {
- showFailure();
- return;
- }
- image = new Image.Builder().setUrl(imageUri).build();
-
- if (image.isEmpty()) {
- showFailure();
- return;
- }
- intent.putExtra(MediaStore.EXTRA_OUTPUT, image.getUri()); // set the image file name
-
- // start the image capture Intent
- startActivityForResult(intent, SELECT_NEW_IMAGE);
- }
-
- private void selectImageFromStorage() {
- 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(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));
- return;
- }
-
- if (resultCode != RESULT_OK) {
- // Image capture failed, advise user
- showFailure();
- return;
- }
-
- // null is an acceptable result if the image has been placed in the imageUri file by the
- // camera application.
- if (data != null) {
- final Uri selectedImage = data.getData();
- if (Build.VERSION.SDK_INT < VERSION_CODES.KITKAT) {
- final String[] filePathColumn = { MediaColumns.DATA };
-
- Cursor cursor = null;
- try {
- cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
- if (cursor == null) {
- showFailure();
- return;
- }
- cursor.moveToFirst();
-
- final int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
- final String filePath = cursor.getString(columnIndex);
- if (StringUtils.isBlank(filePath)) {
- showFailure();
- return;
- }
- image = new Image.Builder().setUrl(filePath).build();
- } catch (final Exception e) {
- Log.e("ImageSelectActivity.onActivityResult", e);
- showFailure();
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- Log.d("SELECT IMAGE data = " + data.toString());
- } else {
- InputStream input = null;
- OutputStream output = null;
- try {
- input = getContentResolver().openInputStream(selectedImage);
- final File outputFile = ImageUtils.getOutputImageFile();
- if (outputFile != null) {
- output = new FileOutputStream(outputFile);
- LocalStorage.copy(input, output);
- image = new Image.Builder().setUrl(outputFile).build();
- }
- } catch (final FileNotFoundException e) {
- Log.e("ImageSelectActivity.onStartResult", e);
- } finally {
- IOUtils.closeQuietly(input);
- IOUtils.closeQuietly(output);
- }
- }
- }
-
- if (requestCode == SELECT_NEW_IMAGE) {
- showToast(getResources().getString(R.string.info_stored_image) + '\n' + image.getUrl());
- }
-
- loadImagePreview();
- }
-
- /**
- * Scales and writes the scaled image.
- *
- * @return the scaled image path, or <tt>null</tt> if the image cannot be decoded
- */
- @Nullable
- private String writeScaledImage(@Nullable final String filePath) {
- if (filePath == null) {
- return null;
- }
- scaleChoiceIndex = scaleView.getSelectedItemPosition();
- final int maxXY = getResources().getIntArray(R.array.log_image_scale_values)[scaleChoiceIndex];
- return ImageUtils.readScaleAndWriteImage(filePath, maxXY);
- }
-
- private void showFailure() {
- showToast(getResources().getString(R.string.err_acquire_image_failed));
- }
-
- private void loadImagePreview() {
- if (image.isEmpty()) {
- return;
- }
- if (!image.existsLocal()) {
- Log.i("Image does not exist");
- return;
- }
-
- new AsyncTask<Void, Void, Bitmap>() {
- @Override
- protected Bitmap doInBackground(final Void... params) {
- return ImageUtils.readAndScaleImageToFitDisplay(image.getPath());
- }
-
- @Override
- protected void onPostExecute(final Bitmap bitmap) {
- imagePreview.setImageBitmap(bitmap);
- imagePreview.setVisibility(View.VISIBLE);
- }
- }.execute();
- }
-}
+package cgeo.geocaching;
+
+import butterknife.ButterKnife;
+import butterknife.Bind;
+
+import cgeo.geocaching.activity.AbstractActionBarActivity;
+import cgeo.geocaching.files.LocalStorage;
+import cgeo.geocaching.settings.Settings;
+import cgeo.geocaching.ui.dialog.Dialogs;
+import cgeo.geocaching.utils.ImageUtils;
+import cgeo.geocaching.utils.Log;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.Nullable;
+
+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;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.Spinner;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ImageSelectActivity extends AbstractActionBarActivity {
+
+ @Bind(R.id.caption) protected EditText captionView;
+ @Bind(R.id.description) protected EditText descriptionView;
+ @Bind(R.id.logImageScale) protected Spinner scaleView;
+ @Bind(R.id.camera) protected Button cameraButton;
+ @Bind(R.id.stored) protected Button storedButton;
+ @Bind(R.id.save) protected Button saveButton;
+ @Bind(R.id.cancel) protected Button clearButton;
+ @Bind(R.id.image_preview) protected ImageView imagePreview;
+
+ private static final String SAVED_STATE_IMAGE = "cgeo.geocaching.saved_state_image";
+ private static final String SAVED_STATE_IMAGE_SCALE = "cgeo.geocaching.saved_state_image_scale";
+
+ private static final int SELECT_NEW_IMAGE = 1;
+ private static final int SELECT_STORED_IMAGE = 2;
+
+ // Data to be saved while reconfiguring
+ private Image image;
+ private int scaleChoiceIndex;
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ onCreate(savedInstanceState, R.layout.imageselect_activity);
+ ButterKnife.bind(this);
+
+ scaleChoiceIndex = Settings.getLogImageScale();
+
+ // Get parameters from intent and basic cache information from database
+ final Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ image = extras.getParcelable(Intents.EXTRA_IMAGE);
+ scaleChoiceIndex = extras.getInt(Intents.EXTRA_SCALE, scaleChoiceIndex);
+ }
+
+ // Restore previous state
+ if (savedInstanceState != null) {
+ image = savedInstanceState.getParcelable(SAVED_STATE_IMAGE);
+ scaleChoiceIndex = savedInstanceState.getInt(SAVED_STATE_IMAGE_SCALE);
+ }
+
+ if (image == null) {
+ image = Image.NONE;
+ }
+
+ cameraButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View view) {
+ selectImageFromCamera();
+ }
+ });
+
+ storedButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View view) {
+ selectImageFromStorage();
+ }
+ });
+
+ if (image.hasTitle()) {
+ captionView.setText(image.getTitle());
+ Dialogs.moveCursorToEnd(captionView);
+ }
+
+ if (image.hasDescription()) {
+ descriptionView.setText(image.getDescription());
+ Dialogs.moveCursorToEnd(captionView);
+ }
+
+ scaleView.setSelection(scaleChoiceIndex);
+ scaleView.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ 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(final AdapterView<?> arg0) {
+ }
+ });
+
+ saveButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ saveImageInfo(true);
+ }
+ });
+
+ clearButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ saveImageInfo(false);
+ }
+ });
+
+ loadImagePreview();
+ }
+
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ syncEditTexts();
+ outState.putParcelable(SAVED_STATE_IMAGE, image);
+ outState.putInt(SAVED_STATE_IMAGE_SCALE, scaleChoiceIndex);
+ }
+
+ public void saveImageInfo(final boolean saveInfo) {
+ if (saveInfo) {
+ new AsyncTask<Void, Void, String>() {
+ @Override
+ protected String doInBackground(final Void... params) {
+ return writeScaledImage(image.getPath());
+ }
+
+ @Override
+ protected void onPostExecute(final String filename) {
+ if (filename != null) {
+ image = new Image.Builder().setUrl(filename).build();
+ final Intent intent = new Intent();
+ syncEditTexts();
+ intent.putExtra(Intents.EXTRA_IMAGE, image);
+ 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();
+ }
+ }
+
+ private void syncEditTexts() {
+ image = new Image.Builder()
+ .setUrl(image.uri)
+ .setTitle(captionView.getText().toString())
+ .setDescription(descriptionView.getText().toString())
+ .build();
+
+ scaleChoiceIndex = scaleView.getSelectedItemPosition();
+ }
+
+ private void selectImageFromCamera() {
+ // create Intent to take a picture and return control to the calling application
+ final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+
+ image = new Image.Builder().setUrl(ImageUtils.getOutputImageFileUri()).build();
+
+ if (image.isEmpty()) {
+ showFailure();
+ return;
+ }
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, image.getUri()); // set the image file name
+
+ // start the image capture Intent
+ startActivityForResult(intent, SELECT_NEW_IMAGE);
+ }
+
+ private void selectImageFromStorage() {
+ 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(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));
+ return;
+ }
+
+ if (resultCode != RESULT_OK) {
+ // Image capture failed, advise user
+ showFailure();
+ return;
+ }
+
+ // null is an acceptable result if the image has been placed in the imageUri file by the
+ // camera application.
+ if (data != null) {
+ final Uri selectedImage = data.getData();
+ if (Build.VERSION.SDK_INT < VERSION_CODES.KITKAT) {
+ final String[] filePathColumn = { MediaColumns.DATA };
+
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
+ if (cursor == null) {
+ showFailure();
+ return;
+ }
+ cursor.moveToFirst();
+
+ final int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
+ final String filePath = cursor.getString(columnIndex);
+ if (StringUtils.isBlank(filePath)) {
+ showFailure();
+ return;
+ }
+ image = new Image.Builder().setUrl(filePath).build();
+ } catch (final Exception e) {
+ Log.e("ImageSelectActivity.onActivityResult", e);
+ showFailure();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ Log.d("SELECT IMAGE data = " + data.toString());
+ } else {
+ InputStream input = null;
+ OutputStream output = null;
+ try {
+ input = getContentResolver().openInputStream(selectedImage);
+ final File outputFile = ImageUtils.getOutputImageFile();
+ output = new FileOutputStream(outputFile);
+ LocalStorage.copy(input, output);
+ image = new Image.Builder().setUrl(outputFile).build();
+ } catch (final FileNotFoundException e) {
+ Log.e("ImageSelectActivity.onStartResult", e);
+ } finally {
+ IOUtils.closeQuietly(input);
+ IOUtils.closeQuietly(output);
+ }
+ }
+ }
+
+ if (requestCode == SELECT_NEW_IMAGE) {
+ showToast(getResources().getString(R.string.info_stored_image) + '\n' + image.getUrl());
+ }
+
+ loadImagePreview();
+ }
+
+ /**
+ * Scales and writes the scaled image.
+ *
+ * @return the scaled image path, or <tt>null</tt> if the image cannot be decoded
+ */
+ @Nullable
+ private String writeScaledImage(@Nullable final String filePath) {
+ if (filePath == null) {
+ return null;
+ }
+ scaleChoiceIndex = scaleView.getSelectedItemPosition();
+ final int maxXY = getResources().getIntArray(R.array.log_image_scale_values)[scaleChoiceIndex];
+ return ImageUtils.readScaleAndWriteImage(filePath, maxXY);
+ }
+
+ private void showFailure() {
+ showToast(getResources().getString(R.string.err_acquire_image_failed));
+ }
+
+ private void loadImagePreview() {
+ if (image.isEmpty()) {
+ return;
+ }
+ if (!image.existsLocal()) {
+ Log.i("Image does not exist");
+ return;
+ }
+
+ new AsyncTask<Void, Void, Bitmap>() {
+ @Override
+ protected Bitmap doInBackground(final Void... params) {
+ return ImageUtils.readAndScaleImageToFitDisplay(image.getPath());
+ }
+
+ @Override
+ protected void onPostExecute(final Bitmap bitmap) {
+ imagePreview.setImageBitmap(bitmap);
+ imagePreview.setVisibility(View.VISIBLE);
+ }
+ }.execute();
+ }
+}
diff --git a/main/src/cgeo/geocaching/connector/ILoggingManager.java b/main/src/cgeo/geocaching/connector/ILoggingManager.java
index a0381d8..bba1fc4 100644
--- a/main/src/cgeo/geocaching/connector/ILoggingManager.java
+++ b/main/src/cgeo/geocaching/connector/ILoggingManager.java
@@ -1,41 +1,43 @@
-package cgeo.geocaching.connector;
-
-import cgeo.geocaching.Image;
-import cgeo.geocaching.TrackableLog;
-import cgeo.geocaching.enumerations.LogType;
-
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-
-import java.util.Calendar;
-import java.util.List;
-
-public interface ILoggingManager {
-
- /**
- * Post a log for a cache online
- *
- * @param logPassword
- * optional, maybe null
- */
- @NonNull
- LogResult postLog(@NonNull LogType logType,
- @NonNull Calendar date,
- @NonNull String log,
- @Nullable String logPassword,
- @NonNull List<TrackableLog> trackableLogs);
-
- @NonNull
- ImageResult postLogImage(String logId,
- Image image);
-
- public boolean hasLoaderError();
-
- @NonNull
- public List<TrackableLog> getTrackables();
-
- @NonNull
- public List<LogType> getPossibleLogTypes();
-
- public void init();
-}
+package cgeo.geocaching.connector;
+
+import cgeo.geocaching.Image;
+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;
+import java.util.List;
+
+public interface ILoggingManager {
+
+ /**
+ * Post a log for a cache online
+ *
+ * @param logPassword
+ * optional, maybe null
+ */
+ @NonNull
+ LogResult postLog(@NonNull LogType logType,
+ @NonNull Calendar date,
+ @NonNull String log,
+ @Nullable String logPassword,
+ @NonNull List<TrackableLog> trackableLogs);
+
+ @NonNull
+ ImageResult postLogImage(String logId,
+ Image image);
+
+ public boolean hasLoaderError();
+
+ @NonNull
+ public List<TrackableLog> getTrackables();
+
+ @NonNull
+ public List<LogType> getPossibleLogTypes();
+
+ public void init();
+}
diff --git a/main/src/cgeo/geocaching/connector/gc/GCParser.java b/main/src/cgeo/geocaching/connector/gc/GCParser.java
index e4bb6ac..47ff153 100644
--- a/main/src/cgeo/geocaching/connector/gc/GCParser.java
+++ b/main/src/cgeo/geocaching/connector/gc/GCParser.java
@@ -1,2044 +1,2042 @@
-package cgeo.geocaching.connector.gc;
-
-import cgeo.geocaching.CgeoApplication;
-import cgeo.geocaching.DataStore;
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.Image;
-import cgeo.geocaching.LogEntry;
-import cgeo.geocaching.PocketQueryList;
-import cgeo.geocaching.R;
-import cgeo.geocaching.SearchResult;
-import cgeo.geocaching.Trackable;
-import cgeo.geocaching.TrackableLog;
-import cgeo.geocaching.Waypoint;
-import cgeo.geocaching.connector.trackable.TrackableBrand;
-import cgeo.geocaching.enumerations.CacheSize;
-import cgeo.geocaching.enumerations.CacheType;
-import cgeo.geocaching.enumerations.LoadFlags;
-import cgeo.geocaching.enumerations.LoadFlags.SaveFlag;
-import cgeo.geocaching.enumerations.LogType;
-import cgeo.geocaching.enumerations.LogTypeTrackable;
-import cgeo.geocaching.enumerations.StatusCode;
-import cgeo.geocaching.enumerations.WaypointType;
-import cgeo.geocaching.files.LocParser;
-import cgeo.geocaching.gcvote.GCVote;
-import cgeo.geocaching.gcvote.GCVoteRating;
-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.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;
-import cgeo.geocaching.utils.SynchronizedDateFormat;
-import cgeo.geocaching.utils.TextUtils;
-
-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 android.net.Uri;
-import android.text.Html;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.text.Collator;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-import ch.boye.httpclientandroidlib.HttpResponse;
-import rx.Observable;
-import rx.Observable.OnSubscribe;
-import rx.Subscriber;
-import rx.functions.Action1;
-import rx.functions.Func0;
-import rx.functions.Func2;
-import rx.schedulers.Schedulers;
-
-public final class GCParser {
- @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);
-
- private GCParser() {
- // Utility class
- }
-
- @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");
- return null;
- }
-
- final List<String> cids = new ArrayList<>();
- String page = pageContent;
-
- final SearchResult searchResult = new SearchResult();
- searchResult.setUrl(url);
- searchResult.setViewstates(GCLogin.getViewstates(page));
-
- // recaptcha
- if (showCaptcha) {
- final String recaptchaJsParam = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_RECAPTCHA, false, null);
-
- if (recaptchaJsParam != null) {
- recaptchaReceiver.setKey(recaptchaJsParam.trim());
-
- recaptchaReceiver.fetchChallenge();
- }
- if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) {
- recaptchaReceiver.notifyNeed();
- }
- }
-
- if (!page.contains("SearchResultsTable")) {
- // there are no results. aborting here avoids a wrong error log in the next parsing step
- return searchResult;
- }
-
- int startPos = page.indexOf("<div id=\"ctl00_ContentBody_ResultsPanel\"");
- if (startPos == -1) {
- Log.e("GCParser.parseSearch: ID \"ctl00_ContentBody_dlResults\" not found on page");
- return null;
- }
-
- page = page.substring(startPos); // cut on <table
-
- startPos = page.indexOf('>');
- final int endPos = page.indexOf("ctl00_ContentBody_UnitTxt");
- if (startPos == -1 || endPos == -1) {
- Log.e("GCParser.parseSearch: ID \"ctl00_ContentBody_UnitTxt\" not found on page");
- return null;
- }
-
- page = page.substring(startPos + 1, endPos - startPos + 1); // cut between <table> and </table>
-
- final String[] rows = StringUtils.splitByWholeSeparator(page, "<tr class=");
- final int rowsCount = rows.length;
-
- int excludedCaches = 0;
- final List<Geocache> caches = new ArrayList<>();
- for (int z = 1; z < rowsCount; z++) {
- final Geocache cache = new Geocache();
- final String row = rows[z];
-
- // check for cache type presence
- if (!row.contains("images/wpttypes")) {
- continue;
- }
-
- try {
- final MatcherWrapper matcherGuidAndDisabled = new MatcherWrapper(GCConstants.PATTERN_SEARCH_GUIDANDDISABLED, row);
-
- while (matcherGuidAndDisabled.find()) {
- if (matcherGuidAndDisabled.groupCount() > 0) {
- if (matcherGuidAndDisabled.group(2) != null) {
- cache.setName(Html.fromHtml(matcherGuidAndDisabled.group(2).trim()).toString());
- }
- if (matcherGuidAndDisabled.group(3) != null) {
- cache.setLocation(Html.fromHtml(matcherGuidAndDisabled.group(3).trim()).toString());
- }
-
- final String attr = matcherGuidAndDisabled.group(1);
- if (attr != null) {
- cache.setDisabled(attr.contains("Strike"));
-
- cache.setArchived(attr.contains("OldWarning"));
- }
- }
- }
- } catch (final RuntimeException e) {
- // failed to parse GUID and/or Disabled
- Log.w("GCParser.parseSearch: Failed to parse GUID and/or Disabled data", e);
- }
-
- if (Settings.isExcludeDisabledCaches() && (cache.isDisabled() || cache.isArchived())) {
- // skip disabled and archived caches
- excludedCaches++;
- continue;
- }
-
- 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, null)));
-
- // cache direction - image
- if (Settings.getLoadDirImg()) {
- final String direction = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, null);
- if (direction != null) {
- cache.setDirectionImg(direction);
- }
- }
-
- // cache distance - estimated distance for basic members
- final String distance = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, 2, null, false);
- if (distance != null) {
- cache.setDistance(DistanceParser.parseDistance(distance,
- !Settings.useImperialUnits()));
- }
-
- // difficulty/terrain
- final MatcherWrapper matcherDT = new MatcherWrapper(GCConstants.PATTERN_SEARCH_DIFFICULTY_TERRAIN, row);
- if (matcherDT.find()) {
- final Float difficulty = parseStars(matcherDT.group(1));
- if (difficulty != null) {
- cache.setDifficulty(difficulty);
- }
- final Float terrain = parseStars(matcherDT.group(3));
- if (terrain != null) {
- cache.setTerrain(terrain);
- }
- }
-
- // size
- 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, null);
- if (StringUtils.isNotBlank(dateHidden)) {
- try {
- final Date date = GCLogin.parseGcCustomDate(dateHidden);
- if (date != null) {
- cache.setHidden(date);
- }
- } catch (final ParseException e) {
- Log.e("Error parsing event date from search", e);
- }
- }
-
- // cache inventory
- final MatcherWrapper matcherTbs = new MatcherWrapper(GCConstants.PATTERN_SEARCH_TRACKABLES, row);
- String inventoryPre = null;
- while (matcherTbs.find()) {
- if (matcherTbs.groupCount() > 0) {
- try {
- cache.setInventoryItems(Integer.parseInt(matcherTbs.group(1)));
- } catch (final NumberFormatException e) {
- Log.e("Error parsing trackables count", e);
- }
- inventoryPre = matcherTbs.group(2);
- }
- }
-
- if (StringUtils.isNotBlank(inventoryPre)) {
- assert inventoryPre != null;
- final MatcherWrapper matcherTbsInside = new MatcherWrapper(GCConstants.PATTERN_SEARCH_TRACKABLESINSIDE, inventoryPre);
- while (matcherTbsInside.find()) {
- if (matcherTbsInside.groupCount() == 2 &&
- matcherTbsInside.group(2) != null &&
- !matcherTbsInside.group(2).equalsIgnoreCase("premium member only cache") &&
- cache.getInventoryItems() <= 0) {
- cache.setInventoryItems(1);
- }
- }
- }
-
- // premium cache
- cache.setPremiumMembersOnly(row.contains("/images/icons/16/premium_only.png"));
-
- // found it
- cache.setFound(row.contains("/images/icons/16/found.png") || row.contains("uxUserLogDate\" class=\"Success\""));
-
- // id
- String result = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_ID, null);
- if (null != result) {
- cache.setCacheId(result);
- cids.add(cache.getCacheId());
- }
-
- // favorite count
- try {
- result = getNumberString(TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_FAVORITE, false, 1, null, true));
- if (null != result) {
- cache.setFavoritePoints(Integer.parseInt(result));
- }
- } catch (final NumberFormatException e) {
- Log.w("GCParser.parseSearch: Failed to parse favorite count", e);
- }
-
- caches.add(cache);
- }
- searchResult.addAndPutInCache(caches);
-
- // total caches found
- try {
- final String result = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_TOTALCOUNT, false, 1, null, true);
- if (null != result) {
- searchResult.setTotalCountGC(Integer.parseInt(result) - excludedCaches);
- }
- } catch (final NumberFormatException e) {
- Log.w("GCParser.parseSearch: Failed to parse cache count", e);
- }
-
- String recaptchaText = null;
- if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) {
- recaptchaReceiver.waitForUser();
- recaptchaText = recaptchaReceiver.getText();
- }
-
- 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
- final Parameters params = new Parameters(
- "__EVENTTARGET", "",
- "__EVENTARGUMENT", "");
- GCLogin.putViewstates(params, searchResult.getViewstates());
- for (final String cid : cids) {
- params.put("CID", cid);
- }
-
- if (StringUtils.isNotBlank(recaptchaText)) {
- assert recaptchaReceiver != null; // Help Eclipse here. If recaptchaReceiver could be null, recaptchaText would have stayed null also.
- params.put("recaptcha_challenge_field", recaptchaReceiver.getChallenge());
- params.put("recaptcha_response_field", recaptchaText);
- }
- params.put("Download", "Download Waypoints");
-
- // retrieve target url
- final String queryUrl = TextUtils.getMatch(pageContent, GCConstants.PATTERN_SEARCH_POST_ACTION, "");
-
- if (StringUtils.isEmpty(queryUrl)) {
- Log.w("Loc download url not found");
- } else {
-
- final String coordinates = Network.getResponseData(Network.postRequest("http://www.geocaching.com/seek/" + queryUrl, params), false);
-
- 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;
- }
-
- LocParser.parseLoc(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
- if (parsed.left != StatusCode.NO_ERROR) {
- return new SearchResult(parsed.left);
- }
-
- final Geocache cache = parsed.right;
- getExtraOnlineInfo(cache, page, handler);
- // too late: it is already stored through parseCacheFromText
- cache.setDetailedUpdatedNow();
- if (CancellableHandler.isCancelled(handler)) {
- return null;
- }
-
- // save full detailed caches
- CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_cache);
- DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB));
-
- // update progress message so user knows we're still working. This is more of a place holder than
- // actual indication of what the program is doing
- CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_render);
- 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.saveLogs(parsed.right.getGeocode(), getLogs(parseUserToken(page), Logs.ALL).toBlocking().toIterable());
- }
- return result;
- }
-
- /**
- * 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-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 UNKNOWN_PARSE_ERROR;
- }
-
- if (pageIn.contains(GCConstants.STRING_UNPUBLISHED_OTHER) || pageIn.contains(GCConstants.STRING_UNPUBLISHED_FROM_SEARCH)) {
- return ImmutablePair.of(StatusCode.UNPUBLISHED_CACHE, null);
- }
-
- if (pageIn.contains(GCConstants.STRING_PREMIUMONLY_1) || pageIn.contains(GCConstants.STRING_PREMIUMONLY_2)) {
- return ImmutablePair.of(StatusCode.PREMIUM_ONLY, null);
- }
-
- final String cacheName = Html.fromHtml(TextUtils.getMatch(pageIn, GCConstants.PATTERN_NAME, true, "")).toString();
- if (GCConstants.STRING_UNKNOWN_ERROR.equalsIgnoreCase(cacheName)) {
- 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 = "";
- final MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_PERSONALNOTE, pageIn);
- if (matcher.find()) {
- personalNoteWithLineBreaks = matcher.group(1).trim();
- }
-
- final String page = TextUtils.replaceWhitespace(pageIn);
-
- final Geocache cache = new Geocache();
- cache.setDisabled(page.contains(GCConstants.STRING_DISABLED));
-
- cache.setArchived(page.contains(GCConstants.STRING_ARCHIVED));
-
- cache.setPremiumMembersOnly(TextUtils.matches(page, GCConstants.PATTERN_PREMIUMMEMBERS));
-
- cache.setFavorite(TextUtils.matches(page, GCConstants.PATTERN_FAVORITE));
-
- // cache geocode
- cache.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_GEOCODE, true, cache.getGeocode()));
-
- // cache id
- cache.setCacheId(TextUtils.getMatch(page, GCConstants.PATTERN_CACHEID, true, cache.getCacheId()));
-
- // cache guid
- cache.setGuid(TextUtils.getMatch(page, GCConstants.PATTERN_GUID, true, cache.getGuid()));
-
- // name
- cache.setName(cacheName);
-
- // owner real name
- cache.setOwnerUserId(Network.decode(TextUtils.getMatch(page, GCConstants.PATTERN_OWNER_USERID, true, cache.getOwnerUserId())));
-
- cache.setUserModifiedCoords(false);
-
- String tableInside = page;
-
- final int pos = tableInside.indexOf(GCConstants.STRING_CACHEDETAILS);
- if (pos == -1) {
- Log.e("GCParser.parseCache: ID \"cacheDetails\" not found on page");
- return UNKNOWN_PARSE_ERROR;
- }
-
- tableInside = tableInside.substring(pos);
-
- if (StringUtils.isNotBlank(tableInside)) {
- // cache terrain
- String result = TextUtils.getMatch(tableInside, GCConstants.PATTERN_TERRAIN, true, null);
- if (result != null) {
- try {
- cache.setTerrain(Float.parseFloat(StringUtils.replaceChars(result, '_', '.')));
- } catch (final NumberFormatException e) {
- Log.e("Error parsing terrain value", e);
- }
- }
-
- // cache difficulty
- result = TextUtils.getMatch(tableInside, GCConstants.PATTERN_DIFFICULTY, true, null);
- if (result != null) {
- try {
- cache.setDifficulty(Float.parseFloat(StringUtils.replaceChars(result, '_', '.')));
- } catch (final NumberFormatException e) {
- Log.e("Error parsing difficulty value", e);
- }
- }
-
- // owner
- cache.setOwnerDisplayName(StringEscapeUtils.unescapeHtml4(TextUtils.getMatch(tableInside, GCConstants.PATTERN_OWNER_DISPLAYNAME, true, cache.getOwnerDisplayName())));
-
- // hidden
- try {
- String hiddenString = TextUtils.getMatch(tableInside, GCConstants.PATTERN_HIDDEN, true, null);
- if (StringUtils.isNotBlank(hiddenString)) {
- cache.setHidden(GCLogin.parseGcCustomDate(hiddenString));
- }
- if (cache.getHiddenDate() == null) {
- // event date
- hiddenString = TextUtils.getMatch(tableInside, GCConstants.PATTERN_HIDDENEVENT, true, null);
- if (StringUtils.isNotBlank(hiddenString)) {
- cache.setHidden(GCLogin.parseGcCustomDate(hiddenString));
- }
- }
- } catch (final ParseException e) {
- // failed to parse cache hidden date
- Log.w("GCParser.parseCache: Failed to parse cache hidden (event) date", e);
- }
-
- // favorite
- try {
- cache.setFavoritePoints(Integer.parseInt(TextUtils.getMatch(tableInside, GCConstants.PATTERN_FAVORITECOUNT, true, "0")));
- } catch (final NumberFormatException e) {
- Log.e("Error parsing favorite count", e);
- }
-
- // cache size
- cache.setSize(CacheSize.getById(TextUtils.getMatch(tableInside, GCConstants.PATTERN_SIZE, true, CacheSize.NOT_CHOSEN.id)));
- }
-
- // cache found
- cache.setFound(TextUtils.matches(page, GCConstants.PATTERN_FOUND) || TextUtils.matches(page, GCConstants.PATTERN_FOUND_ALTERNATIVE));
-
- // cache type
- cache.setType(CacheType.getByGuid(TextUtils.getMatch(page, GCConstants.PATTERN_TYPE, true, cache.getType().id)));
-
- // on watchlist
- cache.setOnWatchlist(TextUtils.matches(page, GCConstants.PATTERN_WATCHLIST));
-
- // latitude and longitude. Can only be retrieved if user is logged in
- String latlon = TextUtils.getMatch(page, GCConstants.PATTERN_LATLON, true, "");
- if (StringUtils.isNotEmpty(latlon)) {
- try {
- cache.setCoords(new Geopoint(latlon));
- cache.setReliableLatLon(true);
- } catch (final Geopoint.GeopointException e) {
- Log.w("GCParser.parseCache: Failed to parse cache coordinates", e);
- }
- }
-
- // cache location
- cache.setLocation(TextUtils.getMatch(page, GCConstants.PATTERN_LOCATION, true, ""));
-
- // cache hint
- final String result = TextUtils.getMatch(page, GCConstants.PATTERN_HINT, false, null);
- if (result != null) {
- // replace linebreak and paragraph tags
- final String hint = GCConstants.PATTERN_LINEBREAK.matcher(result).replaceAll("\n");
- cache.setHint(StringUtils.replace(hint, "</p>", "").trim());
- }
-
- cache.checkFields();
-
- // cache personal note
- cache.setPersonalNote(personalNoteWithLineBreaks);
-
- // cache short description
- cache.setShortDescription(TextUtils.getMatch(page, GCConstants.PATTERN_SHORTDESC, true, ""));
-
- // cache description
- 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("<br/><br/><a href=\"%s\"><b>%s</b></a>", relatedWebPage, relatedWebPage);
- }
- cache.setDescription(longDescription + relatedWebPage);
-
- // cache attributes
- try {
- final List<String> attributes = new ArrayList<>();
- final String attributesPre = TextUtils.getMatch(page, GCConstants.PATTERN_ATTRIBUTES, true, null);
- if (attributesPre != null) {
- final MatcherWrapper matcherAttributesInside = new MatcherWrapper(GCConstants.PATTERN_ATTRIBUTESINSIDE, attributesPre);
-
- while (matcherAttributesInside.find()) {
- if (matcherAttributesInside.groupCount() > 1 && !matcherAttributesInside.group(2).equalsIgnoreCase("blank")) {
- // by default, use the tooltip of the attribute
- String attribute = matcherAttributesInside.group(2).toLowerCase(Locale.US);
-
- // if the image name can be recognized, use the image name as attribute
- final String imageName = matcherAttributesInside.group(1).trim();
- if (StringUtils.isNotEmpty(imageName)) {
- final int start = imageName.lastIndexOf('/');
- final int end = imageName.lastIndexOf('.');
- if (start >= 0 && end >= 0) {
- attribute = imageName.substring(start + 1, end).replace('-', '_').toLowerCase(Locale.US);
- }
- }
- attributes.add(attribute);
- }
- }
- }
- cache.setAttributes(attributes);
- } catch (final RuntimeException e) {
- // failed to parse cache attributes
- Log.w("GCParser.parseCache: Failed to parse cache attributes", e);
- }
-
- // cache spoilers
- try {
- if (CancellableHandler.isCancelled(handler)) {
- return UNKNOWN_PARSE_ERROR;
- }
- CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_spoilers);
-
- final MatcherWrapper matcherSpoilersInside = new MatcherWrapper(GCConstants.PATTERN_SPOILER_IMAGE, page);
-
- while (matcherSpoilersInside.find()) {
- // the original spoiler URL (include .../display/... contains a low-resolution image
- // if we shorten the URL we get the original-resolution image
- final String url = matcherSpoilersInside.group(1).replace("/display", "");
-
- String title = null;
- if (matcherSpoilersInside.group(3) != null) {
- title = matcherSpoilersInside.group(3);
- }
- String description = null;
- if (matcherSpoilersInside.group(4) != null) {
- description = matcherSpoilersInside.group(4);
- }
- if (title != null) {
- cache.addSpoiler(new Image.Builder().setUrl(url).setTitle(title).setDescription(description).build());
- }
- }
- } catch (final RuntimeException e) {
- // failed to parse cache spoilers
- Log.w("GCParser.parseCache: Failed to parse cache spoilers", e);
- }
-
- // cache inventory
- try {
- final MatcherWrapper matcherInventory = new MatcherWrapper(GCConstants.PATTERN_INVENTORY, page);
- if (matcherInventory.find()) {
- if (matcherInventory.groupCount() > 1) {
- final String inventoryPre = matcherInventory.group(2);
-
- final ArrayList<Trackable> inventory = new ArrayList<>();
- if (StringUtils.isNotBlank(inventoryPre)) {
- final MatcherWrapper matcherInventoryInside = new MatcherWrapper(GCConstants.PATTERN_INVENTORYINSIDE, inventoryPre);
-
- while (matcherInventoryInside.find()) {
- if (matcherInventoryInside.groupCount() > 0) {
- final Trackable inventoryItem = new Trackable();
- inventoryItem.forceSetBrand(TrackableBrand.TRAVELBUG);
- inventoryItem.setGuid(matcherInventoryInside.group(1));
- inventoryItem.setName(matcherInventoryInside.group(2));
-
- inventory.add(inventoryItem);
- }
- }
- }
- cache.mergeInventory(inventory, EnumSet.of(TrackableBrand.TRAVELBUG));
- }
- }
- } catch (final RuntimeException e) {
- // failed to parse cache inventory
- Log.w("GCParser.parseCache: Failed to parse cache inventory (2)", e);
- }
-
- // cache logs counts
- try {
- final String countlogs = TextUtils.getMatch(page, GCConstants.PATTERN_COUNTLOGS, true, null);
- if (null != countlogs) {
- final MatcherWrapper matcherLog = new MatcherWrapper(GCConstants.PATTERN_COUNTLOG, countlogs);
-
- while (matcherLog.find()) {
- final String typeStr = matcherLog.group(1);
- final String countStr = getNumberString(matcherLog.group(2));
-
- if (StringUtils.isNotBlank(typeStr)
- && LogType.UNKNOWN != LogType.getByIconName(typeStr)
- && StringUtils.isNotBlank(countStr)) {
- cache.getLogCounts().put(LogType.getByIconName(typeStr), Integer.valueOf(countStr));
- }
- }
- }
- if (cache.getLogCounts().isEmpty()) {
- Log.w("GCParser.parseCache: Failed to parse cache log count");
- }
- } catch (final NumberFormatException e) {
- // failed to parse logs
- Log.w("GCParser.parseCache: Failed to parse cache log count", e);
- }
-
- // waypoints - reset collection
- cache.setWaypoints(Collections.<Waypoint> emptyList(), false);
-
- // add waypoint for original coordinates in case of user-modified listing-coordinates
- try {
- final String originalCoords = TextUtils.getMatch(page, GCConstants.PATTERN_LATLON_ORIG, false, null);
-
- if (null != originalCoords) {
- final Waypoint waypoint = new Waypoint(CgeoApplication.getInstance().getString(R.string.cache_coordinates_original), WaypointType.ORIGINAL, false);
- waypoint.setCoords(new Geopoint(originalCoords));
- cache.addOrChangeWaypoint(waypoint, false);
- cache.setUserModifiedCoords(true);
- }
- } 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 UNKNOWN_PARSE_ERROR;
- }
- CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_waypoints);
-
- String wpList = page.substring(wpBegin);
-
- int wpEnd = wpList.indexOf("</p>");
- if (wpEnd > -1 && wpEnd <= wpList.length()) {
- wpList = wpList.substring(0, wpEnd);
- }
-
- if (!wpList.contains("No additional waypoints to display.")) {
- wpEnd = wpList.indexOf("</table>");
- wpList = wpList.substring(0, wpEnd);
-
- wpBegin = wpList.indexOf("<tbody>");
- wpEnd = wpList.indexOf("</tbody>");
- if (wpBegin >= 0 && wpEnd >= 0 && wpEnd <= wpList.length()) {
- wpList = wpList.substring(wpBegin + 7, wpEnd);
- }
-
- final String[] wpItems = StringUtils.splitByWholeSeparator(wpList, "<tr");
-
- 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
- final String name = TextUtils.getMatch(wp[6], GCConstants.PATTERN_WPNAME, true, 1, CgeoApplication.getInstance().getString(R.string.waypoint), true);
-
- // waypoint type
- final String resulttype = TextUtils.getMatch(wp[3], GCConstants.PATTERN_WPTYPE, null);
-
- final Waypoint waypoint = new Waypoint(name, WaypointType.findById(resulttype), false);
-
- // waypoint prefix
- waypoint.setPrefix(TextUtils.getMatch(wp[4], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, true, 2, waypoint.getPrefix(), false));
-
- // waypoint lookup
- waypoint.setLookup(TextUtils.getMatch(wp[5], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, true, 2, waypoint.getLookup(), false));
-
- // waypoint latitude and longitude
- latlon = Html.fromHtml(TextUtils.getMatch(wp[7], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, false, 2, "", false)).toString().trim();
- if (!StringUtils.startsWith(latlon, "???")) {
- waypoint.setCoords(new Geopoint(latlon));
- }
-
- 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(wpNote[3], GCConstants.PATTERN_WPNOTE, waypoint.getNote()));
- }
-
- cache.addOrChangeWaypoint(waypoint, false);
- }
- }
- }
-
- // last check for necessary cache conditions
- if (StringUtils.isBlank(cache.getGeocode())) {
- 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, ".,", "");
- }
-
- @Nullable
- public static SearchResult searchByNextPage(final SearchResult search, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) {
- if (search == null) {
- return null;
- }
- final String[] viewstates = search.getViewstates();
-
- final String url = search.getUrl();
-
- if (StringUtils.isBlank(url)) {
- Log.e("GCParser.searchByNextPage: No url found");
- return search;
- }
-
- if (GCLogin.isEmpty(viewstates)) {
- Log.e("GCParser.searchByNextPage: No viewstate given");
- return search;
- }
-
- final Parameters params = new Parameters(
- "__EVENTTARGET", "ctl00$ContentBody$pgrBottom$ctl08",
- "__EVENTARGUMENT", "");
- GCLogin.putViewstates(params, viewstates);
-
- final String page = GCLogin.getInstance().postRequestLogged(url, params);
- if (!GCLogin.getInstance().getLoginStatus(page)) {
- Log.e("GCParser.postLogTrackable: Can not log in geocaching");
- return search;
- }
-
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.searchByNextPage: No data from server");
- return search;
- }
-
- final SearchResult searchResult = parseSearch(url, page, showCaptcha, recaptchaReceiver);
- if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) {
- Log.w("GCParser.searchByNextPage: No cache parsed");
- return search;
- }
-
- // search results don't need to be filtered so load GCVote ratings here
- GCVote.loadRatings(new ArrayList<>(searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB)));
-
- // save to application
- search.setError(searchResult.getError());
- search.setViewstates(searchResult.getViewstates());
- for (final String geocode : searchResult.getGeocodes()) {
- search.addGeocode(geocode);
- }
- return search;
- }
-
- /**
- * 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 {@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) {
- if (!my && Settings.isExcludeMyCaches()) {
- if (params == null) {
- return new Parameters("f", "1");
- }
- params.put("f", "1");
- Log.i("Skipping caches found or hidden by user.");
- }
-
- return params;
- }
-
- @Nullable
- 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 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");
- return null;
- }
- assert page != null;
-
- final SearchResult searchResult = parseSearch(fullUri, page, showCaptcha, recaptchaReceiver);
- if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) {
- Log.e("GCParser.searchByAny: No cache parsed");
- return searchResult;
- }
-
- final SearchResult search = searchResult.filterSearchResults(Settings.isExcludeDisabledCaches(), cacheType);
-
- GCLogin.getInstance().getLoginStatus(page);
-
- return search;
- }
-
- 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);
- }
-
- 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;
- }
-
- final Parameters params = new Parameters("key", keyword);
- return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver);
- }
-
- private static boolean isSearchForMyCaches(final String userName) {
- if (userName.equalsIgnoreCase(Settings.getGcCredentials().left)) {
- Log.i("Overriding users choice because of self search, downloading all caches.");
- return true;
- }
- return false;
- }
-
- 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;
- }
-
- final Parameters params = new Parameters("ul", userName);
-
- return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, 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;
- }
-
- final Parameters params = new Parameters("pq", pocketGuid);
-
- return searchByAny(cacheType, false, showCaptcha, params, 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;
- }
-
- final Parameters params = new Parameters("u", userName);
- return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, recaptchaReceiver);
- }
-
- @Nullable
- public static Trackable searchTrackable(final String geocode, final String guid, final String id) {
- if (StringUtils.isBlank(geocode) && StringUtils.isBlank(guid) && StringUtils.isBlank(id)) {
- Log.w("GCParser.searchTrackable: No geocode nor guid nor id given");
- return null;
- }
-
- Trackable trackable = new Trackable();
-
- final Parameters params = new Parameters();
- if (StringUtils.isNotBlank(geocode)) {
- params.put("tracker", geocode);
- trackable.setGeocode(geocode);
- } else if (StringUtils.isNotBlank(guid)) {
- params.put("guid", guid);
- } else if (StringUtils.isNotBlank(id)) {
- params.put("id", id);
- }
-
- final String page = GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/track/details.aspx", params);
-
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.searchTrackable: No data from server");
- return trackable;
- }
- assert page != null;
-
- trackable = parseTrackable(page, geocode);
- if (trackable == null) {
- Log.w("GCParser.searchTrackable: No trackable parsed");
- return null;
- }
-
- return trackable;
- }
-
- /**
- * 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);
-
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.searchPocketQueryList: No data from server");
- return Observable.just(Collections.<PocketQueryList>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());
- }
-
- final List<PocketQueryList> list = new ArrayList<>();
-
- final MatcherWrapper matcherPocket = new MatcherWrapper(GCConstants.PATTERN_LIST_PQ, subPage);
-
- 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);
- }
-
- // just in case, lets sort the resulting list
- final Collator collator = TextUtils.getCollator();
- Collections.sort(list, new Comparator<PocketQueryList>() {
-
- @Override
- public int compare(final PocketQueryList left, final PocketQueryList right) {
- return collator.compare(left.getName(), right.getName());
- }
- });
-
- return Observable.just(list);
- }
- }).subscribeOn(RxUtils.networkScheduler);
-
- 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,
- final String log, final List<TrackableLog> trackables) {
- if (GCLogin.isEmpty(viewstates)) {
- Log.e("GCParser.postLog: No viewstate given");
- return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, "");
- }
-
- if (StringUtils.isBlank(log)) {
- Log.e("GCParser.postLog: No log text given");
- return new ImmutablePair<>(StatusCode.NO_LOG_TEXT, "");
- }
-
- final String logInfo = log.replace("\n", "\r\n").trim(); // windows' eol and remove leading and trailing whitespaces
-
- Log.i("Trying to post log for cache #" + cacheid + " - action: " + logType
- + "; date: " + year + "." + month + "." + day + ", log: " + logInfo
- + "; trackables: " + (trackables != null ? trackables.size() : "0"));
-
- final Parameters params = new Parameters(
- "__EVENTTARGET", "",
- "__EVENTARGUMENT", "",
- "__LASTFOCUS", "",
- "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id),
- "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),
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged", String.format("%02d", month) + "/" + String.format("%02d", day) + "/" + String.format("%04d", year),
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Month", Integer.toString(month),
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day),
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Year", Integer.toString(year),
- "ctl00$ContentBody$LogBookPanel1$LogButton", "Submit Log Entry",
- "ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo,
- "ctl00$ContentBody$LogBookPanel1$btnSubmitLog", "Submit Log Entry",
- "ctl00$ContentBody$LogBookPanel1$uxLogCreationSource", "Old",
- "ctl00$ContentBody$uxVistOtherListingGC", "");
- GCLogin.putViewstates(params, viewstates);
- if (trackables != null && !trackables.isEmpty()) { // we have some trackables to proceed
- final StringBuilder hdnSelected = new StringBuilder();
-
- for (final TrackableLog tb : trackables) {
- if (tb.action != LogTypeTrackable.DO_NOTHING && tb.brand == TrackableBrand.TRAVELBUG) {
- hdnSelected.append(Integer.toString(tb.id));
- hdnSelected.append(tb.action.action);
- hdnSelected.append(',');
- }
- }
-
- params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnSelectedActions", hdnSelected.toString(), // selected trackables
- "ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnCurrentFilter", "");
- }
-
- final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/seek/log.aspx").encodedQuery("ID=" + cacheid).build().toString();
- final GCLogin gcLogin = GCLogin.getInstance();
- String page = gcLogin.postRequestLogged(uri, params);
- if (!gcLogin.getLoginStatus(page)) {
- Log.e("GCParser.postLog: Cannot log in geocaching");
- return new ImmutablePair<>(StatusCode.NOT_LOGGED_IN, "");
- }
-
- // maintenance, archived needs to be confirmed
-
- final MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_MAINTENANCE, page);
-
- try {
- if (matcher.find() && matcher.groupCount() > 0) {
- final String[] viewstatesConfirm = GCLogin.getViewstates(page);
-
- if (GCLogin.isEmpty(viewstatesConfirm)) {
- Log.e("GCParser.postLog: No viewstate for confirm log");
- return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, "");
- }
-
- params.clear();
- GCLogin.putViewstates(params, viewstatesConfirm);
- params.put("__EVENTTARGET", "");
- params.put("__EVENTARGUMENT", "");
- params.put("__LASTFOCUS", "");
- params.put("ctl00$ContentBody$LogBookPanel1$btnConfirm", "Yes");
- params.put("ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo);
- params.put("ctl00$ContentBody$uxVistOtherListingGC", "");
- if (trackables != null && !trackables.isEmpty()) { // we have some trackables to proceed
- final StringBuilder hdnSelected = new StringBuilder();
-
- for (final TrackableLog tb : trackables) {
- final String action = Integer.toString(tb.id) + tb.action.action;
- final StringBuilder paramText = new StringBuilder("ctl00$ContentBody$LogBookPanel1$uxTrackables$repTravelBugs$ctl");
-
- if (tb.ctl < 10) {
- paramText.append('0');
- }
- paramText.append(tb.ctl).append("$ddlAction");
- params.put(paramText.toString(), action);
- if (tb.action != LogTypeTrackable.DO_NOTHING) {
- hdnSelected.append(action);
- hdnSelected.append(',');
- }
- }
-
- params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnSelectedActions", hdnSelected.toString()); // selected trackables
- params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnCurrentFilter", "");
- }
-
- page = Network.getResponseData(Network.postRequest(uri, params));
- }
- } catch (final RuntimeException e) {
- 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);
- if (matcherOk.find()) {
- Log.i("Log successfully posted to cache #" + cacheid);
-
- if (geocode != null) {
- DataStore.saveVisitDate(geocode);
- }
-
- gcLogin.getLoginStatus(page);
- // the log-successful-page contains still the old value
- if (gcLogin.getActualCachesFound() >= 0) {
- gcLogin.setActualCachesFound(gcLogin.getActualCachesFound() + (logType.isFoundLog() ? 1 : 0));
- }
-
- final String logID = TextUtils.getMatch(page, GCConstants.PATTERN_LOG_IMAGE_UPLOAD, "");
-
- return new ImmutablePair<>(StatusCode.NO_ERROR, logID);
- }
- } catch (final Exception e) {
- Log.e("GCParser.postLog.check", e);
- }
-
- Log.e("GCParser.postLog: Failed to post log because of unknown error");
- return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, "");
- }
-
- /**
- * Upload an image to a log that has already been posted
- *
- * @param logId
- * the ID of the log to upload the image to. Found on page returned when log is uploaded
- * @param image
- * The Image Object
- * @return status code to indicate success or failure
- */
- static ImmutablePair<StatusCode, String> uploadLogImage(final String logId, @NonNull final Image image) {
- final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/seek/upload.aspx").encodedQuery("LID=" + logId).build().toString();
-
- final String page = GCLogin.getInstance().getRequestLogged(uri, null);
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.uploadLogImage: No data from server");
- return new ImmutablePair<>(StatusCode.UNKNOWN_ERROR, null);
- }
- assert page != null;
-
- final String[] viewstates = GCLogin.getViewstates(page);
-
- final Parameters uploadParams = new Parameters(
- "__EVENTTARGET", "",
- "__EVENTARGUMENT", "",
- "ctl00$ContentBody$ImageUploadControl1$uxFileCaption", image.getTitle(),
- "ctl00$ContentBody$ImageUploadControl1$uxFileDesc", image.getDescription(),
- "ctl00$ContentBody$ImageUploadControl1$uxUpload", "Upload");
- GCLogin.putViewstates(uploadParams, viewstates);
-
- final String response = Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$ImageUploadControl1$uxFileUpload", "image/jpeg", image.getFile()));
-
- 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()) {
- Log.i("Logimage successfully uploaded.");
- final String uploadedImageUrl = matcherUrl.group(1);
- return ImmutablePair.of(StatusCode.NO_ERROR, uploadedImageUrl);
- }
- Log.e("GCParser.uploadLogIMage: Failed to upload image because of unknown error");
-
- return ImmutablePair.of(StatusCode.LOGIMAGE_POST_ERROR, null);
- }
-
- /**
- * Post a log to GC.com.
- *
- * @return status code of the upload and ID of the log
- */
- public static StatusCode postLogTrackable(final String tbid, final String trackingCode, final String[] viewstates,
- final LogTypeTrackable logType, final int year, final int month, final int day, final String log) {
- if (GCLogin.isEmpty(viewstates)) {
- Log.e("GCParser.postLogTrackable: No viewstate given");
- return StatusCode.LOG_POST_ERROR;
- }
-
- if (StringUtils.isBlank(log)) {
- Log.e("GCParser.postLogTrackable: No log text given");
- return StatusCode.NO_LOG_TEXT;
- }
-
- Log.i("Trying to post log for trackable #" + trackingCode + " - action: " + logType + "; date: " + year + "." + month + "." + day + ", log: " + log);
-
- final String logInfo = log.replace("\n", "\r\n"); // windows' eol
-
- final Calendar currentDate = Calendar.getInstance();
- final Parameters params = new Parameters(
- "__EVENTTARGET", "",
- "__EVENTARGUMENT", "",
- "__LASTFOCUS", "",
- "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id),
- "ctl00$ContentBody$LogBookPanel1$tbCode", trackingCode);
- GCLogin.putViewstates(params, viewstates);
- if (currentDate.get(Calendar.YEAR) == year && (currentDate.get(Calendar.MONTH) + 1) == month && currentDate.get(Calendar.DATE) == day) {
- params.put("ctl00$ContentBody$LogBookPanel1$DateTimeLogged", "");
- 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.formatGcCustomDate(year, month, day));
- }
- params.put(
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day),
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Month", Integer.toString(month),
- "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Year", Integer.toString(year),
- "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Day", Integer.toString(day),
- "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Month", Integer.toString(month),
- "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Year", Integer.toString(year),
- "ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo,
- "ctl00$ContentBody$LogBookPanel1$btnSubmitLog", "Submit Log Entry",
- "ctl00$ContentBody$uxVistOtherTrackableTB", "",
- "ctl00$ContentBody$LogBookPanel1$LogButton", "Submit Log Entry",
- "ctl00$ContentBody$uxVistOtherListingGC", "");
-
- final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/track/log.aspx").encodedQuery("wid=" + tbid).build().toString();
- final String page = GCLogin.getInstance().postRequestLogged(uri, params);
- if (!GCLogin.getInstance().getLoginStatus(page)) {
- Log.e("GCParser.postLogTrackable: Cannot log in geocaching");
- return StatusCode.NOT_LOGGED_IN;
- }
-
- try {
-
- final MatcherWrapper matcherOk = new MatcherWrapper(GCConstants.PATTERN_OK2, page);
- if (matcherOk.find()) {
- Log.i("Log successfully posted to trackable #" + trackingCode);
- return StatusCode.NO_ERROR;
- }
- } catch (final Exception e) {
- Log.e("GCParser.postLogTrackable.check", e);
- }
-
- Log.e("GCParser.postLogTrackable: Failed to post log because of unknown error");
- return StatusCode.LOG_POST_ERROR;
- }
-
- /**
- * Adds the cache to the watchlist of the user.
- *
- * @param cache
- * the cache to add
- * @return <code>false</code> if an error occurred, <code>true</code> otherwise
- */
- static boolean addToWatchlist(final Geocache cache) {
- final String uri = "http://www.geocaching.com/my/watchlist.aspx?w=" + cache.getCacheId();
- final String page = GCLogin.getInstance().postRequestLogged(uri, null);
-
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.addToWatchlist: No data from server");
- return false; // error
- }
-
- final boolean guidOnPage = isGuidContainedInPage(cache, page);
- if (guidOnPage) {
- Log.i("GCParser.addToWatchlist: cache is on watchlist");
- cache.setOnWatchlist(true);
- } else {
- Log.e("GCParser.addToWatchlist: cache is not on watchlist");
- }
- return guidOnPage; // on watchlist (=added) / else: error
- }
-
- /**
- * Removes the cache from the watch list
- *
- * @param cache
- * the cache to remove
- * @return <code>false</code> if an error occurred, <code>true</code> otherwise
- */
- static boolean removeFromWatchlist(final Geocache cache) {
- final String uri = "http://www.geocaching.com/my/watchlist.aspx?ds=1&action=rem&id=" + cache.getCacheId();
- String page = GCLogin.getInstance().postRequestLogged(uri, null);
-
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.removeFromWatchlist: No data from server");
- return false; // error
- }
-
- // removing cache from list needs approval by hitting "Yes" button
- final Parameters params = new Parameters(
- "__EVENTTARGET", "",
- "__EVENTARGUMENT", "",
- "ctl00$ContentBody$btnYes", "Yes");
- GCLogin.transferViewstates(page, params);
-
- page = Network.getResponseData(Network.postRequest(uri, params));
- final boolean guidOnPage = isGuidContainedInPage(cache, page);
- if (!guidOnPage) {
- Log.i("GCParser.removeFromWatchlist: cache removed from watchlist");
- cache.setOnWatchlist(false);
- } else {
- Log.e("GCParser.removeFromWatchlist: cache not removed from watchlist");
- }
- return !guidOnPage; // on watch list (=error) / not on watch list
- }
-
- /**
- * Checks if a page contains the guid of a cache
- *
- * @param cache the geocache
- * @param page
- * the page to search in, may be null
- * @return true if the page contains the guid of the cache, false otherwise
- */
- private static boolean isGuidContainedInPage(final Geocache cache, final String page) {
- if (StringUtils.isBlank(page) || StringUtils.isBlank(cache.getGuid())) {
- return false;
- }
- return Pattern.compile(cache.getGuid(), Pattern.CASE_INSENSITIVE).matcher(page).find();
- }
-
- @Nullable
- static String requestHtmlPage(@Nullable final String geocode, @Nullable final String guid, final String log) {
- final Parameters params = new Parameters("decrypt", "y");
- if (StringUtils.isNotBlank(geocode)) {
- params.put("wp", geocode);
- } else if (StringUtils.isNotBlank(guid)) {
- params.put("guid", guid);
- }
- params.put("log", log);
- params.put("numlogs", "0");
-
- return GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/seek/cache_details.aspx", params);
- }
-
- /**
- * Adds the cache to the favorites of the user.
- *
- * This must not be called from the UI thread.
- *
- * @param cache
- * the cache to add
- * @return <code>false</code> if an error occurred, <code>true</code> otherwise
- */
- static boolean addToFavorites(final Geocache cache) {
- return changeFavorite(cache, true);
- }
-
- private static boolean changeFavorite(final Geocache cache, final boolean add) {
- final String userToken = getUserToken(cache);
- if (StringUtils.isEmpty(userToken)) {
- return false;
- }
-
- final String uri = "http://www.geocaching.com/datastore/favorites.svc/update?u=" + userToken + "&f=" + Boolean.toString(add);
-
- final HttpResponse response = Network.postRequest(uri, null);
-
- if (response != null && response.getStatusLine().getStatusCode() == 200) {
- Log.i("GCParser.changeFavorite: cache added/removed to/from favorites");
- cache.setFavorite(add);
- cache.setFavoritePoints(cache.getFavoritePoints() + (add ? 1 : -1));
- return true;
- }
- Log.e("GCParser.changeFavorite: cache not added/removed to/from favorites");
- return false;
- }
-
- private static String getUserToken(final Geocache cache) {
- return parseUserToken(requestHtmlPage(cache.getGeocode(), null, "n"));
- }
-
- private static String parseUserToken(final String page) {
- return TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, "");
- }
-
- /**
- * Removes the cache from the favorites.
- *
- * This must not be called from the UI thread.
- *
- * @param cache
- * the cache to remove
- * @return <code>false</code> if an error occurred, <code>true</code> otherwise
- */
- static boolean removeFromFavorites(final Geocache cache) {
- return changeFavorite(cache, false);
- }
-
- /**
- * Parse a trackable HTML description into a Trackable object
- *
- * @param page
- * the HTML page to parse, already processed through {@link TextUtils#replaceWhitespace}
- * @return the parsed trackable, or null if none could be parsed
- */
- static Trackable parseTrackable(final String page, final String possibleTrackingcode) {
- if (StringUtils.isBlank(page)) {
- Log.e("GCParser.parseTrackable: No page given");
- return null;
- }
-
- if (page.contains(GCConstants.ERROR_TB_DOES_NOT_EXIST) || page.contains(GCConstants.ERROR_TB_ARITHMETIC_OVERFLOW) || page.contains(GCConstants.ERROR_TB_ELEMENT_EXCEPTION)) {
- return null;
- }
-
- final Trackable trackable = new Trackable();
- trackable.forceSetBrand(TrackableBrand.TRAVELBUG);
-
- // trackable geocode
- trackable.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GEOCODE, true, StringUtils.upperCase(possibleTrackingcode)));
- if (trackable.getGeocode() == null) {
- Log.e("GCParser.parseTrackable: could not figure out trackable geocode");
- return null;
- }
-
- // trackable id
- trackable.setGuid(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GUID, true, trackable.getGuid()));
-
- // trackable icon
- trackable.setIconUrl(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ICON, true, trackable.getIconUrl()));
-
- // trackable name
- trackable.setName(Html.fromHtml(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_NAME, true, "")).toString());
-
- // trackable type
- if (StringUtils.isNotBlank(trackable.getName())) {
- trackable.setType(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_TYPE, true, trackable.getType()));
- }
-
- // trackable owner name
- try {
- final MatcherWrapper matcherOwner = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_OWNER, page);
- if (matcherOwner.find() && matcherOwner.groupCount() > 0) {
- trackable.setOwnerGuid(matcherOwner.group(1));
- trackable.setOwner(matcherOwner.group(2).trim());
- }
- } catch (final RuntimeException e) {
- // failed to parse trackable owner name
- Log.w("GCParser.parseTrackable: Failed to parse trackable owner name", e);
- }
-
- // trackable origin
- trackable.setOrigin(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ORIGIN, true, trackable.getOrigin()));
-
- // trackable spotted
- try {
- final MatcherWrapper matcherSpottedCache = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDCACHE, page);
- if (matcherSpottedCache.find() && matcherSpottedCache.groupCount() > 0) {
- trackable.setSpottedGuid(matcherSpottedCache.group(1));
- trackable.setSpottedName(matcherSpottedCache.group(2).trim());
- trackable.setSpottedType(Trackable.SPOTTED_CACHE);
- }
-
- final MatcherWrapper matcherSpottedUser = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDUSER, page);
- if (matcherSpottedUser.find() && matcherSpottedUser.groupCount() > 0) {
- trackable.setSpottedGuid(matcherSpottedUser.group(1));
- trackable.setSpottedName(matcherSpottedUser.group(2).trim());
- trackable.setSpottedType(Trackable.SPOTTED_USER);
- }
-
- if (TextUtils.matches(page, GCConstants.PATTERN_TRACKABLE_SPOTTEDUNKNOWN)) {
- trackable.setSpottedType(Trackable.SPOTTED_UNKNOWN);
- }
-
- if (TextUtils.matches(page, GCConstants.PATTERN_TRACKABLE_SPOTTEDOWNER)) {
- trackable.setSpottedType(Trackable.SPOTTED_OWNER);
- }
- } catch (final RuntimeException e) {
- // 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(DATE_TB_IN_1.parse(releaseString));
- } catch (final ParseException ignored) {
- if (trackable.getReleased() == null) {
- try {
- trackable.setReleased(DATE_TB_IN_2.parse(releaseString));
- } catch (final ParseException e) {
- Log.e("Could not parse trackable release " + releaseString, e);
- }
- }
- }
- }
-
- // trackable distance
- final String distance = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_DISTANCE, false, null);
- if (null != distance) {
- try {
- trackable.setDistance(DistanceParser.parseDistance(distance,
- !Settings.useImperialUnits()));
- } catch (final NumberFormatException e) {
- Log.e("GCParser.parseTrackable: Failed to parse distance", e);
- }
- }
-
- // trackable goal
- trackable.setGoal(HtmlUtils.removeExtraParagraph(convertLinks(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GOAL, true, trackable.getGoal()))));
-
- // trackable details & image
- try {
- final MatcherWrapper matcherDetailsImage = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_DETAILSIMAGE, page);
- if (matcherDetailsImage.find() && matcherDetailsImage.groupCount() > 0) {
- final String image = StringUtils.trim(matcherDetailsImage.group(3));
- final String details = StringUtils.trim(matcherDetailsImage.group(4));
-
- if (StringUtils.isNotEmpty(image)) {
- trackable.setImage(StringUtils.replace(image, "/display/", "/large/"));
- }
- if (StringUtils.isNotEmpty(details) && !StringUtils.equals(details, "No additional details available.")) {
- 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", e);
- }
- if (StringUtils.isEmpty(trackable.getDetails()) && page.contains(GCConstants.ERROR_TB_NOT_ACTIVATED)) {
- trackable.setDetails(CgeoApplication.getInstance().getString(R.string.trackable_not_activated));
- }
-
- // trackable logs
- try {
- final MatcherWrapper matcherLogs = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG, page);
- /*
- * 1. Type (image)
- * 2. Date
- * 3. Author
- * 4. Cache-GUID
- * 5. <ignored> (strike-through property for ancient caches)
- * 6. Cache-name
- * 7. Log text
- */
- while (matcherLogs.find()) {
- long date = 0;
- try {
- date = GCLogin.parseGcCustomDate(matcherLogs.group(2)).getTime();
- } catch (final ParseException ignored) {
- }
-
- final LogEntry logDone = new LogEntry(
- Html.fromHtml(matcherLogs.group(3)).toString().trim(),
- date,
- LogType.getByIconName(matcherLogs.group(1)),
- matcherLogs.group(7).trim());
-
- if (matcherLogs.group(4) != null && matcherLogs.group(6) != null) {
- logDone.cacheGuid = matcherLogs.group(4);
- logDone.cacheName = matcherLogs.group(6);
- }
-
- // Apply the pattern for images in a trackable log entry against each full log (group(0))
- final String logEntry = matcherLogs.group(0);
- final MatcherWrapper matcherLogImages = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG_IMAGES, logEntry);
- /*
- * 1. Image URL
- * 2. Image title
- */
- while (matcherLogImages.find()) {
- final Image logImage = new Image.Builder()
- .setUrl(matcherLogImages.group(1))
- .setTitle(matcherLogImages.group(2))
- .build();
- logDone.addLogImage(logImage);
- }
-
- trackable.getLogs().add(logDone);
- }
- } catch (final Exception e) {
- // failed to parse logs
- Log.w("GCParser.parseCache: Failed to parse cache logs", e);
- }
-
- // tracking code
- if (!StringUtils.equalsIgnoreCase(trackable.getGeocode(), possibleTrackingcode)) {
- trackable.setTrackingcode(possibleTrackingcode);
- }
-
- if (CgeoApplication.getInstance() != null) {
- DataStore.saveTrackable(trackable);
- }
-
- return trackable;
- }
-
- private static String convertLinks(final String input) {
- if (input == null) {
- return null;
- }
- return StringUtils.replace(input, "../", GCConstants.GC_URL);
- }
-
- private enum Logs {
- ALL(null),
- FRIENDS("sf"),
- OWN("sp");
-
- final String paramName;
-
- Logs(final String paramName) {
- this.paramName = paramName;
- }
-
- private String getParamName() {
- return this.paramName;
- }
- }
-
- /**
- * Extract special logs (friends, own) through separate 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 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 Parameters params = new Parameters(
- "tkn", userToken,
- "idx", "1",
- "num", String.valueOf(GCConstants.NUMBER_OF_LOGS),
- "decrypt", "true");
- if (logType != Logs.ALL) {
- params.add(logType.getParamName(), Boolean.toString(Boolean.TRUE));
- }
- final HttpResponse response = Network.getRequest("http://www.geocaching.com/seek/geocache.logbook", params);
- if (response == null) {
- Log.e("GCParser.loadLogsFromDetails: cannot log logs, response is null");
- return Observable.empty();
- }
- final int statusCode = response.getStatusLine().getStatusCode();
- if (statusCode != 200) {
- Log.e("GCParser.loadLogsFromDetails: error " + statusCode + " when requesting log information");
- return Observable.empty();
- }
- 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, responseStream);
- }
- }).subscribeOn(RxUtils.networkScheduler);
- }
-
- 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) {
- try {
- 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 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.path("LogTypeImage").asText(".gif");
- final String logIconName = logIconNameExt.substring(0, logIconNameExt.length() - 4);
-
- final long date;
- try {
- 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.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.path("UserName").asText()),
- date,
- LogType.getByIconName(logIconName),
- logText);
- logDone.found = entry.path("GeocacheFindCount").asInt();
- logDone.friend = markAsFriendsLog;
-
- 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.Builder().setUrl(url).setTitle(title).build();
- logDone.addLogImage(logImage);
- }
-
- subscriber.onNext(logDone);
- }
- } catch (final IOException e) {
- Log.w("GCParser.loadLogsFromDetails: Failed to parse cache logs", e);
- }
- subscriber.onCompleted();
- }
- });
- }
-
- @NonNull
- static List<LogType> parseTypes(final String page) {
- if (StringUtils.isEmpty(page)) {
- return Collections.emptyList();
- }
-
- final List<LogType> types = new ArrayList<>();
-
- final MatcherWrapper typeBoxMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPEBOX, page);
- if (typeBoxMatcher.find() && typeBoxMatcher.groupCount() > 0) {
- final String typesText = typeBoxMatcher.group(1);
- final MatcherWrapper typeMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPE2, typesText);
- while (typeMatcher.find()) {
- if (typeMatcher.groupCount() > 1) {
- try {
- final int type = Integer.parseInt(typeMatcher.group(2));
- if (type > 0) {
- types.add(LogType.getById(type));
- }
- } catch (final NumberFormatException e) {
- Log.e("Error parsing log types", e);
- }
- }
- }
- }
-
- // we don't support this log type
- types.remove(LogType.UPDATE_COORDINATES);
-
- return types;
- }
-
- @NonNull
- public static List<LogTypeTrackable> parseLogTypesTrackables(final String page) {
- if (StringUtils.isEmpty(page)) {
- return new ArrayList<>();
- }
-
- final List<LogTypeTrackable> types = new ArrayList<>();
-
- final MatcherWrapper typeBoxMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPEBOX, page);
- if (typeBoxMatcher.find() && typeBoxMatcher.groupCount() > 0) {
- final String typesText = typeBoxMatcher.group(1);
- final MatcherWrapper typeMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPE2, typesText);
- while (typeMatcher.find()) {
- if (typeMatcher.groupCount() > 1) {
- try {
- final int type = Integer.parseInt(typeMatcher.group(2));
- if (type > 0) {
- types.add(LogTypeTrackable.getById(type));
- }
- } catch (final NumberFormatException e) {
- Log.e("Error parsing trackable log types", e);
- }
- }
- }
- }
- return types;
- }
-
- static List<TrackableLog> parseTrackableLog(final String page) {
- if (StringUtils.isEmpty(page)) {
- return Collections.emptyList();
- }
-
- String table = StringUtils.substringBetween(page, "<table id=\"tblTravelBugs\"", "</table>");
-
- // if no trackables are currently in the account, the table is not available, so return an empty list instead of null
- if (StringUtils.isBlank(table)) {
- return Collections.emptyList();
- }
-
- table = StringUtils.substringBetween(table, "<tbody>", "</tbody>");
- if (StringUtils.isBlank(table)) {
- Log.e("GCParser.parseTrackableLog: tbody not found on page");
- return Collections.emptyList();
- }
-
- final List<TrackableLog> trackableLogs = new ArrayList<>();
-
- final MatcherWrapper trackableMatcher = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE, page);
- while (trackableMatcher.find()) {
- if (trackableMatcher.groupCount() > 0) {
-
- final String trackCode = trackableMatcher.group(1);
- final String name = Html.fromHtml(trackableMatcher.group(2)).toString();
- try {
- final Integer ctl = Integer.valueOf(trackableMatcher.group(3));
- final Integer id = Integer.valueOf(trackableMatcher.group(5));
- if (trackCode != null && ctl != null && id != null) {
- final TrackableLog entry = new TrackableLog("", trackCode, name, id, ctl, TrackableBrand.TRAVELBUG);
-
- Log.i("Trackable in inventory (#" + entry.ctl + "/" + entry.id + "): " + entry.trackCode + " - " + entry.name);
- trackableLogs.add(entry);
- }
- } catch (final NumberFormatException e) {
- Log.e("GCParser.parseTrackableLog", e);
- }
- }
- }
-
- return trackableLogs;
- }
-
- /**
- * Insert the right cache type restriction in parameters
- *
- * @param params
- * the parameters to insert the restriction into
- * @param cacheType
- * the type of cache, or null to include everything
- */
- static private void insertCacheType(final Parameters params, final CacheType cacheType) {
- params.put("tx", cacheType.guid);
- }
-
- private static void getExtraOnlineInfo(final Geocache cache, final String page, final CancellableHandler handler) {
- // This method starts the page parsing for logs in the background, as well as retrieve the friends and own logs
- // if requested. It merges them and stores them in the background, while the rating is retrieved if needed and
- // stored. Then we wait for the log merging and saving to be completed before returning.
- if (CancellableHandler.isCancelled(handler)) {
- return;
- }
-
- 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
- public List<LogEntry> call(final List<LogEntry> logEntries, final List<LogEntry> specialLogEntries) {
- mergeFriendsLogs(logEntries, specialLogEntries);
- return logEntries;
- }
- }).cache();
- mergedLogs.subscribe(new Action1<List<LogEntry>>() {
- @Override
- public void call(final List<LogEntry> 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);
- final GCVoteRating rating = GCVote.getRating(cache.getGuid(), cache.getGeocode());
- if (rating != null) {
- cache.setRating(rating.getRating());
- cache.setVotes(rating.getVotes());
- cache.setMyVote(rating.getMyVote());
- }
- }
-
- // Wait for completion of logs parsing, retrieving and merging
- RxUtils.waitForCompletion(mergedLogs);
- }
-
- /**
- * Merge log entries and mark them as friends logs (personal and friends) to identify
- * them on friends/personal logs tab.
- *
- * @param mergedLogs
- * the list to merge logs with
- * @param logsToMerge
- * the list of logs to merge
- */
- private static void mergeFriendsLogs(final List<LogEntry> mergedLogs, final Iterable<LogEntry> logsToMerge) {
- for (final LogEntry log : logsToMerge) {
- if (mergedLogs.contains(log)) {
- mergedLogs.get(mergedLogs.indexOf(log)).friend = true;
- } else {
- mergedLogs.add(log);
- }
- }
- }
-
- static boolean uploadModifiedCoordinates(final Geocache cache, final Geopoint wpt) {
- return editModifiedCoordinates(cache, wpt);
- }
-
- static boolean deleteModifiedCoordinates(final Geocache cache) {
- return editModifiedCoordinates(cache, null);
- }
-
- static boolean editModifiedCoordinates(final Geocache cache, final Geopoint wpt) {
- final String userToken = getUserToken(cache);
- if (StringUtils.isEmpty(userToken)) {
- return false;
- }
-
- 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 uriSuffix = wpt != null ? "SetUserCoordinate" : "ResetUserCoordinate";
-
- final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/";
- final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo);
-
- 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;
- }
-
- static boolean uploadPersonalNote(final Geocache cache) {
- final String userToken = getUserToken(cache);
- if (StringUtils.isEmpty(userToken)) {
- return false;
- }
-
- final ObjectNode jo = new ObjectNode(JsonUtils.factory);
- jo.putObject("dto").put("et", StringUtils.defaultString(cache.getPersonalNote())).put("ut", userToken);
-
- final String uriSuffix = "SetUserCacheNote";
-
- final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/";
- final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo);
-
- 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;
- }
-
- 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\">");
- }
-}
+package cgeo.geocaching.connector.gc;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.DataStore;
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.Image;
+import cgeo.geocaching.LogEntry;
+import cgeo.geocaching.PocketQueryList;
+import cgeo.geocaching.R;
+import cgeo.geocaching.SearchResult;
+import cgeo.geocaching.Trackable;
+import cgeo.geocaching.TrackableLog;
+import cgeo.geocaching.Waypoint;
+import cgeo.geocaching.connector.trackable.TrackableBrand;
+import cgeo.geocaching.enumerations.CacheSize;
+import cgeo.geocaching.enumerations.CacheType;
+import cgeo.geocaching.enumerations.LoadFlags;
+import cgeo.geocaching.enumerations.LoadFlags.SaveFlag;
+import cgeo.geocaching.enumerations.LogType;
+import cgeo.geocaching.enumerations.LogTypeTrackable;
+import cgeo.geocaching.enumerations.StatusCode;
+import cgeo.geocaching.enumerations.WaypointType;
+import cgeo.geocaching.files.LocParser;
+import cgeo.geocaching.gcvote.GCVote;
+import cgeo.geocaching.gcvote.GCVoteRating;
+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.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;
+import cgeo.geocaching.utils.SynchronizedDateFormat;
+import cgeo.geocaching.utils.TextUtils;
+
+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 android.net.Uri;
+import android.text.Html;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.Collator;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import rx.Observable;
+import rx.Observable.OnSubscribe;
+import rx.Subscriber;
+import rx.functions.Action1;
+import rx.functions.Func0;
+import rx.functions.Func2;
+import rx.schedulers.Schedulers;
+
+public final class GCParser {
+ @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);
+
+ private GCParser() {
+ // Utility class
+ }
+
+ @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");
+ return null;
+ }
+
+ final List<String> cids = new ArrayList<>();
+ String page = pageContent;
+
+ final SearchResult searchResult = new SearchResult();
+ searchResult.setUrl(url);
+ searchResult.setViewstates(GCLogin.getViewstates(page));
+
+ // recaptcha
+ if (showCaptcha) {
+ final String recaptchaJsParam = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_RECAPTCHA, false, null);
+
+ if (recaptchaJsParam != null) {
+ recaptchaReceiver.setKey(recaptchaJsParam.trim());
+
+ recaptchaReceiver.fetchChallenge();
+ }
+ if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) {
+ recaptchaReceiver.notifyNeed();
+ }
+ }
+
+ if (!page.contains("SearchResultsTable")) {
+ // there are no results. aborting here avoids a wrong error log in the next parsing step
+ return searchResult;
+ }
+
+ int startPos = page.indexOf("<div id=\"ctl00_ContentBody_ResultsPanel\"");
+ if (startPos == -1) {
+ Log.e("GCParser.parseSearch: ID \"ctl00_ContentBody_dlResults\" not found on page");
+ return null;
+ }
+
+ page = page.substring(startPos); // cut on <table
+
+ startPos = page.indexOf('>');
+ final int endPos = page.indexOf("ctl00_ContentBody_UnitTxt");
+ if (startPos == -1 || endPos == -1) {
+ Log.e("GCParser.parseSearch: ID \"ctl00_ContentBody_UnitTxt\" not found on page");
+ return null;
+ }
+
+ page = page.substring(startPos + 1, endPos - startPos + 1); // cut between <table> and </table>
+
+ final String[] rows = StringUtils.splitByWholeSeparator(page, "<tr class=");
+ final int rowsCount = rows.length;
+
+ int excludedCaches = 0;
+ final List<Geocache> caches = new ArrayList<>();
+ for (int z = 1; z < rowsCount; z++) {
+ final Geocache cache = new Geocache();
+ final String row = rows[z];
+
+ // check for cache type presence
+ if (!row.contains("images/wpttypes")) {
+ continue;
+ }
+
+ try {
+ final MatcherWrapper matcherGuidAndDisabled = new MatcherWrapper(GCConstants.PATTERN_SEARCH_GUIDANDDISABLED, row);
+
+ while (matcherGuidAndDisabled.find()) {
+ if (matcherGuidAndDisabled.groupCount() > 0) {
+ if (matcherGuidAndDisabled.group(2) != null) {
+ cache.setName(Html.fromHtml(matcherGuidAndDisabled.group(2).trim()).toString());
+ }
+ if (matcherGuidAndDisabled.group(3) != null) {
+ cache.setLocation(Html.fromHtml(matcherGuidAndDisabled.group(3).trim()).toString());
+ }
+
+ final String attr = matcherGuidAndDisabled.group(1);
+ if (attr != null) {
+ cache.setDisabled(attr.contains("Strike"));
+
+ cache.setArchived(attr.contains("OldWarning"));
+ }
+ }
+ }
+ } catch (final RuntimeException e) {
+ // failed to parse GUID and/or Disabled
+ Log.w("GCParser.parseSearch: Failed to parse GUID and/or Disabled data", e);
+ }
+
+ if (Settings.isExcludeDisabledCaches() && (cache.isDisabled() || cache.isArchived())) {
+ // skip disabled and archived caches
+ excludedCaches++;
+ continue;
+ }
+
+ 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, null)));
+
+ // cache direction - image
+ if (Settings.getLoadDirImg()) {
+ final String direction = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, null);
+ if (direction != null) {
+ cache.setDirectionImg(direction);
+ }
+ }
+
+ // cache distance - estimated distance for basic members
+ final String distance = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_DIRECTION_DISTANCE, false, 2, null, false);
+ if (distance != null) {
+ cache.setDistance(DistanceParser.parseDistance(distance,
+ !Settings.useImperialUnits()));
+ }
+
+ // difficulty/terrain
+ final MatcherWrapper matcherDT = new MatcherWrapper(GCConstants.PATTERN_SEARCH_DIFFICULTY_TERRAIN, row);
+ if (matcherDT.find()) {
+ final Float difficulty = parseStars(matcherDT.group(1));
+ if (difficulty != null) {
+ cache.setDifficulty(difficulty);
+ }
+ final Float terrain = parseStars(matcherDT.group(3));
+ if (terrain != null) {
+ cache.setTerrain(terrain);
+ }
+ }
+
+ // size
+ 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, null);
+ if (StringUtils.isNotBlank(dateHidden)) {
+ try {
+ final Date date = GCLogin.parseGcCustomDate(dateHidden);
+ if (date != null) {
+ cache.setHidden(date);
+ }
+ } catch (final ParseException e) {
+ Log.e("Error parsing event date from search", e);
+ }
+ }
+
+ // cache inventory
+ final MatcherWrapper matcherTbs = new MatcherWrapper(GCConstants.PATTERN_SEARCH_TRACKABLES, row);
+ String inventoryPre = null;
+ while (matcherTbs.find()) {
+ if (matcherTbs.groupCount() > 0) {
+ try {
+ cache.setInventoryItems(Integer.parseInt(matcherTbs.group(1)));
+ } catch (final NumberFormatException e) {
+ Log.e("Error parsing trackables count", e);
+ }
+ inventoryPre = matcherTbs.group(2);
+ }
+ }
+
+ if (StringUtils.isNotBlank(inventoryPre)) {
+ assert inventoryPre != null;
+ final MatcherWrapper matcherTbsInside = new MatcherWrapper(GCConstants.PATTERN_SEARCH_TRACKABLESINSIDE, inventoryPre);
+ while (matcherTbsInside.find()) {
+ if (matcherTbsInside.groupCount() == 2 &&
+ matcherTbsInside.group(2) != null &&
+ !matcherTbsInside.group(2).equalsIgnoreCase("premium member only cache") &&
+ cache.getInventoryItems() <= 0) {
+ cache.setInventoryItems(1);
+ }
+ }
+ }
+
+ // premium cache
+ cache.setPremiumMembersOnly(row.contains("/images/icons/16/premium_only.png"));
+
+ // found it
+ cache.setFound(row.contains("/images/icons/16/found.png") || row.contains("uxUserLogDate\" class=\"Success\""));
+
+ // id
+ String result = TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_ID, null);
+ if (null != result) {
+ cache.setCacheId(result);
+ cids.add(cache.getCacheId());
+ }
+
+ // favorite count
+ try {
+ result = getNumberString(TextUtils.getMatch(row, GCConstants.PATTERN_SEARCH_FAVORITE, false, 1, null, true));
+ if (null != result) {
+ cache.setFavoritePoints(Integer.parseInt(result));
+ }
+ } catch (final NumberFormatException e) {
+ Log.w("GCParser.parseSearch: Failed to parse favorite count", e);
+ }
+
+ caches.add(cache);
+ }
+ searchResult.addAndPutInCache(caches);
+
+ // total caches found
+ try {
+ final String result = TextUtils.getMatch(page, GCConstants.PATTERN_SEARCH_TOTALCOUNT, false, 1, null, true);
+ if (null != result) {
+ searchResult.setTotalCountGC(Integer.parseInt(result) - excludedCaches);
+ }
+ } catch (final NumberFormatException e) {
+ Log.w("GCParser.parseSearch: Failed to parse cache count", e);
+ }
+
+ String recaptchaText = null;
+ if (recaptchaReceiver != null && StringUtils.isNotBlank(recaptchaReceiver.getChallenge())) {
+ recaptchaReceiver.waitForUser();
+ recaptchaText = recaptchaReceiver.getText();
+ }
+
+ 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
+ final Parameters params = new Parameters(
+ "__EVENTTARGET", "",
+ "__EVENTARGUMENT", "");
+ GCLogin.putViewstates(params, searchResult.getViewstates());
+ for (final String cid : cids) {
+ params.put("CID", cid);
+ }
+
+ if (StringUtils.isNotBlank(recaptchaText)) {
+ assert recaptchaReceiver != null; // Help Eclipse here. If recaptchaReceiver could be null, recaptchaText would have stayed null also.
+ params.put("recaptcha_challenge_field", recaptchaReceiver.getChallenge());
+ params.put("recaptcha_response_field", recaptchaText);
+ }
+ params.put("Download", "Download Waypoints");
+
+ // retrieve target url
+ final String queryUrl = TextUtils.getMatch(pageContent, GCConstants.PATTERN_SEARCH_POST_ACTION, "");
+
+ if (StringUtils.isEmpty(queryUrl)) {
+ Log.w("Loc download url not found");
+ } else {
+
+ final String coordinates = Network.getResponseData(Network.postRequest("http://www.geocaching.com/seek/" + queryUrl, params), false);
+
+ 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;
+ }
+
+ LocParser.parseLoc(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
+ if (parsed.left != StatusCode.NO_ERROR) {
+ return new SearchResult(parsed.left);
+ }
+
+ final Geocache cache = parsed.right;
+ getExtraOnlineInfo(cache, page, handler);
+ // too late: it is already stored through parseCacheFromText
+ cache.setDetailedUpdatedNow();
+ if (CancellableHandler.isCancelled(handler)) {
+ return null;
+ }
+
+ // save full detailed caches
+ CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_cache);
+ DataStore.saveCache(cache, EnumSet.of(SaveFlag.DB));
+
+ // update progress message so user knows we're still working. This is more of a place holder than
+ // actual indication of what the program is doing
+ CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_render);
+ 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.saveLogs(parsed.right.getGeocode(), getLogs(parseUserToken(page), Logs.ALL).toBlocking().toIterable());
+ }
+ return result;
+ }
+
+ /**
+ * 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-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 UNKNOWN_PARSE_ERROR;
+ }
+
+ if (pageIn.contains(GCConstants.STRING_UNPUBLISHED_OTHER) || pageIn.contains(GCConstants.STRING_UNPUBLISHED_FROM_SEARCH)) {
+ return ImmutablePair.of(StatusCode.UNPUBLISHED_CACHE, null);
+ }
+
+ if (pageIn.contains(GCConstants.STRING_PREMIUMONLY_1) || pageIn.contains(GCConstants.STRING_PREMIUMONLY_2)) {
+ return ImmutablePair.of(StatusCode.PREMIUM_ONLY, null);
+ }
+
+ final String cacheName = Html.fromHtml(TextUtils.getMatch(pageIn, GCConstants.PATTERN_NAME, true, "")).toString();
+ if (GCConstants.STRING_UNKNOWN_ERROR.equalsIgnoreCase(cacheName)) {
+ 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 = "";
+ final MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_PERSONALNOTE, pageIn);
+ if (matcher.find()) {
+ personalNoteWithLineBreaks = matcher.group(1).trim();
+ }
+
+ final String page = TextUtils.replaceWhitespace(pageIn);
+
+ final Geocache cache = new Geocache();
+ cache.setDisabled(page.contains(GCConstants.STRING_DISABLED));
+
+ cache.setArchived(page.contains(GCConstants.STRING_ARCHIVED));
+
+ cache.setPremiumMembersOnly(TextUtils.matches(page, GCConstants.PATTERN_PREMIUMMEMBERS));
+
+ cache.setFavorite(TextUtils.matches(page, GCConstants.PATTERN_FAVORITE));
+
+ // cache geocode
+ cache.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_GEOCODE, true, cache.getGeocode()));
+
+ // cache id
+ cache.setCacheId(TextUtils.getMatch(page, GCConstants.PATTERN_CACHEID, true, cache.getCacheId()));
+
+ // cache guid
+ cache.setGuid(TextUtils.getMatch(page, GCConstants.PATTERN_GUID, true, cache.getGuid()));
+
+ // name
+ cache.setName(cacheName);
+
+ // owner real name
+ cache.setOwnerUserId(Network.decode(TextUtils.getMatch(page, GCConstants.PATTERN_OWNER_USERID, true, cache.getOwnerUserId())));
+
+ cache.setUserModifiedCoords(false);
+
+ String tableInside = page;
+
+ final int pos = tableInside.indexOf(GCConstants.STRING_CACHEDETAILS);
+ if (pos == -1) {
+ Log.e("GCParser.parseCache: ID \"cacheDetails\" not found on page");
+ return UNKNOWN_PARSE_ERROR;
+ }
+
+ tableInside = tableInside.substring(pos);
+
+ if (StringUtils.isNotBlank(tableInside)) {
+ // cache terrain
+ String result = TextUtils.getMatch(tableInside, GCConstants.PATTERN_TERRAIN, true, null);
+ if (result != null) {
+ try {
+ cache.setTerrain(Float.parseFloat(StringUtils.replaceChars(result, '_', '.')));
+ } catch (final NumberFormatException e) {
+ Log.e("Error parsing terrain value", e);
+ }
+ }
+
+ // cache difficulty
+ result = TextUtils.getMatch(tableInside, GCConstants.PATTERN_DIFFICULTY, true, null);
+ if (result != null) {
+ try {
+ cache.setDifficulty(Float.parseFloat(StringUtils.replaceChars(result, '_', '.')));
+ } catch (final NumberFormatException e) {
+ Log.e("Error parsing difficulty value", e);
+ }
+ }
+
+ // owner
+ cache.setOwnerDisplayName(StringEscapeUtils.unescapeHtml4(TextUtils.getMatch(tableInside, GCConstants.PATTERN_OWNER_DISPLAYNAME, true, cache.getOwnerDisplayName())));
+
+ // hidden
+ try {
+ String hiddenString = TextUtils.getMatch(tableInside, GCConstants.PATTERN_HIDDEN, true, null);
+ if (StringUtils.isNotBlank(hiddenString)) {
+ cache.setHidden(GCLogin.parseGcCustomDate(hiddenString));
+ }
+ if (cache.getHiddenDate() == null) {
+ // event date
+ hiddenString = TextUtils.getMatch(tableInside, GCConstants.PATTERN_HIDDENEVENT, true, null);
+ if (StringUtils.isNotBlank(hiddenString)) {
+ cache.setHidden(GCLogin.parseGcCustomDate(hiddenString));
+ }
+ }
+ } catch (final ParseException e) {
+ // failed to parse cache hidden date
+ Log.w("GCParser.parseCache: Failed to parse cache hidden (event) date", e);
+ }
+
+ // favorite
+ try {
+ cache.setFavoritePoints(Integer.parseInt(TextUtils.getMatch(tableInside, GCConstants.PATTERN_FAVORITECOUNT, true, "0")));
+ } catch (final NumberFormatException e) {
+ Log.e("Error parsing favorite count", e);
+ }
+
+ // cache size
+ cache.setSize(CacheSize.getById(TextUtils.getMatch(tableInside, GCConstants.PATTERN_SIZE, true, CacheSize.NOT_CHOSEN.id)));
+ }
+
+ // cache found
+ cache.setFound(TextUtils.matches(page, GCConstants.PATTERN_FOUND) || TextUtils.matches(page, GCConstants.PATTERN_FOUND_ALTERNATIVE));
+
+ // cache type
+ cache.setType(CacheType.getByGuid(TextUtils.getMatch(page, GCConstants.PATTERN_TYPE, true, cache.getType().id)));
+
+ // on watchlist
+ cache.setOnWatchlist(TextUtils.matches(page, GCConstants.PATTERN_WATCHLIST));
+
+ // latitude and longitude. Can only be retrieved if user is logged in
+ String latlon = TextUtils.getMatch(page, GCConstants.PATTERN_LATLON, true, "");
+ if (StringUtils.isNotEmpty(latlon)) {
+ try {
+ cache.setCoords(new Geopoint(latlon));
+ cache.setReliableLatLon(true);
+ } catch (final Geopoint.GeopointException e) {
+ Log.w("GCParser.parseCache: Failed to parse cache coordinates", e);
+ }
+ }
+
+ // cache location
+ cache.setLocation(TextUtils.getMatch(page, GCConstants.PATTERN_LOCATION, true, ""));
+
+ // cache hint
+ final String result = TextUtils.getMatch(page, GCConstants.PATTERN_HINT, false, null);
+ if (result != null) {
+ // replace linebreak and paragraph tags
+ final String hint = GCConstants.PATTERN_LINEBREAK.matcher(result).replaceAll("\n");
+ cache.setHint(StringUtils.replace(hint, "</p>", "").trim());
+ }
+
+ cache.checkFields();
+
+ // cache personal note
+ cache.setPersonalNote(personalNoteWithLineBreaks);
+
+ // cache short description
+ cache.setShortDescription(TextUtils.getMatch(page, GCConstants.PATTERN_SHORTDESC, true, ""));
+
+ // cache description
+ 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("<br/><br/><a href=\"%s\"><b>%s</b></a>", relatedWebPage, relatedWebPage);
+ }
+ cache.setDescription(longDescription + relatedWebPage);
+
+ // cache attributes
+ try {
+ final List<String> attributes = new ArrayList<>();
+ final String attributesPre = TextUtils.getMatch(page, GCConstants.PATTERN_ATTRIBUTES, true, null);
+ if (attributesPre != null) {
+ final MatcherWrapper matcherAttributesInside = new MatcherWrapper(GCConstants.PATTERN_ATTRIBUTESINSIDE, attributesPre);
+
+ while (matcherAttributesInside.find()) {
+ if (matcherAttributesInside.groupCount() > 1 && !matcherAttributesInside.group(2).equalsIgnoreCase("blank")) {
+ // by default, use the tooltip of the attribute
+ String attribute = matcherAttributesInside.group(2).toLowerCase(Locale.US);
+
+ // if the image name can be recognized, use the image name as attribute
+ final String imageName = matcherAttributesInside.group(1).trim();
+ if (StringUtils.isNotEmpty(imageName)) {
+ final int start = imageName.lastIndexOf('/');
+ final int end = imageName.lastIndexOf('.');
+ if (start >= 0 && end >= 0) {
+ attribute = imageName.substring(start + 1, end).replace('-', '_').toLowerCase(Locale.US);
+ }
+ }
+ attributes.add(attribute);
+ }
+ }
+ }
+ cache.setAttributes(attributes);
+ } catch (final RuntimeException e) {
+ // failed to parse cache attributes
+ Log.w("GCParser.parseCache: Failed to parse cache attributes", e);
+ }
+
+ // cache spoilers
+ try {
+ if (CancellableHandler.isCancelled(handler)) {
+ return UNKNOWN_PARSE_ERROR;
+ }
+ CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_spoilers);
+
+ final MatcherWrapper matcherSpoilersInside = new MatcherWrapper(GCConstants.PATTERN_SPOILER_IMAGE, page);
+
+ while (matcherSpoilersInside.find()) {
+ // the original spoiler URL (include .../display/... contains a low-resolution image
+ // if we shorten the URL we get the original-resolution image
+ final String url = matcherSpoilersInside.group(1).replace("/display", "");
+
+ String title = null;
+ if (matcherSpoilersInside.group(3) != null) {
+ title = matcherSpoilersInside.group(3);
+ }
+ String description = null;
+ if (matcherSpoilersInside.group(4) != null) {
+ description = matcherSpoilersInside.group(4);
+ }
+ cache.addSpoiler(new Image.Builder().setUrl(url).setTitle(title).setDescription(description).build());
+ }
+ } catch (final RuntimeException e) {
+ // failed to parse cache spoilers
+ Log.w("GCParser.parseCache: Failed to parse cache spoilers", e);
+ }
+
+ // cache inventory
+ try {
+ final MatcherWrapper matcherInventory = new MatcherWrapper(GCConstants.PATTERN_INVENTORY, page);
+ if (matcherInventory.find()) {
+ if (matcherInventory.groupCount() > 1) {
+ final String inventoryPre = matcherInventory.group(2);
+
+ final ArrayList<Trackable> inventory = new ArrayList<>();
+ if (StringUtils.isNotBlank(inventoryPre)) {
+ final MatcherWrapper matcherInventoryInside = new MatcherWrapper(GCConstants.PATTERN_INVENTORYINSIDE, inventoryPre);
+
+ while (matcherInventoryInside.find()) {
+ if (matcherInventoryInside.groupCount() > 0) {
+ final Trackable inventoryItem = new Trackable();
+ inventoryItem.forceSetBrand(TrackableBrand.TRAVELBUG);
+ inventoryItem.setGuid(matcherInventoryInside.group(1));
+ inventoryItem.setName(matcherInventoryInside.group(2));
+
+ inventory.add(inventoryItem);
+ }
+ }
+ }
+ cache.mergeInventory(inventory, EnumSet.of(TrackableBrand.TRAVELBUG));
+ }
+ }
+ } catch (final RuntimeException e) {
+ // failed to parse cache inventory
+ Log.w("GCParser.parseCache: Failed to parse cache inventory (2)", e);
+ }
+
+ // cache logs counts
+ try {
+ final String countlogs = TextUtils.getMatch(page, GCConstants.PATTERN_COUNTLOGS, true, null);
+ if (null != countlogs) {
+ final MatcherWrapper matcherLog = new MatcherWrapper(GCConstants.PATTERN_COUNTLOG, countlogs);
+
+ while (matcherLog.find()) {
+ final String typeStr = matcherLog.group(1);
+ final String countStr = getNumberString(matcherLog.group(2));
+
+ if (StringUtils.isNotBlank(typeStr)
+ && LogType.UNKNOWN != LogType.getByIconName(typeStr)
+ && StringUtils.isNotBlank(countStr)) {
+ cache.getLogCounts().put(LogType.getByIconName(typeStr), Integer.valueOf(countStr));
+ }
+ }
+ }
+ if (cache.getLogCounts().isEmpty()) {
+ Log.w("GCParser.parseCache: Failed to parse cache log count");
+ }
+ } catch (final NumberFormatException e) {
+ // failed to parse logs
+ Log.w("GCParser.parseCache: Failed to parse cache log count", e);
+ }
+
+ // waypoints - reset collection
+ cache.setWaypoints(Collections.<Waypoint> emptyList(), false);
+
+ // add waypoint for original coordinates in case of user-modified listing-coordinates
+ try {
+ final String originalCoords = TextUtils.getMatch(page, GCConstants.PATTERN_LATLON_ORIG, false, null);
+
+ if (null != originalCoords) {
+ final Waypoint waypoint = new Waypoint(CgeoApplication.getInstance().getString(R.string.cache_coordinates_original), WaypointType.ORIGINAL, false);
+ waypoint.setCoords(new Geopoint(originalCoords));
+ cache.addOrChangeWaypoint(waypoint, false);
+ cache.setUserModifiedCoords(true);
+ }
+ } 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 UNKNOWN_PARSE_ERROR;
+ }
+ CancellableHandler.sendLoadProgressDetail(handler, R.string.cache_dialog_loading_details_status_waypoints);
+
+ String wpList = page.substring(wpBegin);
+
+ int wpEnd = wpList.indexOf("</p>");
+ if (wpEnd > -1 && wpEnd <= wpList.length()) {
+ wpList = wpList.substring(0, wpEnd);
+ }
+
+ if (!wpList.contains("No additional waypoints to display.")) {
+ wpEnd = wpList.indexOf("</table>");
+ wpList = wpList.substring(0, wpEnd);
+
+ wpBegin = wpList.indexOf("<tbody>");
+ wpEnd = wpList.indexOf("</tbody>");
+ if (wpBegin >= 0 && wpEnd >= 0 && wpEnd <= wpList.length()) {
+ wpList = wpList.substring(wpBegin + 7, wpEnd);
+ }
+
+ final String[] wpItems = StringUtils.splitByWholeSeparator(wpList, "<tr");
+
+ 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
+ final String name = TextUtils.getMatch(wp[6], GCConstants.PATTERN_WPNAME, true, 1, CgeoApplication.getInstance().getString(R.string.waypoint), true);
+
+ // waypoint type
+ final String resulttype = TextUtils.getMatch(wp[3], GCConstants.PATTERN_WPTYPE, null);
+
+ final Waypoint waypoint = new Waypoint(name, WaypointType.findById(resulttype), false);
+
+ // waypoint prefix
+ waypoint.setPrefix(TextUtils.getMatch(wp[4], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, true, 2, waypoint.getPrefix(), false));
+
+ // waypoint lookup
+ waypoint.setLookup(TextUtils.getMatch(wp[5], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, true, 2, waypoint.getLookup(), false));
+
+ // waypoint latitude and longitude
+ latlon = Html.fromHtml(TextUtils.getMatch(wp[7], GCConstants.PATTERN_WPPREFIXORLOOKUPORLATLON, false, 2, "", false)).toString().trim();
+ if (!StringUtils.startsWith(latlon, "???")) {
+ waypoint.setCoords(new Geopoint(latlon));
+ }
+
+ 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(wpNote[3], GCConstants.PATTERN_WPNOTE, waypoint.getNote()));
+ }
+
+ cache.addOrChangeWaypoint(waypoint, false);
+ }
+ }
+ }
+
+ // last check for necessary cache conditions
+ if (StringUtils.isBlank(cache.getGeocode())) {
+ 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, ".,", "");
+ }
+
+ @Nullable
+ public static SearchResult searchByNextPage(final SearchResult search, final boolean showCaptcha, final RecaptchaReceiver recaptchaReceiver) {
+ if (search == null) {
+ return null;
+ }
+ final String[] viewstates = search.getViewstates();
+
+ final String url = search.getUrl();
+
+ if (StringUtils.isBlank(url)) {
+ Log.e("GCParser.searchByNextPage: No url found");
+ return search;
+ }
+
+ if (GCLogin.isEmpty(viewstates)) {
+ Log.e("GCParser.searchByNextPage: No viewstate given");
+ return search;
+ }
+
+ final Parameters params = new Parameters(
+ "__EVENTTARGET", "ctl00$ContentBody$pgrBottom$ctl08",
+ "__EVENTARGUMENT", "");
+ GCLogin.putViewstates(params, viewstates);
+
+ final String page = GCLogin.getInstance().postRequestLogged(url, params);
+ if (!GCLogin.getInstance().getLoginStatus(page)) {
+ Log.e("GCParser.postLogTrackable: Can not log in geocaching");
+ return search;
+ }
+
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.searchByNextPage: No data from server");
+ return search;
+ }
+
+ final SearchResult searchResult = parseSearch(url, page, showCaptcha, recaptchaReceiver);
+ if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) {
+ Log.w("GCParser.searchByNextPage: No cache parsed");
+ return search;
+ }
+
+ // search results don't need to be filtered so load GCVote ratings here
+ GCVote.loadRatings(new ArrayList<>(searchResult.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB)));
+
+ // save to application
+ search.setError(searchResult.getError());
+ search.setViewstates(searchResult.getViewstates());
+ for (final String geocode : searchResult.getGeocodes()) {
+ search.addGeocode(geocode);
+ }
+ return search;
+ }
+
+ /**
+ * 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 {@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) {
+ if (!my && Settings.isExcludeMyCaches()) {
+ if (params == null) {
+ return new Parameters("f", "1");
+ }
+ params.put("f", "1");
+ Log.i("Skipping caches found or hidden by user.");
+ }
+
+ return params;
+ }
+
+ @Nullable
+ 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 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");
+ return null;
+ }
+ assert page != null;
+
+ final SearchResult searchResult = parseSearch(fullUri, page, showCaptcha, recaptchaReceiver);
+ if (searchResult == null || CollectionUtils.isEmpty(searchResult.getGeocodes())) {
+ Log.e("GCParser.searchByAny: No cache parsed");
+ return searchResult;
+ }
+
+ final SearchResult search = searchResult.filterSearchResults(Settings.isExcludeDisabledCaches(), cacheType);
+
+ GCLogin.getInstance().getLoginStatus(page);
+
+ return search;
+ }
+
+ 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);
+ }
+
+ 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;
+ }
+
+ final Parameters params = new Parameters("key", keyword);
+ return searchByAny(cacheType, false, showCaptcha, params, recaptchaReceiver);
+ }
+
+ private static boolean isSearchForMyCaches(final String userName) {
+ if (userName.equalsIgnoreCase(Settings.getGcCredentials().left)) {
+ Log.i("Overriding users choice because of self search, downloading all caches.");
+ return true;
+ }
+ return false;
+ }
+
+ 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;
+ }
+
+ final Parameters params = new Parameters("ul", userName);
+
+ return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, 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;
+ }
+
+ final Parameters params = new Parameters("pq", pocketGuid);
+
+ return searchByAny(cacheType, false, showCaptcha, params, 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;
+ }
+
+ final Parameters params = new Parameters("u", userName);
+ return searchByAny(cacheType, isSearchForMyCaches(userName), showCaptcha, params, recaptchaReceiver);
+ }
+
+ @Nullable
+ public static Trackable searchTrackable(final String geocode, final String guid, final String id) {
+ if (StringUtils.isBlank(geocode) && StringUtils.isBlank(guid) && StringUtils.isBlank(id)) {
+ Log.w("GCParser.searchTrackable: No geocode nor guid nor id given");
+ return null;
+ }
+
+ Trackable trackable = new Trackable();
+
+ final Parameters params = new Parameters();
+ if (StringUtils.isNotBlank(geocode)) {
+ params.put("tracker", geocode);
+ trackable.setGeocode(geocode);
+ } else if (StringUtils.isNotBlank(guid)) {
+ params.put("guid", guid);
+ } else if (StringUtils.isNotBlank(id)) {
+ params.put("id", id);
+ }
+
+ final String page = GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/track/details.aspx", params);
+
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.searchTrackable: No data from server");
+ return trackable;
+ }
+ assert page != null;
+
+ trackable = parseTrackable(page, geocode);
+ if (trackable == null) {
+ Log.w("GCParser.searchTrackable: No trackable parsed");
+ return null;
+ }
+
+ return trackable;
+ }
+
+ /**
+ * 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);
+
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.searchPocketQueryList: No data from server");
+ return Observable.just(Collections.<PocketQueryList>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());
+ }
+
+ final List<PocketQueryList> list = new ArrayList<>();
+
+ final MatcherWrapper matcherPocket = new MatcherWrapper(GCConstants.PATTERN_LIST_PQ, subPage);
+
+ 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);
+ }
+
+ // just in case, lets sort the resulting list
+ final Collator collator = TextUtils.getCollator();
+ Collections.sort(list, new Comparator<PocketQueryList>() {
+
+ @Override
+ public int compare(final PocketQueryList left, final PocketQueryList right) {
+ return collator.compare(left.getName(), right.getName());
+ }
+ });
+
+ return Observable.just(list);
+ }
+ }).subscribeOn(RxUtils.networkScheduler);
+
+ 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,
+ final String log, final List<TrackableLog> trackables) {
+ if (GCLogin.isEmpty(viewstates)) {
+ Log.e("GCParser.postLog: No viewstate given");
+ return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, "");
+ }
+
+ if (StringUtils.isBlank(log)) {
+ Log.e("GCParser.postLog: No log text given");
+ return new ImmutablePair<>(StatusCode.NO_LOG_TEXT, "");
+ }
+
+ final String logInfo = log.replace("\n", "\r\n").trim(); // windows' eol and remove leading and trailing whitespaces
+
+ Log.i("Trying to post log for cache #" + cacheid + " - action: " + logType
+ + "; date: " + year + "." + month + "." + day + ", log: " + logInfo
+ + "; trackables: " + (trackables != null ? trackables.size() : "0"));
+
+ final Parameters params = new Parameters(
+ "__EVENTTARGET", "",
+ "__EVENTARGUMENT", "",
+ "__LASTFOCUS", "",
+ "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id),
+ "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),
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged", String.format("%02d", month) + "/" + String.format("%02d", day) + "/" + String.format("%04d", year),
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Month", Integer.toString(month),
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day),
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Year", Integer.toString(year),
+ "ctl00$ContentBody$LogBookPanel1$LogButton", "Submit Log Entry",
+ "ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo,
+ "ctl00$ContentBody$LogBookPanel1$btnSubmitLog", "Submit Log Entry",
+ "ctl00$ContentBody$LogBookPanel1$uxLogCreationSource", "Old",
+ "ctl00$ContentBody$uxVistOtherListingGC", "");
+ GCLogin.putViewstates(params, viewstates);
+ if (trackables != null && !trackables.isEmpty()) { // we have some trackables to proceed
+ final StringBuilder hdnSelected = new StringBuilder();
+
+ for (final TrackableLog tb : trackables) {
+ if (tb.action != LogTypeTrackable.DO_NOTHING && tb.brand == TrackableBrand.TRAVELBUG) {
+ hdnSelected.append(Integer.toString(tb.id));
+ hdnSelected.append(tb.action.action);
+ hdnSelected.append(',');
+ }
+ }
+
+ params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnSelectedActions", hdnSelected.toString(), // selected trackables
+ "ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnCurrentFilter", "");
+ }
+
+ final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/seek/log.aspx").encodedQuery("ID=" + cacheid).build().toString();
+ final GCLogin gcLogin = GCLogin.getInstance();
+ String page = gcLogin.postRequestLogged(uri, params);
+ if (!gcLogin.getLoginStatus(page)) {
+ Log.e("GCParser.postLog: Cannot log in geocaching");
+ return new ImmutablePair<>(StatusCode.NOT_LOGGED_IN, "");
+ }
+
+ // maintenance, archived needs to be confirmed
+
+ final MatcherWrapper matcher = new MatcherWrapper(GCConstants.PATTERN_MAINTENANCE, page);
+
+ try {
+ if (matcher.find() && matcher.groupCount() > 0) {
+ final String[] viewstatesConfirm = GCLogin.getViewstates(page);
+
+ if (GCLogin.isEmpty(viewstatesConfirm)) {
+ Log.e("GCParser.postLog: No viewstate for confirm log");
+ return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, "");
+ }
+
+ params.clear();
+ GCLogin.putViewstates(params, viewstatesConfirm);
+ params.put("__EVENTTARGET", "");
+ params.put("__EVENTARGUMENT", "");
+ params.put("__LASTFOCUS", "");
+ params.put("ctl00$ContentBody$LogBookPanel1$btnConfirm", "Yes");
+ params.put("ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo);
+ params.put("ctl00$ContentBody$uxVistOtherListingGC", "");
+ if (trackables != null && !trackables.isEmpty()) { // we have some trackables to proceed
+ final StringBuilder hdnSelected = new StringBuilder();
+
+ for (final TrackableLog tb : trackables) {
+ final String action = Integer.toString(tb.id) + tb.action.action;
+ final StringBuilder paramText = new StringBuilder("ctl00$ContentBody$LogBookPanel1$uxTrackables$repTravelBugs$ctl");
+
+ if (tb.ctl < 10) {
+ paramText.append('0');
+ }
+ paramText.append(tb.ctl).append("$ddlAction");
+ params.put(paramText.toString(), action);
+ if (tb.action != LogTypeTrackable.DO_NOTHING) {
+ hdnSelected.append(action);
+ hdnSelected.append(',');
+ }
+ }
+
+ params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnSelectedActions", hdnSelected.toString()); // selected trackables
+ params.put("ctl00$ContentBody$LogBookPanel1$uxTrackables$hdnCurrentFilter", "");
+ }
+
+ page = Network.getResponseData(Network.postRequest(uri, params));
+ }
+ } catch (final RuntimeException e) {
+ 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);
+ if (matcherOk.find()) {
+ Log.i("Log successfully posted to cache #" + cacheid);
+
+ if (geocode != null) {
+ DataStore.saveVisitDate(geocode);
+ }
+
+ gcLogin.getLoginStatus(page);
+ // the log-successful-page contains still the old value
+ if (gcLogin.getActualCachesFound() >= 0) {
+ gcLogin.setActualCachesFound(gcLogin.getActualCachesFound() + (logType.isFoundLog() ? 1 : 0));
+ }
+
+ final String logID = TextUtils.getMatch(page, GCConstants.PATTERN_LOG_IMAGE_UPLOAD, "");
+
+ return new ImmutablePair<>(StatusCode.NO_ERROR, logID);
+ }
+ } catch (final Exception e) {
+ Log.e("GCParser.postLog.check", e);
+ }
+
+ Log.e("GCParser.postLog: Failed to post log because of unknown error");
+ return new ImmutablePair<>(StatusCode.LOG_POST_ERROR, "");
+ }
+
+ /**
+ * Upload an image to a log that has already been posted
+ *
+ * @param logId
+ * the ID of the log to upload the image to. Found on page returned when log is uploaded
+ * @param image
+ * The Image Object
+ * @return status code to indicate success or failure
+ */
+ static ImmutablePair<StatusCode, String> uploadLogImage(final String logId, @NonNull final Image image) {
+ final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/seek/upload.aspx").encodedQuery("LID=" + logId).build().toString();
+
+ final String page = GCLogin.getInstance().getRequestLogged(uri, null);
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.uploadLogImage: No data from server");
+ return new ImmutablePair<>(StatusCode.UNKNOWN_ERROR, null);
+ }
+ assert page != null;
+
+ final String[] viewstates = GCLogin.getViewstates(page);
+
+ final Parameters uploadParams = new Parameters(
+ "__EVENTTARGET", "",
+ "__EVENTARGUMENT", "",
+ "ctl00$ContentBody$ImageUploadControl1$uxFileCaption", image.getTitle(),
+ "ctl00$ContentBody$ImageUploadControl1$uxFileDesc", image.getDescription(),
+ "ctl00$ContentBody$ImageUploadControl1$uxUpload", "Upload");
+ GCLogin.putViewstates(uploadParams, viewstates);
+
+ final String response = Network.getResponseData(Network.postRequest(uri, uploadParams, "ctl00$ContentBody$ImageUploadControl1$uxFileUpload", "image/jpeg", image.getFile()));
+
+ 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()) {
+ Log.i("Logimage successfully uploaded.");
+ final String uploadedImageUrl = matcherUrl.group(1);
+ return ImmutablePair.of(StatusCode.NO_ERROR, uploadedImageUrl);
+ }
+ Log.e("GCParser.uploadLogIMage: Failed to upload image because of unknown error");
+
+ return ImmutablePair.of(StatusCode.LOGIMAGE_POST_ERROR, null);
+ }
+
+ /**
+ * Post a log to GC.com.
+ *
+ * @return status code of the upload and ID of the log
+ */
+ public static StatusCode postLogTrackable(final String tbid, final String trackingCode, final String[] viewstates,
+ final LogTypeTrackable logType, final int year, final int month, final int day, final String log) {
+ if (GCLogin.isEmpty(viewstates)) {
+ Log.e("GCParser.postLogTrackable: No viewstate given");
+ return StatusCode.LOG_POST_ERROR;
+ }
+
+ if (StringUtils.isBlank(log)) {
+ Log.e("GCParser.postLogTrackable: No log text given");
+ return StatusCode.NO_LOG_TEXT;
+ }
+
+ Log.i("Trying to post log for trackable #" + trackingCode + " - action: " + logType + "; date: " + year + "." + month + "." + day + ", log: " + log);
+
+ final String logInfo = log.replace("\n", "\r\n"); // windows' eol
+
+ final Calendar currentDate = Calendar.getInstance();
+ final Parameters params = new Parameters(
+ "__EVENTTARGET", "",
+ "__EVENTARGUMENT", "",
+ "__LASTFOCUS", "",
+ "ctl00$ContentBody$LogBookPanel1$ddLogType", Integer.toString(logType.id),
+ "ctl00$ContentBody$LogBookPanel1$tbCode", trackingCode);
+ GCLogin.putViewstates(params, viewstates);
+ if (currentDate.get(Calendar.YEAR) == year && (currentDate.get(Calendar.MONTH) + 1) == month && currentDate.get(Calendar.DATE) == day) {
+ params.put("ctl00$ContentBody$LogBookPanel1$DateTimeLogged", "");
+ 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.formatGcCustomDate(year, month, day));
+ }
+ params.put(
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Day", Integer.toString(day),
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Month", Integer.toString(month),
+ "ctl00$ContentBody$LogBookPanel1$DateTimeLogged$Year", Integer.toString(year),
+ "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Day", Integer.toString(day),
+ "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Month", Integer.toString(month),
+ "ctl00$ContentBody$LogBookPanel1$uxDateVisited$Year", Integer.toString(year),
+ "ctl00$ContentBody$LogBookPanel1$uxLogInfo", logInfo,
+ "ctl00$ContentBody$LogBookPanel1$btnSubmitLog", "Submit Log Entry",
+ "ctl00$ContentBody$uxVistOtherTrackableTB", "",
+ "ctl00$ContentBody$LogBookPanel1$LogButton", "Submit Log Entry",
+ "ctl00$ContentBody$uxVistOtherListingGC", "");
+
+ final String uri = new Uri.Builder().scheme("http").authority("www.geocaching.com").path("/track/log.aspx").encodedQuery("wid=" + tbid).build().toString();
+ final String page = GCLogin.getInstance().postRequestLogged(uri, params);
+ if (!GCLogin.getInstance().getLoginStatus(page)) {
+ Log.e("GCParser.postLogTrackable: Cannot log in geocaching");
+ return StatusCode.NOT_LOGGED_IN;
+ }
+
+ try {
+
+ final MatcherWrapper matcherOk = new MatcherWrapper(GCConstants.PATTERN_OK2, page);
+ if (matcherOk.find()) {
+ Log.i("Log successfully posted to trackable #" + trackingCode);
+ return StatusCode.NO_ERROR;
+ }
+ } catch (final Exception e) {
+ Log.e("GCParser.postLogTrackable.check", e);
+ }
+
+ Log.e("GCParser.postLogTrackable: Failed to post log because of unknown error");
+ return StatusCode.LOG_POST_ERROR;
+ }
+
+ /**
+ * Adds the cache to the watchlist of the user.
+ *
+ * @param cache
+ * the cache to add
+ * @return <code>false</code> if an error occurred, <code>true</code> otherwise
+ */
+ static boolean addToWatchlist(final Geocache cache) {
+ final String uri = "http://www.geocaching.com/my/watchlist.aspx?w=" + cache.getCacheId();
+ final String page = GCLogin.getInstance().postRequestLogged(uri, null);
+
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.addToWatchlist: No data from server");
+ return false; // error
+ }
+
+ final boolean guidOnPage = isGuidContainedInPage(cache, page);
+ if (guidOnPage) {
+ Log.i("GCParser.addToWatchlist: cache is on watchlist");
+ cache.setOnWatchlist(true);
+ } else {
+ Log.e("GCParser.addToWatchlist: cache is not on watchlist");
+ }
+ return guidOnPage; // on watchlist (=added) / else: error
+ }
+
+ /**
+ * Removes the cache from the watch list
+ *
+ * @param cache
+ * the cache to remove
+ * @return <code>false</code> if an error occurred, <code>true</code> otherwise
+ */
+ static boolean removeFromWatchlist(final Geocache cache) {
+ final String uri = "http://www.geocaching.com/my/watchlist.aspx?ds=1&action=rem&id=" + cache.getCacheId();
+ String page = GCLogin.getInstance().postRequestLogged(uri, null);
+
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.removeFromWatchlist: No data from server");
+ return false; // error
+ }
+
+ // removing cache from list needs approval by hitting "Yes" button
+ final Parameters params = new Parameters(
+ "__EVENTTARGET", "",
+ "__EVENTARGUMENT", "",
+ "ctl00$ContentBody$btnYes", "Yes");
+ GCLogin.transferViewstates(page, params);
+
+ page = Network.getResponseData(Network.postRequest(uri, params));
+ final boolean guidOnPage = isGuidContainedInPage(cache, page);
+ if (!guidOnPage) {
+ Log.i("GCParser.removeFromWatchlist: cache removed from watchlist");
+ cache.setOnWatchlist(false);
+ } else {
+ Log.e("GCParser.removeFromWatchlist: cache not removed from watchlist");
+ }
+ return !guidOnPage; // on watch list (=error) / not on watch list
+ }
+
+ /**
+ * Checks if a page contains the guid of a cache
+ *
+ * @param cache the geocache
+ * @param page
+ * the page to search in, may be null
+ * @return true if the page contains the guid of the cache, false otherwise
+ */
+ private static boolean isGuidContainedInPage(final Geocache cache, final String page) {
+ if (StringUtils.isBlank(page) || StringUtils.isBlank(cache.getGuid())) {
+ return false;
+ }
+ return Pattern.compile(cache.getGuid(), Pattern.CASE_INSENSITIVE).matcher(page).find();
+ }
+
+ @Nullable
+ static String requestHtmlPage(@Nullable final String geocode, @Nullable final String guid, final String log) {
+ final Parameters params = new Parameters("decrypt", "y");
+ if (StringUtils.isNotBlank(geocode)) {
+ params.put("wp", geocode);
+ } else if (StringUtils.isNotBlank(guid)) {
+ params.put("guid", guid);
+ }
+ params.put("log", log);
+ params.put("numlogs", "0");
+
+ return GCLogin.getInstance().getRequestLogged("http://www.geocaching.com/seek/cache_details.aspx", params);
+ }
+
+ /**
+ * Adds the cache to the favorites of the user.
+ *
+ * This must not be called from the UI thread.
+ *
+ * @param cache
+ * the cache to add
+ * @return <code>false</code> if an error occurred, <code>true</code> otherwise
+ */
+ static boolean addToFavorites(final Geocache cache) {
+ return changeFavorite(cache, true);
+ }
+
+ private static boolean changeFavorite(final Geocache cache, final boolean add) {
+ final String userToken = getUserToken(cache);
+ if (StringUtils.isEmpty(userToken)) {
+ return false;
+ }
+
+ final String uri = "http://www.geocaching.com/datastore/favorites.svc/update?u=" + userToken + "&f=" + Boolean.toString(add);
+
+ final HttpResponse response = Network.postRequest(uri, null);
+
+ if (response != null && response.getStatusLine().getStatusCode() == 200) {
+ Log.i("GCParser.changeFavorite: cache added/removed to/from favorites");
+ cache.setFavorite(add);
+ cache.setFavoritePoints(cache.getFavoritePoints() + (add ? 1 : -1));
+ return true;
+ }
+ Log.e("GCParser.changeFavorite: cache not added/removed to/from favorites");
+ return false;
+ }
+
+ private static String getUserToken(final Geocache cache) {
+ return parseUserToken(requestHtmlPage(cache.getGeocode(), null, "n"));
+ }
+
+ private static String parseUserToken(final String page) {
+ return TextUtils.getMatch(page, GCConstants.PATTERN_USERTOKEN, "");
+ }
+
+ /**
+ * Removes the cache from the favorites.
+ *
+ * This must not be called from the UI thread.
+ *
+ * @param cache
+ * the cache to remove
+ * @return <code>false</code> if an error occurred, <code>true</code> otherwise
+ */
+ static boolean removeFromFavorites(final Geocache cache) {
+ return changeFavorite(cache, false);
+ }
+
+ /**
+ * Parse a trackable HTML description into a Trackable object
+ *
+ * @param page
+ * the HTML page to parse, already processed through {@link TextUtils#replaceWhitespace}
+ * @return the parsed trackable, or null if none could be parsed
+ */
+ static Trackable parseTrackable(final String page, final String possibleTrackingcode) {
+ if (StringUtils.isBlank(page)) {
+ Log.e("GCParser.parseTrackable: No page given");
+ return null;
+ }
+
+ if (page.contains(GCConstants.ERROR_TB_DOES_NOT_EXIST) || page.contains(GCConstants.ERROR_TB_ARITHMETIC_OVERFLOW) || page.contains(GCConstants.ERROR_TB_ELEMENT_EXCEPTION)) {
+ return null;
+ }
+
+ final Trackable trackable = new Trackable();
+ trackable.forceSetBrand(TrackableBrand.TRAVELBUG);
+
+ // trackable geocode
+ trackable.setGeocode(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GEOCODE, true, StringUtils.upperCase(possibleTrackingcode)));
+ if (trackable.getGeocode() == null) {
+ Log.e("GCParser.parseTrackable: could not figure out trackable geocode");
+ return null;
+ }
+
+ // trackable id
+ trackable.setGuid(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GUID, true, trackable.getGuid()));
+
+ // trackable icon
+ trackable.setIconUrl(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ICON, true, trackable.getIconUrl()));
+
+ // trackable name
+ trackable.setName(Html.fromHtml(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_NAME, true, "")).toString());
+
+ // trackable type
+ if (StringUtils.isNotBlank(trackable.getName())) {
+ trackable.setType(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_TYPE, true, trackable.getType()));
+ }
+
+ // trackable owner name
+ try {
+ final MatcherWrapper matcherOwner = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_OWNER, page);
+ if (matcherOwner.find() && matcherOwner.groupCount() > 0) {
+ trackable.setOwnerGuid(matcherOwner.group(1));
+ trackable.setOwner(matcherOwner.group(2).trim());
+ }
+ } catch (final RuntimeException e) {
+ // failed to parse trackable owner name
+ Log.w("GCParser.parseTrackable: Failed to parse trackable owner name", e);
+ }
+
+ // trackable origin
+ trackable.setOrigin(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_ORIGIN, true, trackable.getOrigin()));
+
+ // trackable spotted
+ try {
+ final MatcherWrapper matcherSpottedCache = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDCACHE, page);
+ if (matcherSpottedCache.find() && matcherSpottedCache.groupCount() > 0) {
+ trackable.setSpottedGuid(matcherSpottedCache.group(1));
+ trackable.setSpottedName(matcherSpottedCache.group(2).trim());
+ trackable.setSpottedType(Trackable.SPOTTED_CACHE);
+ }
+
+ final MatcherWrapper matcherSpottedUser = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_SPOTTEDUSER, page);
+ if (matcherSpottedUser.find() && matcherSpottedUser.groupCount() > 0) {
+ trackable.setSpottedGuid(matcherSpottedUser.group(1));
+ trackable.setSpottedName(matcherSpottedUser.group(2).trim());
+ trackable.setSpottedType(Trackable.SPOTTED_USER);
+ }
+
+ if (TextUtils.matches(page, GCConstants.PATTERN_TRACKABLE_SPOTTEDUNKNOWN)) {
+ trackable.setSpottedType(Trackable.SPOTTED_UNKNOWN);
+ }
+
+ if (TextUtils.matches(page, GCConstants.PATTERN_TRACKABLE_SPOTTEDOWNER)) {
+ trackable.setSpottedType(Trackable.SPOTTED_OWNER);
+ }
+ } catch (final RuntimeException e) {
+ // 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(DATE_TB_IN_1.parse(releaseString));
+ } catch (final ParseException ignored) {
+ if (trackable.getReleased() == null) {
+ try {
+ trackable.setReleased(DATE_TB_IN_2.parse(releaseString));
+ } catch (final ParseException e) {
+ Log.e("Could not parse trackable release " + releaseString, e);
+ }
+ }
+ }
+ }
+
+ // trackable distance
+ final String distance = TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_DISTANCE, false, null);
+ if (null != distance) {
+ try {
+ trackable.setDistance(DistanceParser.parseDistance(distance,
+ !Settings.useImperialUnits()));
+ } catch (final NumberFormatException e) {
+ Log.e("GCParser.parseTrackable: Failed to parse distance", e);
+ }
+ }
+
+ // trackable goal
+ trackable.setGoal(HtmlUtils.removeExtraParagraph(convertLinks(TextUtils.getMatch(page, GCConstants.PATTERN_TRACKABLE_GOAL, true, trackable.getGoal()))));
+
+ // trackable details & image
+ try {
+ final MatcherWrapper matcherDetailsImage = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_DETAILSIMAGE, page);
+ if (matcherDetailsImage.find() && matcherDetailsImage.groupCount() > 0) {
+ final String image = StringUtils.trim(matcherDetailsImage.group(3));
+ final String details = StringUtils.trim(matcherDetailsImage.group(4));
+
+ if (StringUtils.isNotEmpty(image)) {
+ trackable.setImage(StringUtils.replace(image, "/display/", "/large/"));
+ }
+ if (StringUtils.isNotEmpty(details) && !StringUtils.equals(details, "No additional details available.")) {
+ 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", e);
+ }
+ if (StringUtils.isEmpty(trackable.getDetails()) && page.contains(GCConstants.ERROR_TB_NOT_ACTIVATED)) {
+ trackable.setDetails(CgeoApplication.getInstance().getString(R.string.trackable_not_activated));
+ }
+
+ // trackable logs
+ try {
+ final MatcherWrapper matcherLogs = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG, page);
+ /*
+ * 1. Type (image)
+ * 2. Date
+ * 3. Author
+ * 4. Cache-GUID
+ * 5. <ignored> (strike-through property for ancient caches)
+ * 6. Cache-name
+ * 7. Log text
+ */
+ while (matcherLogs.find()) {
+ long date = 0;
+ try {
+ date = GCLogin.parseGcCustomDate(matcherLogs.group(2)).getTime();
+ } catch (final ParseException ignored) {
+ }
+
+ final LogEntry logDone = new LogEntry(
+ Html.fromHtml(matcherLogs.group(3)).toString().trim(),
+ date,
+ LogType.getByIconName(matcherLogs.group(1)),
+ matcherLogs.group(7).trim());
+
+ if (matcherLogs.group(4) != null && matcherLogs.group(6) != null) {
+ logDone.cacheGuid = matcherLogs.group(4);
+ logDone.cacheName = matcherLogs.group(6);
+ }
+
+ // Apply the pattern for images in a trackable log entry against each full log (group(0))
+ final String logEntry = matcherLogs.group(0);
+ final MatcherWrapper matcherLogImages = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE_LOG_IMAGES, logEntry);
+ /*
+ * 1. Image URL
+ * 2. Image title
+ */
+ while (matcherLogImages.find()) {
+ final Image logImage = new Image.Builder()
+ .setUrl(matcherLogImages.group(1))
+ .setTitle(matcherLogImages.group(2))
+ .build();
+ logDone.addLogImage(logImage);
+ }
+
+ trackable.getLogs().add(logDone);
+ }
+ } catch (final Exception e) {
+ // failed to parse logs
+ Log.w("GCParser.parseCache: Failed to parse cache logs", e);
+ }
+
+ // tracking code
+ if (!StringUtils.equalsIgnoreCase(trackable.getGeocode(), possibleTrackingcode)) {
+ trackable.setTrackingcode(possibleTrackingcode);
+ }
+
+ if (CgeoApplication.getInstance() != null) {
+ DataStore.saveTrackable(trackable);
+ }
+
+ return trackable;
+ }
+
+ private static String convertLinks(final String input) {
+ if (input == null) {
+ return null;
+ }
+ return StringUtils.replace(input, "../", GCConstants.GC_URL);
+ }
+
+ private enum Logs {
+ ALL(null),
+ FRIENDS("sf"),
+ OWN("sp");
+
+ final String paramName;
+
+ Logs(final String paramName) {
+ this.paramName = paramName;
+ }
+
+ private String getParamName() {
+ return this.paramName;
+ }
+ }
+
+ /**
+ * Extract special logs (friends, own) through separate 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 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 Parameters params = new Parameters(
+ "tkn", userToken,
+ "idx", "1",
+ "num", String.valueOf(GCConstants.NUMBER_OF_LOGS),
+ "decrypt", "true");
+ if (logType != Logs.ALL) {
+ params.add(logType.getParamName(), Boolean.toString(Boolean.TRUE));
+ }
+ final HttpResponse response = Network.getRequest("http://www.geocaching.com/seek/geocache.logbook", params);
+ if (response == null) {
+ Log.e("GCParser.loadLogsFromDetails: cannot log logs, response is null");
+ return Observable.empty();
+ }
+ final int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode != 200) {
+ Log.e("GCParser.loadLogsFromDetails: error " + statusCode + " when requesting log information");
+ return Observable.empty();
+ }
+ 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, responseStream);
+ }
+ }).subscribeOn(RxUtils.networkScheduler);
+ }
+
+ 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) {
+ try {
+ 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 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.path("LogTypeImage").asText(".gif");
+ final String logIconName = logIconNameExt.substring(0, logIconNameExt.length() - 4);
+
+ final long date;
+ try {
+ 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.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.path("UserName").asText()),
+ date,
+ LogType.getByIconName(logIconName),
+ logText);
+ logDone.found = entry.path("GeocacheFindCount").asInt();
+ logDone.friend = markAsFriendsLog;
+
+ 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.Builder().setUrl(url).setTitle(title).build();
+ logDone.addLogImage(logImage);
+ }
+
+ subscriber.onNext(logDone);
+ }
+ } catch (final IOException e) {
+ Log.w("GCParser.loadLogsFromDetails: Failed to parse cache logs", e);
+ }
+ subscriber.onCompleted();
+ }
+ });
+ }
+
+ @NonNull
+ static List<LogType> parseTypes(final String page) {
+ if (StringUtils.isEmpty(page)) {
+ return Collections.emptyList();
+ }
+
+ final List<LogType> types = new ArrayList<>();
+
+ final MatcherWrapper typeBoxMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPEBOX, page);
+ if (typeBoxMatcher.find() && typeBoxMatcher.groupCount() > 0) {
+ final String typesText = typeBoxMatcher.group(1);
+ final MatcherWrapper typeMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPE2, typesText);
+ while (typeMatcher.find()) {
+ if (typeMatcher.groupCount() > 1) {
+ try {
+ final int type = Integer.parseInt(typeMatcher.group(2));
+ if (type > 0) {
+ types.add(LogType.getById(type));
+ }
+ } catch (final NumberFormatException e) {
+ Log.e("Error parsing log types", e);
+ }
+ }
+ }
+ }
+
+ // we don't support this log type
+ types.remove(LogType.UPDATE_COORDINATES);
+
+ return types;
+ }
+
+ @NonNull
+ public static List<LogTypeTrackable> parseLogTypesTrackables(final String page) {
+ if (StringUtils.isEmpty(page)) {
+ return new ArrayList<>();
+ }
+
+ final List<LogTypeTrackable> types = new ArrayList<>();
+
+ final MatcherWrapper typeBoxMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPEBOX, page);
+ if (typeBoxMatcher.find() && typeBoxMatcher.groupCount() > 0) {
+ final String typesText = typeBoxMatcher.group(1);
+ final MatcherWrapper typeMatcher = new MatcherWrapper(GCConstants.PATTERN_TYPE2, typesText);
+ while (typeMatcher.find()) {
+ if (typeMatcher.groupCount() > 1) {
+ try {
+ final int type = Integer.parseInt(typeMatcher.group(2));
+ if (type > 0) {
+ types.add(LogTypeTrackable.getById(type));
+ }
+ } catch (final NumberFormatException e) {
+ Log.e("Error parsing trackable log types", e);
+ }
+ }
+ }
+ }
+ return types;
+ }
+
+ static List<TrackableLog> parseTrackableLog(final String page) {
+ if (StringUtils.isEmpty(page)) {
+ return Collections.emptyList();
+ }
+
+ String table = StringUtils.substringBetween(page, "<table id=\"tblTravelBugs\"", "</table>");
+
+ // if no trackables are currently in the account, the table is not available, so return an empty list instead of null
+ if (StringUtils.isBlank(table)) {
+ return Collections.emptyList();
+ }
+
+ table = StringUtils.substringBetween(table, "<tbody>", "</tbody>");
+ if (StringUtils.isBlank(table)) {
+ Log.e("GCParser.parseTrackableLog: tbody not found on page");
+ return Collections.emptyList();
+ }
+
+ final List<TrackableLog> trackableLogs = new ArrayList<>();
+
+ final MatcherWrapper trackableMatcher = new MatcherWrapper(GCConstants.PATTERN_TRACKABLE, page);
+ while (trackableMatcher.find()) {
+ if (trackableMatcher.groupCount() > 0) {
+
+ final String trackCode = trackableMatcher.group(1);
+ final String name = Html.fromHtml(trackableMatcher.group(2)).toString();
+ try {
+ final Integer ctl = Integer.valueOf(trackableMatcher.group(3));
+ final Integer id = Integer.valueOf(trackableMatcher.group(5));
+ if (trackCode != null && ctl != null && id != null) {
+ final TrackableLog entry = new TrackableLog("", trackCode, name, id, ctl, TrackableBrand.TRAVELBUG);
+
+ Log.i("Trackable in inventory (#" + entry.ctl + "/" + entry.id + "): " + entry.trackCode + " - " + entry.name);
+ trackableLogs.add(entry);
+ }
+ } catch (final NumberFormatException e) {
+ Log.e("GCParser.parseTrackableLog", e);
+ }
+ }
+ }
+
+ return trackableLogs;
+ }
+
+ /**
+ * Insert the right cache type restriction in parameters
+ *
+ * @param params
+ * the parameters to insert the restriction into
+ * @param cacheType
+ * the type of cache, or null to include everything
+ */
+ static private void insertCacheType(final Parameters params, final CacheType cacheType) {
+ params.put("tx", cacheType.guid);
+ }
+
+ private static void getExtraOnlineInfo(final Geocache cache, final String page, final CancellableHandler handler) {
+ // This method starts the page parsing for logs in the background, as well as retrieve the friends and own logs
+ // if requested. It merges them and stores them in the background, while the rating is retrieved if needed and
+ // stored. Then we wait for the log merging and saving to be completed before returning.
+ if (CancellableHandler.isCancelled(handler)) {
+ return;
+ }
+
+ 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
+ public List<LogEntry> call(final List<LogEntry> logEntries, final List<LogEntry> specialLogEntries) {
+ mergeFriendsLogs(logEntries, specialLogEntries);
+ return logEntries;
+ }
+ }).cache();
+ mergedLogs.subscribe(new Action1<List<LogEntry>>() {
+ @Override
+ public void call(final List<LogEntry> 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);
+ final GCVoteRating rating = GCVote.getRating(cache.getGuid(), cache.getGeocode());
+ if (rating != null) {
+ cache.setRating(rating.getRating());
+ cache.setVotes(rating.getVotes());
+ cache.setMyVote(rating.getMyVote());
+ }
+ }
+
+ // Wait for completion of logs parsing, retrieving and merging
+ RxUtils.waitForCompletion(mergedLogs);
+ }
+
+ /**
+ * Merge log entries and mark them as friends logs (personal and friends) to identify
+ * them on friends/personal logs tab.
+ *
+ * @param mergedLogs
+ * the list to merge logs with
+ * @param logsToMerge
+ * the list of logs to merge
+ */
+ private static void mergeFriendsLogs(final List<LogEntry> mergedLogs, final Iterable<LogEntry> logsToMerge) {
+ for (final LogEntry log : logsToMerge) {
+ if (mergedLogs.contains(log)) {
+ mergedLogs.get(mergedLogs.indexOf(log)).friend = true;
+ } else {
+ mergedLogs.add(log);
+ }
+ }
+ }
+
+ static boolean uploadModifiedCoordinates(final Geocache cache, final Geopoint wpt) {
+ return editModifiedCoordinates(cache, wpt);
+ }
+
+ static boolean deleteModifiedCoordinates(final Geocache cache) {
+ return editModifiedCoordinates(cache, null);
+ }
+
+ static boolean editModifiedCoordinates(final Geocache cache, final Geopoint wpt) {
+ final String userToken = getUserToken(cache);
+ if (StringUtils.isEmpty(userToken)) {
+ return false;
+ }
+
+ 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 uriSuffix = wpt != null ? "SetUserCoordinate" : "ResetUserCoordinate";
+
+ final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/";
+ final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo);
+
+ 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;
+ }
+
+ static boolean uploadPersonalNote(final Geocache cache) {
+ final String userToken = getUserToken(cache);
+ if (StringUtils.isEmpty(userToken)) {
+ return false;
+ }
+
+ final ObjectNode jo = new ObjectNode(JsonUtils.factory);
+ jo.putObject("dto").put("et", StringUtils.defaultString(cache.getPersonalNote())).put("ut", userToken);
+
+ final String uriSuffix = "SetUserCacheNote";
+
+ final String uriPrefix = "http://www.geocaching.com/seek/cache_details.aspx/";
+ final HttpResponse response = Network.postJsonRequest(uriPrefix + uriSuffix, jo);
+
+ 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;
+ }
+
+ 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/trackable/GeokretyLoggingManager.java b/main/src/cgeo/geocaching/connector/trackable/GeokretyLoggingManager.java
index e2c10de..63ac8b5 100644
--- a/main/src/cgeo/geocaching/connector/trackable/GeokretyLoggingManager.java
+++ b/main/src/cgeo/geocaching/connector/trackable/GeokretyLoggingManager.java
@@ -1,85 +1,88 @@
-package cgeo.geocaching.connector.trackable;
-
-import cgeo.geocaching.AbstractLoggingActivity;
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.TrackableLog;
-import cgeo.geocaching.connector.LogResult;
-import cgeo.geocaching.enumerations.LogTypeTrackable;
-import cgeo.geocaching.enumerations.StatusCode;
-import cgeo.geocaching.utils.Log;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.ImmutablePair;
-import org.eclipse.jdt.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-public class GeokretyLoggingManager extends AbstractTrackableLoggingManager {
-
- public GeokretyLoggingManager(final AbstractLoggingActivity activity) {
- super(activity);
- }
-
- @Override
- public List<LogTypeTrackable> loadInBackground() {
- return getPossibleLogTypesTrackable();
- }
-
- @Override
- public LogResult postLog(final Geocache cache, final TrackableLog trackableLog, final Calendar date, final String log) {
- try {
- final ImmutablePair<StatusCode, List<String>> response = GeokretyConnector.postLogTrackable(
- getContext(),
- cache,
- trackableLog,
- date,
- log);
-
- final String logs = (response.getRight().isEmpty() ? "" : StringUtils.join(response.getRight(), "\n"));
- return new LogResult(response.getLeft(), logs);
- } catch (final Exception e) {
- Log.e("GeokretyLoggingManager.postLog", e);
- }
-
- return new LogResult(StatusCode.LOG_POST_ERROR, "");
- }
-
- @Override
- @NonNull
- public List<LogTypeTrackable> getPossibleLogTypesTrackable() {
- final List<LogTypeTrackable> list = new ArrayList<>();
- list.add(LogTypeTrackable.RETRIEVED_IT);
- list.add(LogTypeTrackable.DISCOVERED_IT);
- list.add(LogTypeTrackable.DROPPED_OFF);
- list.add(LogTypeTrackable.VISITED);
- list.add(LogTypeTrackable.NOTE);
- return list;
- }
-
- @Override
- public boolean canLogTime() {
- return true;
- }
-
- @Override
- public boolean canLogCoordinates() {
- return true;
- }
-
- @Override
- public void setGuid(final String guid) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isTrackingCodeNeededToPostNote() {
- return true;
- }
-
- @Override
- public boolean postReady() {
- return true;
- }
+package cgeo.geocaching.connector.trackable;
+
+import cgeo.geocaching.AbstractLoggingActivity;
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.TrackableLog;
+import cgeo.geocaching.connector.ImageResult;
+import cgeo.geocaching.connector.LogResult;
+import cgeo.geocaching.enumerations.LogTypeTrackable;
+import cgeo.geocaching.enumerations.StatusCode;
+import cgeo.geocaching.utils.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+public class GeokretyLoggingManager extends AbstractTrackableLoggingManager {
+
+ public GeokretyLoggingManager(final AbstractLoggingActivity activity) {
+ super(activity);
+ }
+
+ @Override
+ public List<LogTypeTrackable> loadInBackground() {
+ return getPossibleLogTypesTrackable();
+ }
+
+ @Override
+ public LogResult postLog(final Geocache cache, final TrackableLog trackableLog, final Calendar date, final String log) {
+ try {
+ final ImmutablePair<StatusCode, List<String>> response = GeokretyConnector.postLogTrackable(
+ getContext(),
+ cache,
+ trackableLog,
+ date,
+ log);
+
+ final String logs = (response.getRight().isEmpty() ? "" : StringUtils.join(response.getRight(), "\n"));
+ return new LogResult(response.getLeft(), logs);
+ } catch (final Exception e) {
+ Log.e("GeokretyLoggingManager.postLog", e);
+ }
+
+ return new LogResult(StatusCode.LOG_POST_ERROR, "");
+ }
+
+ @Override
+ @NonNull
+ public List<LogTypeTrackable> getPossibleLogTypesTrackable() {
+ final List<LogTypeTrackable> list = new ArrayList<>();
+ list.add(LogTypeTrackable.RETRIEVED_IT);
+ list.add(LogTypeTrackable.DISCOVERED_IT);
+ list.add(LogTypeTrackable.DROPPED_OFF);
+ list.add(LogTypeTrackable.VISITED);
+ list.add(LogTypeTrackable.NOTE);
+ return list;
+ }
+
+ @Override
+ public boolean canLogTime() {
+ return true;
+ }
+
+ @Override
+ public boolean canLogCoordinates() {
+ return true;
+ }
+
+ @Override
+ public void setGuid(final String guid) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isTrackingCodeNeededToPostNote() {
+ return true;
+ }
+
+ @Override
+ public boolean postReady() {
+ return true;
+ }
} \ No newline at end of file
diff --git a/main/src/cgeo/geocaching/location/DistanceParser.java b/main/src/cgeo/geocaching/location/DistanceParser.java
index 80e82f2..7cbc19f 100644
--- a/main/src/cgeo/geocaching/location/DistanceParser.java
+++ b/main/src/cgeo/geocaching/location/DistanceParser.java
@@ -1,51 +1,51 @@
-package cgeo.geocaching.location;
-
-import cgeo.geocaching.utils.MatcherWrapper;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-public final class DistanceParser {
-
- private static final Pattern pattern = Pattern.compile("^([0-9.,]+)[ ]*(m|km|ft|yd|mi|)?$", Pattern.CASE_INSENSITIVE);
-
- /**
- * Parse a distance string composed by a number and an optional suffix
- * (such as "1.2km").
- *
- * @param distanceText
- * the string to analyze
- * @return the distance in kilometers
- *
- * @throws NumberFormatException
- * if the given number is invalid
- */
- public static float parseDistance(final String distanceText, final boolean metricUnit)
- throws NumberFormatException {
- final MatcherWrapper matcher = new MatcherWrapper(pattern, distanceText);
-
- if (!matcher.find()) {
- throw new NumberFormatException(distanceText);
- }
-
- final float value = Float.parseFloat(matcher.group(1).replace(',', '.'));
- final String unit = StringUtils.lowerCase(matcher.group(2), Locale.US);
-
- if ("m".equals(unit) || (StringUtils.isEmpty(unit) && metricUnit)) {
- return value / 1000;
- }
- if ("km".equals(unit)) {
- return value;
- }
- if ("yd".equals(unit)) {
- return value * IConversion.YARDS_TO_KILOMETER;
- }
- if ("mi".equals(unit)) {
- return value * IConversion.MILES_TO_KILOMETER;
- }
- return value * IConversion.FEET_TO_KILOMETER;
- }
-
-}
+package cgeo.geocaching.location;
+
+import cgeo.geocaching.utils.MatcherWrapper;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+public final class DistanceParser {
+
+ private static final Pattern pattern = Pattern.compile("^([0-9.,]+)[ ]*(m|km|ft|yd|mi|)?$", Pattern.CASE_INSENSITIVE);
+
+ /**
+ * Parse a distance string composed by a number and an optional suffix
+ * (such as "1.2km").
+ *
+ * @param distanceText
+ * the string to analyze
+ * @return the distance in kilometers
+ *
+ * @throws NumberFormatException
+ * if the given number is invalid
+ */
+ public static float parseDistance(final String distanceText, final boolean metricUnit)
+ throws NumberFormatException {
+ final MatcherWrapper matcher = new MatcherWrapper(pattern, distanceText);
+
+ if (!matcher.find()) {
+ throw new NumberFormatException(distanceText);
+ }
+
+ final float value = Float.parseFloat(matcher.group(1).replace(',', '.'));
+ final String unit = matcher.group(2).toLowerCase(Locale.US);
+
+ if (unit.equals("m") || (StringUtils.isEmpty(unit) && metricUnit)) {
+ return value / 1000;
+ }
+ if (unit.equals("km")) {
+ return value;
+ }
+ if (unit.equals("yd")) {
+ return value * IConversion.YARDS_TO_KILOMETER;
+ }
+ if (unit.equals("mi")) {
+ return value * IConversion.MILES_TO_KILOMETER;
+ }
+ return value * IConversion.FEET_TO_KILOMETER;
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/settings/Settings.java b/main/src/cgeo/geocaching/settings/Settings.java
index 71f4ebb..6fad160 100644
--- a/main/src/cgeo/geocaching/settings/Settings.java
+++ b/main/src/cgeo/geocaching/settings/Settings.java
@@ -1,1208 +1,1205 @@
-package cgeo.geocaching.settings;
-
-import cgeo.geocaching.CgeoApplication;
-import cgeo.geocaching.R;
-import cgeo.geocaching.apps.navi.NavigationAppFactory.NavigationAppsEnum;
-import cgeo.geocaching.connector.capability.ICredentials;
-import cgeo.geocaching.connector.gc.GCConnector;
-import cgeo.geocaching.connector.gc.GCConstants;
-import cgeo.geocaching.enumerations.CacheType;
-import cgeo.geocaching.enumerations.LogTypeTrackable;
-import cgeo.geocaching.list.StoredList;
-import cgeo.geocaching.location.Geopoint;
-import cgeo.geocaching.maps.CGeoMap.MapMode;
-import cgeo.geocaching.maps.LivemapStrategy;
-import cgeo.geocaching.maps.MapProviderFactory;
-import cgeo.geocaching.maps.google.v1.GoogleMapProvider;
-import cgeo.geocaching.maps.interfaces.GeoPointImpl;
-import cgeo.geocaching.maps.interfaces.MapProvider;
-import cgeo.geocaching.maps.interfaces.MapSource;
-import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider;
-import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider.OfflineMapSource;
-import cgeo.geocaching.sensors.OrientationProvider;
-import cgeo.geocaching.sensors.RotationProvider;
-import cgeo.geocaching.utils.CryptUtils;
-import cgeo.geocaching.utils.FileUtils;
-import cgeo.geocaching.utils.FileUtils.FileSelector;
-import cgeo.geocaching.utils.Log;
-
-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.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.os.Build;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Environment;
-import android.preference.PreferenceManager;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * General c:geo preferences/settings set by the user
- */
-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 = ',';
- private static final int SHOW_WP_THRESHOLD_DEFAULT = 10;
- public static final int SHOW_WP_THRESHOLD_MAX = 50;
- private static final int MAP_SOURCE_DEFAULT = GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
-
- public static final boolean HW_ACCEL_DISABLED_BY_DEFAULT =
- Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
- StringUtils.equals(Build.MODEL, "HTC One X") || // HTC One X
- StringUtils.equals(Build.MODEL, "HTC One S") || // HTC One S
- StringUtils.equals(Build.MODEL, "GT-I8190") || // Samsung S3 mini
- StringUtils.equals(Build.MODEL, "GT-S6310L") || // Samsung Galaxy Young
- StringUtils.equals(Build.MODEL, "GT-P5210") || // Samsung Galaxy Tab 3
- StringUtils.equals(Build.MODEL, "GT-S7580") || // Samsung Galaxy Trend Plus
- StringUtils.equals(Build.MODEL, "GT-I9105P") || // Samsung Galaxy SII Plus
- StringUtils.equals(Build.MODEL, "ST25i") || // Sony Xperia U
- StringUtils.equals(Build.MODEL, "bq Aquaris 5") || // bq Aquaris 5
- StringUtils.equals(Build.MODEL, "A1-810") || // Unknown A1-810
- StringUtils.equals(Build.MODEL, "GT-I9195") || // Samsung S4 mini
- StringUtils.equals(Build.MODEL, "GT-I8200N"); // Samsung S3 mini
-
- // twitter api keys
- 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,
- Deg,
- Min,
- Sec;
-
- static final int DEFAULT_INT_VALUE = Min.ordinal();
-
- public static CoordInputFormatEnum fromInt(final int id) {
- final CoordInputFormatEnum[] values = CoordInputFormatEnum.values();
- if (id < 0 || id >= values.length) {
- return Min;
- }
- return values[id];
- }
- }
-
- private static final SharedPreferences sharedPrefs = PreferenceManager
- .getDefaultSharedPreferences(CgeoApplication.getInstance().getBaseContext());
- static {
- migrateSettings();
- final boolean isDebug = sharedPrefs.getBoolean(getKey(R.string.pref_debug), false);
- Log.setDebug(isDebug);
- CgeoApplication.dumpOnOutOfMemory(isDebug);
- }
-
- /**
- * Cache the mapsource locally. If that is an offline map source, each request would potentially access the
- * underlying map file, leading to delays.
- */
- private static MapSource mapSource;
-
- protected Settings() {
- throw new InstantiationError();
- }
-
- private static void migrateSettings() {
- 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();
-
- 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));
-
- e.putInt(getKey(R.string.pref_settingsversion), 1); // mark migrated
- e.apply();
- }
-
- // changes for new settings dialog
- if (currentVersion < 2) {
- final Editor e = sharedPrefs.edit();
-
- e.putBoolean(getKey(R.string.pref_units_imperial), useImperialUnits());
-
- // show waypoints threshold now as a slider
- int wpThreshold = getWayPointsThreshold();
- if (wpThreshold < 0) {
- wpThreshold = 0;
- } else if (wpThreshold > SHOW_WP_THRESHOLD_MAX) {
- wpThreshold = SHOW_WP_THRESHOLD_MAX;
- }
- e.putInt(getKey(R.string.pref_showwaypointsthreshold), wpThreshold);
-
- // KEY_MAP_SOURCE must be string, because it is the key for a ListPreference now
- final int ms = sharedPrefs.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT);
- e.remove(getKey(R.string.pref_mapsource));
- e.putString(getKey(R.string.pref_mapsource), String.valueOf(ms));
-
- // navigation tool ids must be string, because ListPreference uses strings as keys
- final int dnt1 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id);
- final int dnt2 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id);
- e.remove(getKey(R.string.pref_defaultNavigationTool));
- e.remove(getKey(R.string.pref_defaultNavigationTool2));
- e.putString(getKey(R.string.pref_defaultNavigationTool), String.valueOf(dnt1));
- e.putString(getKey(R.string.pref_defaultNavigationTool2), String.valueOf(dnt2));
-
- // defaults for gpx directories
- e.putString(getKey(R.string.pref_gpxImportDir), getGpxImportDir());
- e.putString(getKey(R.string.pref_gpxExportDir), getGpxExportDir());
-
- e.putInt(getKey(R.string.pref_settingsversion), 2); // mark migrated
- e.apply();
- }
- }
-
- private static String getKey(final int prefKeyId) {
- return CgeoApplication.getInstance().getString(prefKeyId);
- }
-
- static String getString(final int prefKeyId, final String defaultValue) {
- return sharedPrefs.getString(getKey(prefKeyId), defaultValue);
- }
-
- private static int getInt(final int prefKeyId, final int defaultValue) {
- return sharedPrefs.getInt(getKey(prefKeyId), defaultValue);
- }
-
- private static long getLong(final int prefKeyId, final long defaultValue) {
- return sharedPrefs.getLong(getKey(prefKeyId), defaultValue);
- }
-
- private static boolean getBoolean(final int prefKeyId, final boolean defaultValue) {
- return sharedPrefs.getBoolean(getKey(prefKeyId), defaultValue);
- }
-
- private static float getFloat(final int prefKeyId, final float defaultValue) {
- return sharedPrefs.getFloat(getKey(prefKeyId), defaultValue);
- }
-
- protected static void putString(final int prefKeyId, final String value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putString(getKey(prefKeyId), value);
- edit.apply();
- }
-
- protected static void putBoolean(final int prefKeyId, final boolean value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putBoolean(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void putInt(final int prefKeyId, final int value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putInt(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void putLong(final int prefKeyId, final long value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putLong(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void putFloat(final int prefKeyId, final float value) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.putFloat(getKey(prefKeyId), value);
- edit.apply();
- }
-
- private static void remove(final int prefKeyId) {
- final SharedPreferences.Editor edit = sharedPrefs.edit();
- edit.remove(getKey(prefKeyId));
- edit.apply();
- }
-
- private static boolean contains(final int prefKeyId) {
- return sharedPrefs.contains(getKey(prefKeyId));
- }
-
- public static boolean hasGCCredentials() {
- final String preUsername = getString(R.string.pref_username, null);
- final String prePassword = getString(R.string.pref_password, null);
-
- return !StringUtils.isBlank(preUsername) && !StringUtils.isBlank(prePassword);
- }
-
- /**
- * Get login and password information of Geocaching.com.
- *
- * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
- */
- public static ImmutablePair<String, String> getGcCredentials() {
- return getCredentials(GCConnector.getInstance());
- }
-
- /**
- * Get login and password information.
- *
- * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
- */
- public static ImmutablePair<String, String> getCredentials(final @NonNull ICredentials connector) {
- final String username = getString(connector.getUsernamePreferenceKey(), null);
- final String password = getString(connector.getPasswordPreferenceKey(), null);
-
- if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
- return new ImmutablePair<>(StringUtils.EMPTY, StringUtils.EMPTY);
- }
-
- return new ImmutablePair<>(username, password);
- }
-
- public static String getUsername() {
- return getString(R.string.pref_username, StringUtils.EMPTY);
- }
-
- public static boolean isGCConnectorActive() {
- return getBoolean(R.string.pref_connectorGCActive, true);
- }
-
- public static boolean isECConnectorActive() {
- return getBoolean(R.string.pref_connectorECActive, false);
- }
-
- public static boolean isGCPremiumMember() {
- final String memberStatus = 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 void setGCMemberStatus(final String memberStatus) {
- if (StringUtils.isBlank(memberStatus)) {
- remove(R.string.pref_memberstatus);
- }
- putString(R.string.pref_memberstatus, memberStatus);
- }
-
- public static ImmutablePair<String, String> getTokenPair(final int tokenPublicPrefKey, final int tokenSecretPrefKey) {
- return new ImmutablePair<>(getString(tokenPublicPrefKey, null), getString(tokenSecretPrefKey, null));
- }
-
- public static void setTokens(final int tokenPublicPrefKey, @Nullable final String tokenPublic, final int tokenSecretPrefKey, @Nullable final String tokenSecret) {
- if (tokenPublic == null) {
- remove(tokenPublicPrefKey);
- } else {
- putString(tokenPublicPrefKey, tokenPublic);
- }
- if (tokenSecret == null) {
- remove(tokenSecretPrefKey);
- } else {
- putString(tokenSecretPrefKey, tokenSecret);
- }
- }
-
- public static boolean isOCConnectorActive(final int isActivePrefKeyId) {
- return getBoolean(isActivePrefKeyId, false);
- }
-
- public static boolean hasOCAuthorization(final int tokenPublicPrefKeyId, final int tokenSecretPrefKeyId) {
- return StringUtils.isNotBlank(getString(tokenPublicPrefKeyId, ""))
- && StringUtils.isNotBlank(getString(tokenSecretPrefKeyId, ""));
- }
-
- public static boolean isGCVoteLogin() {
- return getGCVoteLogin() != null;
- }
-
- public static ImmutablePair<String, String> getGCVoteLogin() {
- final String username = getString(R.string.pref_username, null);
- final String password = getString(R.string.pref_pass_vote, null);
-
- if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
- return null;
- }
-
- return new ImmutablePair<>(username, password);
- }
-
- @NonNull
- public static String getSignature() {
- return StringUtils.defaultString(getString(R.string.pref_signature, StringUtils.EMPTY));
- }
-
- public static void setCookieStore(final String cookies) {
- if (StringUtils.isBlank(cookies)) {
- // erase cookies
- remove(R.string.pref_cookiestore);
- }
- // save 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
- */
- public static void setCacheType(final CacheType cacheType) {
- if (cacheType == null) {
- remove(R.string.pref_cachetype);
- } else {
- putString(R.string.pref_cachetype, cacheType.id);
- }
- }
-
- public static int getLastList() {
- return getInt(R.string.pref_lastusedlist, StoredList.STANDARD_LIST_ID);
- }
-
- public static void saveLastList(final int listId) {
- putInt(R.string.pref_lastusedlist, listId);
- }
-
- public static void setWebNameCode(final String name, final String code) {
- putString(R.string.pref_webDeviceName, name);
- putString(R.string.pref_webDeviceCode, code);
- }
-
- public static MapProvider getMapProvider() {
- return getMapSource().getMapProvider();
- }
-
- public static String getMapFile() {
- return getString(R.string.pref_mapfile, null);
- }
-
- static void setMapFile(final String mapFile) {
- putString(R.string.pref_mapfile, mapFile);
- if (mapFile != null) {
- setMapFileDirectory(new File(mapFile).getParent());
- }
- }
-
- public static String getMapFileDirectory() {
- final String mapDir = getString(R.string.pref_mapDirectory, null);
- if (mapDir != null) {
- return mapDir;
- }
- final String mapFile = getMapFile();
- if (mapFile != null) {
- return new File(mapFile).getParent();
- }
- return null;
- }
-
- static void setMapFileDirectory(final String mapFileDirectory) {
- putString(R.string.pref_mapDirectory, mapFileDirectory);
- MapsforgeMapProvider.getInstance().updateOfflineMaps();
- }
-
- private static boolean isValidMapFile() {
- return isValidMapFile(getMapFile());
- }
-
- public static boolean isValidMapFile(final String mapFileIn) {
- return MapsforgeMapProvider.isValidMapFile(mapFileIn);
- }
-
- public static boolean isScaleMapsforgeText() {
- return getBoolean(R.string.pref_mapsforge_scale_text, true);
- }
-
- public static CoordInputFormatEnum getCoordInputFormat() {
- return CoordInputFormatEnum.fromInt(getInt(R.string.pref_coordinputformat, CoordInputFormatEnum.DEFAULT_INT_VALUE));
- }
-
- public static void setCoordInputFormat(final CoordInputFormatEnum format) {
- putInt(R.string.pref_coordinputformat, format.ordinal());
- }
-
- public static boolean getLogOffline() {
- return getBoolean(R.string.pref_log_offline, false);
- }
-
- public static boolean getChooseList() {
- return getBoolean(R.string.pref_choose_list, false);
- }
-
- public static boolean getLoadDirImg() {
- return !isGCPremiumMember() && getBoolean(R.string.pref_loaddirectionimg, true);
- }
-
- public static void setGcCustomDate(final String format) {
- putString(R.string.pref_gccustomdate, format);
- }
-
- /**
- * @return User selected date format on GC.com
- */
- public static String getGcCustomDate() {
- // 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() {
- return getBoolean(R.string.pref_excludemine, false);
- }
-
- public static boolean useEnglish() {
- return getBoolean(R.string.pref_useenglish, false);
- }
-
- public static boolean isShowAddress() {
- return getBoolean(R.string.pref_showaddress, true);
- }
-
- public static boolean isShowCaptcha() {
- return !isGCPremiumMember() && getBoolean(R.string.pref_showcaptcha, false);
- }
-
- public static boolean isExcludeDisabledCaches() {
- return getBoolean(R.string.pref_excludedisabled, false);
- }
-
- public static boolean isStoreOfflineMaps() {
- return getBoolean(R.string.pref_offlinemaps, true);
- }
-
- public static boolean isStoreOfflineWpMaps() {
- return getBoolean(R.string.pref_offlinewpmaps, false);
- }
-
- public static boolean isStoreLogImages() {
- return getBoolean(R.string.pref_logimages, false);
- }
-
- public static boolean isAutoLoadDescription() {
- return getBoolean(R.string.pref_autoloaddesc, true);
- }
-
- public static boolean isRatingWanted() {
- return getBoolean(R.string.pref_ratingwanted, true);
- }
-
- public static boolean isGeokretyConnectorActive() {
- return getBoolean(R.string.pref_connectorGeokretyActive, false);
- }
-
- public static boolean isGeokretyCacheActive() {
- return getBoolean(R.string.pref_geokrety_cache, true);
- }
-
- static boolean hasGeokretyAuthorization() {
- return StringUtils.isNotBlank(getGeokretySecId());
- }
-
- public static String getGeokretySecId() {
- return getString(R.string.pref_fakekey_geokrety_authorization, null);
- }
-
- public static void setGeokretySecId(final String secid) {
- putString(R.string.pref_fakekey_geokrety_authorization, secid);
- }
-
- public static boolean isRegisteredForGeokretyLogging() {
- return getGeokretySecId() != null;
- }
-
- /**
- * Retrieve showed popup counter for warning about logging Trackable recommend Geocode
- *
- * @return number of times the popup has appeared
- */
- public static int getLogTrackableWithoutGeocodeShowCount() {
- return getInt(R.string.pref_logtrackablewithoutgeocodeshowcount, 0);
- }
-
- /**
- * Store showed popup counter for warning about logging Trackable recommend Geocode
- *
- * @param showCount the count to save
- */
- public static void setLogTrackableWithoutGeocodeShowCount(final int showCount) {
- putInt(R.string.pref_logtrackablewithoutgeocodeshowcount, showCount);
- }
-
- public static boolean isFriendLogsWanted() {
- if (!hasGCCredentials()) {
- // don't show a friends log if the user is anonymous
- return false;
- }
- return getBoolean(R.string.pref_friendlogswanted, true);
- }
-
- public static boolean isLiveList() {
- return getBoolean(R.string.pref_livelist, true);
- }
-
- public static boolean isTrackableAutoVisit() {
- return getBoolean(R.string.pref_trackautovisit, false);
- }
-
- public static boolean isAutoInsertSignature() {
- return getBoolean(R.string.pref_sigautoinsert, false);
- }
-
- 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());
- }
-
- private static boolean useImperialUnitsByDefault() {
- final String countryCode = Locale.getDefault().getCountry();
- return "US".equals(countryCode) // USA
- || "LR".equals(countryCode) // Liberia
- || "MM".equals(countryCode); // Burma
- }
-
- public static boolean isLiveMap() {
- return getBoolean(R.string.pref_maplive, true);
- }
-
- public static void setLiveMap(final boolean live) {
- putBoolean(R.string.pref_maplive, live);
- }
-
- public static boolean isMapTrail() {
- return getBoolean(R.string.pref_maptrail, false);
- }
-
- public static void setMapTrail(final boolean showTrail) {
- putBoolean(R.string.pref_maptrail, showTrail);
- }
-
- /**
- * whether to show a direction line on the map
- */
- public static boolean isMapDirection() {
- return getBoolean(R.string.pref_map_direction, true);
- }
-
- public static void setMapDirection(final boolean showDirection) {
- putBoolean(R.string.pref_map_direction, showDirection);
- }
-
- /**
- * 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 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,
- getInt(R.string.pref_lastmaplon, 0) / 1e6));
- }
-
- public static void setMapCenter(final GeoPointImpl mapViewCenter) {
- putInt(R.string.pref_lastmaplat, mapViewCenter.getLatitudeE6());
- putInt(R.string.pref_lastmaplon, mapViewCenter.getLongitudeE6());
- }
-
- @NonNull
- public static synchronized MapSource getMapSource() {
- if (mapSource != null) {
- return mapSource;
- }
- final int id = getConvertedMapId();
- mapSource = MapProviderFactory.getMapSource(id);
- if (mapSource != null) {
- // don't use offline maps if the map file is not valid
- if (!(mapSource instanceof OfflineMapSource) || isValidMapFile()) {
- return mapSource;
- }
- }
- // fallback to first available map
- return MapProviderFactory.getDefaultSource();
- }
-
- private final static int GOOGLEMAP_BASEID = 30;
- private final static int MAP = 1;
- private final static int SATELLITE = 2;
-
- private final static int MFMAP_BASEID = 40;
- private final static int MAPNIK = 1;
- private final static int CYCLEMAP = 3;
- private final static int OFFLINE = 4;
- private static final int HISTORY_SIZE = 10;
-
- /**
- * 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,
- String.valueOf(MAP_SOURCE_DEFAULT)));
- switch (id) {
- case GOOGLEMAP_BASEID + MAP:
- return GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
- case GOOGLEMAP_BASEID + SATELLITE:
- return GoogleMapProvider.GOOGLE_SATELLITE_ID.hashCode();
- case MFMAP_BASEID + MAPNIK:
- return MapsforgeMapProvider.MAPSFORGE_MAPNIK_ID.hashCode();
- case MFMAP_BASEID + CYCLEMAP:
- return MapsforgeMapProvider.MAPSFORGE_CYCLEMAP_ID.hashCode();
- case MFMAP_BASEID + OFFLINE: {
- final String mapFile = getMapFile();
- if (StringUtils.isNotEmpty(mapFile)) {
- return mapFile.hashCode();
- }
- break;
- }
- default:
- break;
- }
- return id;
- }
-
- 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());
- }
- // cache the value
- mapSource = newMapSource;
- }
-
- public static void setAnyCoordinates(final Geopoint coords) {
- if (null != coords) {
- putFloat(R.string.pref_anylatitude, (float) coords.getLatitude());
- putFloat(R.string.pref_anylongitude, (float) coords.getLongitude());
- } else {
- remove(R.string.pref_anylatitude);
- remove(R.string.pref_anylongitude);
- }
- }
-
- public static Geopoint getAnyCoordinates() {
- if (contains(R.string.pref_anylatitude) && contains(R.string.pref_anylongitude)) {
- final float lat = getFloat(R.string.pref_anylatitude, 0);
- final float lon = getFloat(R.string.pref_anylongitude, 0);
- return new Geopoint(lat, lon);
- }
- return null;
- }
-
- public static boolean isUseCompass() {
- return useCompass;
- }
-
- public static void setUseCompass(final boolean value) {
- useCompass = value;
- }
-
- public static boolean isLightSkin() {
- return getBoolean(R.string.pref_skin, false);
- }
-
- @NonNull
- public static String getTwitterKeyConsumerPublic() {
- return TWITTER_KEY_CONSUMER_PUBLIC;
- }
-
- @NonNull
- public static String getTwitterKeyConsumerSecret() {
- return TWITTER_KEY_CONSUMER_SECRET;
- }
-
- public static String getWebDeviceCode() {
- return getString(R.string.pref_webDeviceCode, null);
- }
-
- public static boolean isRegisteredForSend2cgeo() {
- return getWebDeviceCode() != null;
- }
-
- static String getWebDeviceName() {
- 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));
- }
-
- /**
- * The Threshold for the showing of child waypoints
- */
- public static int getWayPointsThreshold() {
- return getInt(R.string.pref_showwaypointsthreshold, SHOW_WP_THRESHOLD_DEFAULT);
- }
-
- static void setShowWaypointsThreshold(final int threshold) {
- putInt(R.string.pref_showwaypointsthreshold, threshold);
- }
-
- public static boolean isUseTwitter() {
- return getBoolean(R.string.pref_twitter, false);
- }
-
- private static void setUseTwitter(final boolean useTwitter) {
- putBoolean(R.string.pref_twitter, useTwitter);
- }
-
- public static boolean isTwitterLoginValid() {
- return !StringUtils.isBlank(getTokenPublic())
- && !StringUtils.isBlank(getTokenSecret());
- }
-
- public static String getTokenPublic() {
- return getString(R.string.pref_twitter_token_public, null);
- }
-
- public static String getTokenSecret() {
- return getString(R.string.pref_twitter_token_secret, null);
-
- }
-
- static boolean hasTwitterAuthorization() {
- return StringUtils.isNotBlank(getTokenPublic())
- && StringUtils.isNotBlank(getTokenSecret());
- }
-
- public static void setTwitterTokens(@Nullable final String tokenPublic,
- @Nullable final String tokenSecret, final boolean enableTwitter) {
- putString(R.string.pref_twitter_token_public, tokenPublic);
- putString(R.string.pref_twitter_token_secret, tokenSecret);
- if (tokenPublic != null) {
- remove(R.string.pref_temp_twitter_token_public);
- remove(R.string.pref_temp_twitter_token_secret);
- }
- setUseTwitter(enableTwitter);
- }
-
- public static void setTwitterTempTokens(@Nullable final String tokenPublic,
- @Nullable final String tokenSecret) {
- putString(R.string.pref_temp_twitter_token_public, tokenPublic);
- putString(R.string.pref_temp_twitter_token_secret, tokenSecret);
- }
-
- public static ImmutablePair<String, String> getTempToken() {
- final String tokenPublic = getString(R.string.pref_temp_twitter_token_public, null);
- final String tokenSecret = getString(R.string.pref_temp_twitter_token_secret, null);
- return new ImmutablePair<>(tokenPublic, tokenSecret);
- }
-
- public static int getVersion() {
- return getInt(R.string.pref_version, 0);
- }
-
- public static void setVersion(final int version) {
- putInt(R.string.pref_version, version);
- }
-
- public static boolean isOpenLastDetailsPage() {
- return getBoolean(R.string.pref_opendetailslastpage, false);
- }
-
- public static int getLastDetailsPage() {
- return getInt(R.string.pref_lastdetailspage, 1);
- }
-
- public static void setLastDetailsPage(final int index) {
- putInt(R.string.pref_lastdetailspage, index);
- }
-
- public static int getDefaultNavigationTool() {
- return Integer.parseInt(getString(
- R.string.pref_defaultNavigationTool,
- String.valueOf(NavigationAppsEnum.COMPASS.id)));
- }
-
- public static int getDefaultNavigationTool2() {
- return Integer.parseInt(getString(
- R.string.pref_defaultNavigationTool2,
- String.valueOf(NavigationAppsEnum.INTERNAL_MAP.id)));
- }
-
- public static LivemapStrategy getLiveMapStrategy() {
- return LivemapStrategy.getById(getInt(R.string.pref_livemapstrategy, LivemapStrategy.AUTO.id));
- }
-
- public static void setLiveMapStrategy(final LivemapStrategy strategy) {
- putInt(R.string.pref_livemapstrategy, strategy.id);
- }
-
- public static boolean isDebug() {
- return Log.isDebug();
- }
-
- public static int getLiveMapHintShowCount() {
- return getInt(R.string.pref_livemaphintshowcount, 0);
- }
-
- public static void setLiveMapHintShowCount(final int showCount) {
- putInt(R.string.pref_livemaphintshowcount, showCount);
- }
-
- public static boolean isDbOnSDCard() {
- return getBoolean(R.string.pref_dbonsdcard, false);
- }
-
- public static void setDbOnSDCard(final boolean dbOnSDCard) {
- putBoolean(R.string.pref_dbonsdcard, dbOnSDCard);
- }
-
- public static String getGpxExportDir() {
- return getString(R.string.pref_gpxExportDir,
- Environment.getExternalStorageDirectory().getPath() + "/gpx");
- }
-
- public static String getGpxImportDir() {
- return getString(R.string.pref_gpxImportDir,
- Environment.getExternalStorageDirectory().getPath() + "/gpx");
- }
-
- public static boolean getShareAfterExport() {
- return getBoolean(R.string.pref_shareafterexport, true);
- }
-
- public static void setShareAfterExport(final boolean shareAfterExport) {
- putBoolean(R.string.pref_shareafterexport, shareAfterExport);
- }
-
- public static int getTrackableAction() {
- return getInt(R.string.pref_trackableaction, LogTypeTrackable.RETRIEVED_IT.id);
- }
-
- public static void setTrackableAction(final int trackableAction) {
- putInt(R.string.pref_trackableaction, trackableAction);
- }
-
- private static String getCustomRenderThemeBaseFolder() {
- return getString(R.string.pref_renderthemepath, "");
- }
-
- public static String getCustomRenderThemeFilePath() {
- return getString(R.string.pref_renderthemefile, "");
- }
-
- public static void setCustomRenderThemeFile(final String customRenderThemeFile) {
- putString(R.string.pref_renderthemefile, customRenderThemeFile);
- }
-
- public static File[] getMapThemeFiles() {
- final File directory = new File(getCustomRenderThemeBaseFolder());
- final List<File> result = new ArrayList<>();
- FileUtils.listDir(result, directory, new ExtensionsBasedFileSelector(new String[] { "xml" }), null);
-
- return result.toArray(new File[result.size()]);
- }
-
- private static class ExtensionsBasedFileSelector implements FileSelector {
- private final String[] extensions;
- public ExtensionsBasedFileSelector(final String[] extensions) {
- this.extensions = extensions;
- }
- @Override
- public boolean isSelected(final File file) {
- final String filename = file.getName();
- for (final String ext : extensions) {
- if (StringUtils.endsWithIgnoreCase(filename, ext)) {
- return true;
- }
- }
- return false;
- }
- @Override
- public boolean shouldEnd() {
- return false;
- }
- }
-
- /**
- * @return true if plain text log wanted
- */
- public static boolean getPlainLogs() {
- return getBoolean(R.string.pref_plainLogs, false);
- }
-
- /**
- * Force set the plain text log preference
- *
- * @param plainLogs
- * wanted or not
- */
- public static void setPlainLogs(final boolean plainLogs) {
- putBoolean(R.string.pref_plainLogs, plainLogs);
- }
-
- public static boolean getUseNativeUa() {
- return getBoolean(R.string.pref_nativeUa, false);
- }
-
- @NonNull
- public static String getCacheTwitterMessage() {
- return StringUtils.defaultString(getString(R.string.pref_twitter_cache_message, "I found [NAME] ([URL])."));
- }
-
- @NonNull
- public static String getTrackableTwitterMessage() {
- return StringUtils.defaultString(getString(R.string.pref_twitter_trackable_message, "I touched [NAME] ([URL])."));
- }
-
- public static int getLogImageScale() {
- return getInt(R.string.pref_logImageScale, -1);
- }
-
- public static void setLogImageScale(final int scale) {
- putInt(R.string.pref_logImageScale, scale);
- }
-
- public static void setExcludeMine(final boolean exclude) {
- putBoolean(R.string.pref_excludemine, exclude);
- }
-
- static void setLogin(final String username, final String password) {
- if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
- // erase username and password
- remove(R.string.pref_username);
- remove(R.string.pref_password);
- return;
- }
- // save username and password
- putString(R.string.pref_username, username);
- putString(R.string.pref_password, password);
- }
-
- public static long getFieldnoteExportDate() {
- return getLong(R.string.pref_fieldNoteExportDate, 0);
- }
-
- /**
- * Remember date of last field note export.
- */
- public static void setFieldnoteExportDate(final long date) {
- putLong(R.string.pref_fieldNoteExportDate, date);
- }
-
- public static boolean isUseNavigationApp(final NavigationAppsEnum navApp) {
- return getBoolean(navApp.preferenceKey, true);
- }
-
- /**
- * 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);
- }
-
- public static boolean getFieldNoteExportUpload() {
- return getBoolean(R.string.pref_fieldNoteExportUpload, true);
- }
-
- /**
- * 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);
- }
-
- public static boolean getFieldNoteExportOnlyNew() {
- return getBoolean(R.string.pref_fieldNoteExportOnlyNew, false);
- }
-
- public static String getECIconSet() {
- return getString(R.string.pref_ec_icons, "1");
- }
-
- /* Store last checksum of changelog for changelog display */
- public static long getLastChangelogChecksum() {
- return getLong(R.string.pref_changelog_last_checksum, 0);
- }
-
- public static void setLastChangelogChecksum(final long checksum) {
- putLong(R.string.pref_changelog_last_checksum, checksum);
- }
-
- public static List<String> getLastOpenedCaches() {
- final List<String> history = Arrays.asList(StringUtils.split(getString(R.string.pref_caches_history, StringUtils.EMPTY), HISTORY_SEPARATOR));
- return history.subList(0, Math.min(HISTORY_SIZE, history.size()));
- }
-
- public static void addCacheToHistory(@NonNull final String geocode) {
- final List<String> history = new ArrayList<>(getLastOpenedCaches());
- // bring entry to front, if it already existed
- history.remove(geocode);
- history.add(0, geocode);
- putString(R.string.pref_caches_history, StringUtils.join(history, HISTORY_SEPARATOR));
- }
-
- public static boolean useHardwareAcceleration() {
- return getBoolean(R.string.pref_hardware_acceleration, !HW_ACCEL_DISABLED_BY_DEFAULT);
- }
-
- 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);
- }
-
- public static void setForceOrientationSensor(final boolean forceOrientationSensor) {
- putBoolean(R.string.pref_force_orientation_sensor, forceOrientationSensor);
- }
-
- public static boolean useOrientationSensor(final Context context) {
- return OrientationProvider.hasOrientationSensor(context) && (getBoolean(R.string.pref_force_orientation_sensor, false) || !RotationProvider.hasRotationSensor(context));
- }
-}
+package cgeo.geocaching.settings;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.R;
+import cgeo.geocaching.apps.navi.NavigationAppFactory.NavigationAppsEnum;
+import cgeo.geocaching.connector.capability.ICredentials;
+import cgeo.geocaching.connector.gc.GCConnector;
+import cgeo.geocaching.connector.gc.GCConstants;
+import cgeo.geocaching.enumerations.CacheType;
+import cgeo.geocaching.enumerations.LogTypeTrackable;
+import cgeo.geocaching.list.StoredList;
+import cgeo.geocaching.location.Geopoint;
+import cgeo.geocaching.maps.CGeoMap.MapMode;
+import cgeo.geocaching.maps.LivemapStrategy;
+import cgeo.geocaching.maps.MapProviderFactory;
+import cgeo.geocaching.maps.google.v1.GoogleMapProvider;
+import cgeo.geocaching.maps.interfaces.GeoPointImpl;
+import cgeo.geocaching.maps.interfaces.MapProvider;
+import cgeo.geocaching.maps.interfaces.MapSource;
+import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider;
+import cgeo.geocaching.maps.mapsforge.MapsforgeMapProvider.OfflineMapSource;
+import cgeo.geocaching.sensors.OrientationProvider;
+import cgeo.geocaching.sensors.RotationProvider;
+import cgeo.geocaching.utils.CryptUtils;
+import cgeo.geocaching.utils.FileUtils;
+import cgeo.geocaching.utils.FileUtils.FileSelector;
+import cgeo.geocaching.utils.Log;
+
+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.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * General c:geo preferences/settings set by the user
+ */
+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 = ',';
+ private static final int SHOW_WP_THRESHOLD_DEFAULT = 10;
+ public static final int SHOW_WP_THRESHOLD_MAX = 50;
+ private static final int MAP_SOURCE_DEFAULT = GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
+
+ public static final boolean HW_ACCEL_DISABLED_BY_DEFAULT =
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ||
+ StringUtils.equals(Build.MODEL, "HTC One X") || // HTC One X
+ StringUtils.equals(Build.MODEL, "HTC One S") || // HTC One S
+ StringUtils.equals(Build.MODEL, "GT-I8190") || // Samsung S3 mini
+ StringUtils.equals(Build.MODEL, "GT-S6310L") || // Samsung Galaxy Young
+ StringUtils.equals(Build.MODEL, "GT-P5210") || // Samsung Galaxy Tab 3
+ StringUtils.equals(Build.MODEL, "GT-S7580") || // Samsung Galaxy Trend Plus
+ StringUtils.equals(Build.MODEL, "GT-I9105P") || // Samsung Galaxy SII Plus
+ StringUtils.equals(Build.MODEL, "ST25i") || // Sony Xperia U
+ StringUtils.equals(Build.MODEL, "bq Aquaris 5") || // bq Aquaris 5
+ StringUtils.equals(Build.MODEL, "A1-810") || // Unknown A1-810
+ StringUtils.equals(Build.MODEL, "GT-I9195") || // Samsung S4 mini
+ StringUtils.equals(Build.MODEL, "GT-I8200N"); // Samsung S3 mini
+
+ // twitter api keys
+ 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,
+ Deg,
+ Min,
+ Sec;
+
+ static final int DEFAULT_INT_VALUE = Min.ordinal();
+
+ public static CoordInputFormatEnum fromInt(final int id) {
+ final CoordInputFormatEnum[] values = CoordInputFormatEnum.values();
+ if (id < 0 || id >= values.length) {
+ return Min;
+ }
+ return values[id];
+ }
+ }
+
+ private static final SharedPreferences sharedPrefs = PreferenceManager
+ .getDefaultSharedPreferences(CgeoApplication.getInstance().getBaseContext());
+ static {
+ migrateSettings();
+ final boolean isDebug = sharedPrefs.getBoolean(getKey(R.string.pref_debug), false);
+ Log.setDebug(isDebug);
+ CgeoApplication.dumpOnOutOfMemory(isDebug);
+ }
+
+ /**
+ * Cache the mapsource locally. If that is an offline map source, each request would potentially access the
+ * underlying map file, leading to delays.
+ */
+ private static MapSource mapSource;
+
+ protected Settings() {
+ throw new InstantiationError();
+ }
+
+ private static void migrateSettings() {
+ 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();
+
+ 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));
+
+ e.putInt(getKey(R.string.pref_settingsversion), 1); // mark migrated
+ e.apply();
+ }
+
+ // changes for new settings dialog
+ if (currentVersion < 2) {
+ final Editor e = sharedPrefs.edit();
+
+ e.putBoolean(getKey(R.string.pref_units_imperial), useImperialUnits());
+
+ // show waypoints threshold now as a slider
+ int wpThreshold = getWayPointsThreshold();
+ if (wpThreshold < 0) {
+ wpThreshold = 0;
+ } else if (wpThreshold > SHOW_WP_THRESHOLD_MAX) {
+ wpThreshold = SHOW_WP_THRESHOLD_MAX;
+ }
+ e.putInt(getKey(R.string.pref_showwaypointsthreshold), wpThreshold);
+
+ // KEY_MAP_SOURCE must be string, because it is the key for a ListPreference now
+ final int ms = sharedPrefs.getInt(getKey(R.string.pref_mapsource), MAP_SOURCE_DEFAULT);
+ e.remove(getKey(R.string.pref_mapsource));
+ e.putString(getKey(R.string.pref_mapsource), String.valueOf(ms));
+
+ // navigation tool ids must be string, because ListPreference uses strings as keys
+ final int dnt1 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool), NavigationAppsEnum.COMPASS.id);
+ final int dnt2 = sharedPrefs.getInt(getKey(R.string.pref_defaultNavigationTool2), NavigationAppsEnum.INTERNAL_MAP.id);
+ e.remove(getKey(R.string.pref_defaultNavigationTool));
+ e.remove(getKey(R.string.pref_defaultNavigationTool2));
+ e.putString(getKey(R.string.pref_defaultNavigationTool), String.valueOf(dnt1));
+ e.putString(getKey(R.string.pref_defaultNavigationTool2), String.valueOf(dnt2));
+
+ // defaults for gpx directories
+ e.putString(getKey(R.string.pref_gpxImportDir), getGpxImportDir());
+ e.putString(getKey(R.string.pref_gpxExportDir), getGpxExportDir());
+
+ e.putInt(getKey(R.string.pref_settingsversion), 2); // mark migrated
+ e.apply();
+ }
+ }
+
+ private static String getKey(final int prefKeyId) {
+ return CgeoApplication.getInstance().getString(prefKeyId);
+ }
+
+ static String getString(final int prefKeyId, final String defaultValue) {
+ return sharedPrefs.getString(getKey(prefKeyId), defaultValue);
+ }
+
+ private static int getInt(final int prefKeyId, final int defaultValue) {
+ return sharedPrefs.getInt(getKey(prefKeyId), defaultValue);
+ }
+
+ private static long getLong(final int prefKeyId, final long defaultValue) {
+ return sharedPrefs.getLong(getKey(prefKeyId), defaultValue);
+ }
+
+ private static boolean getBoolean(final int prefKeyId, final boolean defaultValue) {
+ return sharedPrefs.getBoolean(getKey(prefKeyId), defaultValue);
+ }
+
+ private static float getFloat(final int prefKeyId, final float defaultValue) {
+ return sharedPrefs.getFloat(getKey(prefKeyId), defaultValue);
+ }
+
+ protected static void putString(final int prefKeyId, final String value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putString(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ protected static void putBoolean(final int prefKeyId, final boolean value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putBoolean(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void putInt(final int prefKeyId, final int value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putInt(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void putLong(final int prefKeyId, final long value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putLong(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void putFloat(final int prefKeyId, final float value) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.putFloat(getKey(prefKeyId), value);
+ edit.apply();
+ }
+
+ private static void remove(final int prefKeyId) {
+ final SharedPreferences.Editor edit = sharedPrefs.edit();
+ edit.remove(getKey(prefKeyId));
+ edit.apply();
+ }
+
+ private static boolean contains(final int prefKeyId) {
+ return sharedPrefs.contains(getKey(prefKeyId));
+ }
+
+ public static boolean hasGCCredentials() {
+ final String preUsername = getString(R.string.pref_username, null);
+ final String prePassword = getString(R.string.pref_password, null);
+
+ return !StringUtils.isBlank(preUsername) && !StringUtils.isBlank(prePassword);
+ }
+
+ /**
+ * Get login and password information of Geocaching.com.
+ *
+ * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
+ */
+ public static ImmutablePair<String, String> getGcCredentials() {
+ return getCredentials(GCConnector.getInstance());
+ }
+
+ /**
+ * Get login and password information.
+ *
+ * @return a pair either with (login, password) or (empty, empty) if no valid information is stored
+ */
+ public static ImmutablePair<String, String> getCredentials(final @NonNull ICredentials connector) {
+ final String username = getString(connector.getUsernamePreferenceKey(), null);
+ final String password = getString(connector.getPasswordPreferenceKey(), null);
+
+ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
+ return new ImmutablePair<>(StringUtils.EMPTY, StringUtils.EMPTY);
+ }
+
+ return new ImmutablePair<>(username, password);
+ }
+
+ public static String getUsername() {
+ return getString(R.string.pref_username, StringUtils.EMPTY);
+ }
+
+ public static boolean isGCConnectorActive() {
+ return getBoolean(R.string.pref_connectorGCActive, true);
+ }
+
+ public static boolean isECConnectorActive() {
+ return getBoolean(R.string.pref_connectorECActive, false);
+ }
+
+ public static boolean isGCPremiumMember() {
+ final String memberStatus = 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 void setGCMemberStatus(final String memberStatus) {
+ if (StringUtils.isBlank(memberStatus)) {
+ remove(R.string.pref_memberstatus);
+ }
+ putString(R.string.pref_memberstatus, memberStatus);
+ }
+
+ public static ImmutablePair<String, String> getTokenPair(final int tokenPublicPrefKey, final int tokenSecretPrefKey) {
+ return new ImmutablePair<>(getString(tokenPublicPrefKey, null), getString(tokenSecretPrefKey, null));
+ }
+
+ public static void setTokens(final int tokenPublicPrefKey, @Nullable final String tokenPublic, final int tokenSecretPrefKey, @Nullable final String tokenSecret) {
+ if (tokenPublic == null) {
+ remove(tokenPublicPrefKey);
+ } else {
+ putString(tokenPublicPrefKey, tokenPublic);
+ }
+ if (tokenSecret == null) {
+ remove(tokenSecretPrefKey);
+ } else {
+ putString(tokenSecretPrefKey, tokenSecret);
+ }
+ }
+
+ public static boolean isOCConnectorActive(final int isActivePrefKeyId) {
+ return getBoolean(isActivePrefKeyId, false);
+ }
+
+ public static boolean hasOCAuthorization(final int tokenPublicPrefKeyId, final int tokenSecretPrefKeyId) {
+ return StringUtils.isNotBlank(getString(tokenPublicPrefKeyId, ""))
+ && StringUtils.isNotBlank(getString(tokenSecretPrefKeyId, ""));
+ }
+
+ public static boolean isGCVoteLogin() {
+ return getGCVoteLogin() != null;
+ }
+
+ public static ImmutablePair<String, String> getGCVoteLogin() {
+ final String username = getString(R.string.pref_username, null);
+ final String password = getString(R.string.pref_pass_vote, null);
+
+ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
+ return null;
+ }
+
+ return new ImmutablePair<>(username, password);
+ }
+
+ public static String getSignature() {
+ return getString(R.string.pref_signature, StringUtils.EMPTY);
+ }
+
+ public static void setCookieStore(final String cookies) {
+ if (StringUtils.isBlank(cookies)) {
+ // erase cookies
+ remove(R.string.pref_cookiestore);
+ }
+ // save 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
+ */
+ public static void setCacheType(final CacheType cacheType) {
+ if (cacheType == null) {
+ remove(R.string.pref_cachetype);
+ } else {
+ putString(R.string.pref_cachetype, cacheType.id);
+ }
+ }
+
+ public static int getLastList() {
+ return getInt(R.string.pref_lastusedlist, StoredList.STANDARD_LIST_ID);
+ }
+
+ public static void saveLastList(final int listId) {
+ putInt(R.string.pref_lastusedlist, listId);
+ }
+
+ public static void setWebNameCode(final String name, final String code) {
+ putString(R.string.pref_webDeviceName, name);
+ putString(R.string.pref_webDeviceCode, code);
+ }
+
+ public static MapProvider getMapProvider() {
+ return getMapSource().getMapProvider();
+ }
+
+ public static String getMapFile() {
+ return getString(R.string.pref_mapfile, null);
+ }
+
+ static void setMapFile(final String mapFile) {
+ putString(R.string.pref_mapfile, mapFile);
+ if (mapFile != null) {
+ setMapFileDirectory(new File(mapFile).getParent());
+ }
+ }
+
+ public static String getMapFileDirectory() {
+ final String mapDir = getString(R.string.pref_mapDirectory, null);
+ if (mapDir != null) {
+ return mapDir;
+ }
+ final String mapFile = getMapFile();
+ if (mapFile != null) {
+ return new File(mapFile).getParent();
+ }
+ return null;
+ }
+
+ static void setMapFileDirectory(final String mapFileDirectory) {
+ putString(R.string.pref_mapDirectory, mapFileDirectory);
+ MapsforgeMapProvider.getInstance().updateOfflineMaps();
+ }
+
+ private static boolean isValidMapFile() {
+ return isValidMapFile(getMapFile());
+ }
+
+ public static boolean isValidMapFile(final String mapFileIn) {
+ return MapsforgeMapProvider.isValidMapFile(mapFileIn);
+ }
+
+ public static boolean isScaleMapsforgeText() {
+ return getBoolean(R.string.pref_mapsforge_scale_text, true);
+ }
+
+ public static CoordInputFormatEnum getCoordInputFormat() {
+ return CoordInputFormatEnum.fromInt(getInt(R.string.pref_coordinputformat, CoordInputFormatEnum.DEFAULT_INT_VALUE));
+ }
+
+ public static void setCoordInputFormat(final CoordInputFormatEnum format) {
+ putInt(R.string.pref_coordinputformat, format.ordinal());
+ }
+
+ public static boolean getLogOffline() {
+ return getBoolean(R.string.pref_log_offline, false);
+ }
+
+ public static boolean getChooseList() {
+ return getBoolean(R.string.pref_choose_list, false);
+ }
+
+ public static boolean getLoadDirImg() {
+ return !isGCPremiumMember() && getBoolean(R.string.pref_loaddirectionimg, true);
+ }
+
+ public static void setGcCustomDate(final String format) {
+ putString(R.string.pref_gccustomdate, format);
+ }
+
+ /**
+ * @return User selected date format on GC.com
+ */
+ public static String getGcCustomDate() {
+ // 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() {
+ return getBoolean(R.string.pref_excludemine, false);
+ }
+
+ public static boolean useEnglish() {
+ return getBoolean(R.string.pref_useenglish, false);
+ }
+
+ public static boolean isShowAddress() {
+ return getBoolean(R.string.pref_showaddress, true);
+ }
+
+ public static boolean isShowCaptcha() {
+ return !isGCPremiumMember() && getBoolean(R.string.pref_showcaptcha, false);
+ }
+
+ public static boolean isExcludeDisabledCaches() {
+ return getBoolean(R.string.pref_excludedisabled, false);
+ }
+
+ public static boolean isStoreOfflineMaps() {
+ return getBoolean(R.string.pref_offlinemaps, true);
+ }
+
+ public static boolean isStoreOfflineWpMaps() {
+ return getBoolean(R.string.pref_offlinewpmaps, false);
+ }
+
+ public static boolean isStoreLogImages() {
+ return getBoolean(R.string.pref_logimages, false);
+ }
+
+ public static boolean isAutoLoadDescription() {
+ return getBoolean(R.string.pref_autoloaddesc, true);
+ }
+
+ public static boolean isRatingWanted() {
+ return getBoolean(R.string.pref_ratingwanted, true);
+ }
+
+ public static boolean isGeokretyConnectorActive() {
+ return getBoolean(R.string.pref_connectorGeokretyActive, false);
+ }
+
+ public static boolean isGeokretyCacheActive() {
+ return getBoolean(R.string.pref_geokrety_cache, true);
+ }
+
+ static boolean hasGeokretyAuthorization() {
+ return StringUtils.isNotBlank(getGeokretySecId());
+ }
+
+ public static String getGeokretySecId() {
+ return getString(R.string.pref_fakekey_geokrety_authorization, null);
+ }
+
+ public static void setGeokretySecId(final String secid) {
+ putString(R.string.pref_fakekey_geokrety_authorization, secid);
+ }
+
+ public static boolean isRegisteredForGeokretyLogging() {
+ return getGeokretySecId() != null;
+ }
+
+ /**
+ * Retrieve showed popup counter for warning about logging Trackable recommend Geocode
+ *
+ * @return number of times the popup has appeared
+ */
+ public static int getLogTrackableWithoutGeocodeShowCount() {
+ return getInt(R.string.pref_logtrackablewithoutgeocodeshowcount, 0);
+ }
+
+ /**
+ * Store showed popup counter for warning about logging Trackable recommend Geocode
+ *
+ * @param showCount the count to save
+ */
+ public static void setLogTrackableWithoutGeocodeShowCount(final int showCount) {
+ putInt(R.string.pref_logtrackablewithoutgeocodeshowcount, showCount);
+ }
+
+ public static boolean isFriendLogsWanted() {
+ if (!hasGCCredentials()) {
+ // don't show a friends log if the user is anonymous
+ return false;
+ }
+ return getBoolean(R.string.pref_friendlogswanted, true);
+ }
+
+ public static boolean isLiveList() {
+ return getBoolean(R.string.pref_livelist, true);
+ }
+
+ public static boolean isTrackableAutoVisit() {
+ return getBoolean(R.string.pref_trackautovisit, false);
+ }
+
+ public static boolean isAutoInsertSignature() {
+ return getBoolean(R.string.pref_sigautoinsert, false);
+ }
+
+ 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());
+ }
+
+ private static boolean useImperialUnitsByDefault() {
+ final String countryCode = Locale.getDefault().getCountry();
+ return "US".equals(countryCode) // USA
+ || "LR".equals(countryCode) // Liberia
+ || "MM".equals(countryCode); // Burma
+ }
+
+ public static boolean isLiveMap() {
+ return getBoolean(R.string.pref_maplive, true);
+ }
+
+ public static void setLiveMap(final boolean live) {
+ putBoolean(R.string.pref_maplive, live);
+ }
+
+ public static boolean isMapTrail() {
+ return getBoolean(R.string.pref_maptrail, false);
+ }
+
+ public static void setMapTrail(final boolean showTrail) {
+ putBoolean(R.string.pref_maptrail, showTrail);
+ }
+
+ /**
+ * whether to show a direction line on the map
+ */
+ public static boolean isMapDirection() {
+ return getBoolean(R.string.pref_map_direction, true);
+ }
+
+ public static void setMapDirection(final boolean showDirection) {
+ putBoolean(R.string.pref_map_direction, showDirection);
+ }
+
+ /**
+ * 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 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,
+ getInt(R.string.pref_lastmaplon, 0) / 1e6));
+ }
+
+ public static void setMapCenter(final GeoPointImpl mapViewCenter) {
+ putInt(R.string.pref_lastmaplat, mapViewCenter.getLatitudeE6());
+ putInt(R.string.pref_lastmaplon, mapViewCenter.getLongitudeE6());
+ }
+
+ @NonNull
+ public static synchronized MapSource getMapSource() {
+ if (mapSource != null) {
+ return mapSource;
+ }
+ final int id = getConvertedMapId();
+ mapSource = MapProviderFactory.getMapSource(id);
+ if (mapSource != null) {
+ // don't use offline maps if the map file is not valid
+ if (!(mapSource instanceof OfflineMapSource) || isValidMapFile()) {
+ return mapSource;
+ }
+ }
+ // fallback to first available map
+ return MapProviderFactory.getDefaultSource();
+ }
+
+ private final static int GOOGLEMAP_BASEID = 30;
+ private final static int MAP = 1;
+ private final static int SATELLITE = 2;
+
+ private final static int MFMAP_BASEID = 40;
+ private final static int MAPNIK = 1;
+ private final static int CYCLEMAP = 3;
+ private final static int OFFLINE = 4;
+ private static final int HISTORY_SIZE = 10;
+
+ /**
+ * 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,
+ String.valueOf(MAP_SOURCE_DEFAULT)));
+ switch (id) {
+ case GOOGLEMAP_BASEID + MAP:
+ return GoogleMapProvider.GOOGLE_MAP_ID.hashCode();
+ case GOOGLEMAP_BASEID + SATELLITE:
+ return GoogleMapProvider.GOOGLE_SATELLITE_ID.hashCode();
+ case MFMAP_BASEID + MAPNIK:
+ return MapsforgeMapProvider.MAPSFORGE_MAPNIK_ID.hashCode();
+ case MFMAP_BASEID + CYCLEMAP:
+ return MapsforgeMapProvider.MAPSFORGE_CYCLEMAP_ID.hashCode();
+ case MFMAP_BASEID + OFFLINE: {
+ final String mapFile = getMapFile();
+ if (StringUtils.isNotEmpty(mapFile)) {
+ return mapFile.hashCode();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return id;
+ }
+
+ 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());
+ }
+ // cache the value
+ mapSource = newMapSource;
+ }
+
+ public static void setAnyCoordinates(final Geopoint coords) {
+ if (null != coords) {
+ putFloat(R.string.pref_anylatitude, (float) coords.getLatitude());
+ putFloat(R.string.pref_anylongitude, (float) coords.getLongitude());
+ } else {
+ remove(R.string.pref_anylatitude);
+ remove(R.string.pref_anylongitude);
+ }
+ }
+
+ public static Geopoint getAnyCoordinates() {
+ if (contains(R.string.pref_anylatitude) && contains(R.string.pref_anylongitude)) {
+ final float lat = getFloat(R.string.pref_anylatitude, 0);
+ final float lon = getFloat(R.string.pref_anylongitude, 0);
+ return new Geopoint(lat, lon);
+ }
+ return null;
+ }
+
+ public static boolean isUseCompass() {
+ return useCompass;
+ }
+
+ public static void setUseCompass(final boolean value) {
+ useCompass = value;
+ }
+
+ public static boolean isLightSkin() {
+ return getBoolean(R.string.pref_skin, false);
+ }
+
+ @NonNull
+ public static String getTwitterKeyConsumerPublic() {
+ return TWITTER_KEY_CONSUMER_PUBLIC;
+ }
+
+ @NonNull
+ public static String getTwitterKeyConsumerSecret() {
+ return TWITTER_KEY_CONSUMER_SECRET;
+ }
+
+ public static String getWebDeviceCode() {
+ return getString(R.string.pref_webDeviceCode, null);
+ }
+
+ public static boolean isRegisteredForSend2cgeo() {
+ return getWebDeviceCode() != null;
+ }
+
+ static String getWebDeviceName() {
+ 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));
+ }
+
+ /**
+ * The Threshold for the showing of child waypoints
+ */
+ public static int getWayPointsThreshold() {
+ return getInt(R.string.pref_showwaypointsthreshold, SHOW_WP_THRESHOLD_DEFAULT);
+ }
+
+ static void setShowWaypointsThreshold(final int threshold) {
+ putInt(R.string.pref_showwaypointsthreshold, threshold);
+ }
+
+ public static boolean isUseTwitter() {
+ return getBoolean(R.string.pref_twitter, false);
+ }
+
+ private static void setUseTwitter(final boolean useTwitter) {
+ putBoolean(R.string.pref_twitter, useTwitter);
+ }
+
+ public static boolean isTwitterLoginValid() {
+ return !StringUtils.isBlank(getTokenPublic())
+ && !StringUtils.isBlank(getTokenSecret());
+ }
+
+ public static String getTokenPublic() {
+ return getString(R.string.pref_twitter_token_public, null);
+ }
+
+ public static String getTokenSecret() {
+ return getString(R.string.pref_twitter_token_secret, null);
+
+ }
+
+ static boolean hasTwitterAuthorization() {
+ return StringUtils.isNotBlank(getTokenPublic())
+ && StringUtils.isNotBlank(getTokenSecret());
+ }
+
+ public static void setTwitterTokens(@Nullable final String tokenPublic,
+ @Nullable final String tokenSecret, final boolean enableTwitter) {
+ putString(R.string.pref_twitter_token_public, tokenPublic);
+ putString(R.string.pref_twitter_token_secret, tokenSecret);
+ if (tokenPublic != null) {
+ remove(R.string.pref_temp_twitter_token_public);
+ remove(R.string.pref_temp_twitter_token_secret);
+ }
+ setUseTwitter(enableTwitter);
+ }
+
+ public static void setTwitterTempTokens(@Nullable final String tokenPublic,
+ @Nullable final String tokenSecret) {
+ putString(R.string.pref_temp_twitter_token_public, tokenPublic);
+ putString(R.string.pref_temp_twitter_token_secret, tokenSecret);
+ }
+
+ public static ImmutablePair<String, String> getTempToken() {
+ final String tokenPublic = getString(R.string.pref_temp_twitter_token_public, null);
+ final String tokenSecret = getString(R.string.pref_temp_twitter_token_secret, null);
+ return new ImmutablePair<>(tokenPublic, tokenSecret);
+ }
+
+ public static int getVersion() {
+ return getInt(R.string.pref_version, 0);
+ }
+
+ public static void setVersion(final int version) {
+ putInt(R.string.pref_version, version);
+ }
+
+ public static boolean isOpenLastDetailsPage() {
+ return getBoolean(R.string.pref_opendetailslastpage, false);
+ }
+
+ public static int getLastDetailsPage() {
+ return getInt(R.string.pref_lastdetailspage, 1);
+ }
+
+ public static void setLastDetailsPage(final int index) {
+ putInt(R.string.pref_lastdetailspage, index);
+ }
+
+ public static int getDefaultNavigationTool() {
+ return Integer.parseInt(getString(
+ R.string.pref_defaultNavigationTool,
+ String.valueOf(NavigationAppsEnum.COMPASS.id)));
+ }
+
+ public static int getDefaultNavigationTool2() {
+ return Integer.parseInt(getString(
+ R.string.pref_defaultNavigationTool2,
+ String.valueOf(NavigationAppsEnum.INTERNAL_MAP.id)));
+ }
+
+ public static LivemapStrategy getLiveMapStrategy() {
+ return LivemapStrategy.getById(getInt(R.string.pref_livemapstrategy, LivemapStrategy.AUTO.id));
+ }
+
+ public static void setLiveMapStrategy(final LivemapStrategy strategy) {
+ putInt(R.string.pref_livemapstrategy, strategy.id);
+ }
+
+ public static boolean isDebug() {
+ return Log.isDebug();
+ }
+
+ public static int getLiveMapHintShowCount() {
+ return getInt(R.string.pref_livemaphintshowcount, 0);
+ }
+
+ public static void setLiveMapHintShowCount(final int showCount) {
+ putInt(R.string.pref_livemaphintshowcount, showCount);
+ }
+
+ public static boolean isDbOnSDCard() {
+ return getBoolean(R.string.pref_dbonsdcard, false);
+ }
+
+ public static void setDbOnSDCard(final boolean dbOnSDCard) {
+ putBoolean(R.string.pref_dbonsdcard, dbOnSDCard);
+ }
+
+ public static String getGpxExportDir() {
+ return getString(R.string.pref_gpxExportDir,
+ Environment.getExternalStorageDirectory().getPath() + "/gpx");
+ }
+
+ public static String getGpxImportDir() {
+ return getString(R.string.pref_gpxImportDir,
+ Environment.getExternalStorageDirectory().getPath() + "/gpx");
+ }
+
+ public static boolean getShareAfterExport() {
+ return getBoolean(R.string.pref_shareafterexport, true);
+ }
+
+ public static void setShareAfterExport(final boolean shareAfterExport) {
+ putBoolean(R.string.pref_shareafterexport, shareAfterExport);
+ }
+
+ public static int getTrackableAction() {
+ return getInt(R.string.pref_trackableaction, LogTypeTrackable.RETRIEVED_IT.id);
+ }
+
+ public static void setTrackableAction(final int trackableAction) {
+ putInt(R.string.pref_trackableaction, trackableAction);
+ }
+
+ private static String getCustomRenderThemeBaseFolder() {
+ return getString(R.string.pref_renderthemepath, "");
+ }
+
+ public static String getCustomRenderThemeFilePath() {
+ return getString(R.string.pref_renderthemefile, "");
+ }
+
+ public static void setCustomRenderThemeFile(final String customRenderThemeFile) {
+ putString(R.string.pref_renderthemefile, customRenderThemeFile);
+ }
+
+ public static File[] getMapThemeFiles() {
+ final File directory = new File(getCustomRenderThemeBaseFolder());
+ final List<File> result = new ArrayList<>();
+ FileUtils.listDir(result, directory, new ExtensionsBasedFileSelector(new String[] { "xml" }), null);
+
+ return result.toArray(new File[result.size()]);
+ }
+
+ private static class ExtensionsBasedFileSelector implements FileSelector {
+ private final String[] extensions;
+ public ExtensionsBasedFileSelector(final String[] extensions) {
+ this.extensions = extensions;
+ }
+ @Override
+ public boolean isSelected(final File file) {
+ final String filename = file.getName();
+ for (final String ext : extensions) {
+ if (StringUtils.endsWithIgnoreCase(filename, ext)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ @Override
+ public boolean shouldEnd() {
+ return false;
+ }
+ }
+
+ /**
+ * @return true if plain text log wanted
+ */
+ public static boolean getPlainLogs() {
+ return getBoolean(R.string.pref_plainLogs, false);
+ }
+
+ /**
+ * Force set the plain text log preference
+ *
+ * @param plainLogs
+ * wanted or not
+ */
+ public static void setPlainLogs(final boolean plainLogs) {
+ putBoolean(R.string.pref_plainLogs, plainLogs);
+ }
+
+ public static boolean getUseNativeUa() {
+ return getBoolean(R.string.pref_nativeUa, false);
+ }
+
+ public static String getCacheTwitterMessage() {
+ return getString(R.string.pref_twitter_cache_message, "I found [NAME] ([URL]).");
+ }
+
+ public static String getTrackableTwitterMessage() {
+ return getString(R.string.pref_twitter_trackable_message, "I touched [NAME] ([URL]).");
+ }
+
+ public static int getLogImageScale() {
+ return getInt(R.string.pref_logImageScale, -1);
+ }
+
+ public static void setLogImageScale(final int scale) {
+ putInt(R.string.pref_logImageScale, scale);
+ }
+
+ public static void setExcludeMine(final boolean exclude) {
+ putBoolean(R.string.pref_excludemine, exclude);
+ }
+
+ static void setLogin(final String username, final String password) {
+ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
+ // erase username and password
+ remove(R.string.pref_username);
+ remove(R.string.pref_password);
+ return;
+ }
+ // save username and password
+ putString(R.string.pref_username, username);
+ putString(R.string.pref_password, password);
+ }
+
+ public static long getFieldnoteExportDate() {
+ return getLong(R.string.pref_fieldNoteExportDate, 0);
+ }
+
+ /**
+ * Remember date of last field note export.
+ */
+ public static void setFieldnoteExportDate(final long date) {
+ putLong(R.string.pref_fieldNoteExportDate, date);
+ }
+
+ public static boolean isUseNavigationApp(final NavigationAppsEnum navApp) {
+ return getBoolean(navApp.preferenceKey, true);
+ }
+
+ /**
+ * 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);
+ }
+
+ public static boolean getFieldNoteExportUpload() {
+ return getBoolean(R.string.pref_fieldNoteExportUpload, true);
+ }
+
+ /**
+ * 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);
+ }
+
+ public static boolean getFieldNoteExportOnlyNew() {
+ return getBoolean(R.string.pref_fieldNoteExportOnlyNew, false);
+ }
+
+ public static String getECIconSet() {
+ return getString(R.string.pref_ec_icons, "1");
+ }
+
+ /* Store last checksum of changelog for changelog display */
+ public static long getLastChangelogChecksum() {
+ return getLong(R.string.pref_changelog_last_checksum, 0);
+ }
+
+ public static void setLastChangelogChecksum(final long checksum) {
+ putLong(R.string.pref_changelog_last_checksum, checksum);
+ }
+
+ public static List<String> getLastOpenedCaches() {
+ final List<String> history = Arrays.asList(StringUtils.split(getString(R.string.pref_caches_history, StringUtils.EMPTY), HISTORY_SEPARATOR));
+ return history.subList(0, Math.min(HISTORY_SIZE, history.size()));
+ }
+
+ public static void addCacheToHistory(@NonNull final String geocode) {
+ final List<String> history = new ArrayList<>(getLastOpenedCaches());
+ // bring entry to front, if it already existed
+ history.remove(geocode);
+ history.add(0, geocode);
+ putString(R.string.pref_caches_history, StringUtils.join(history, HISTORY_SEPARATOR));
+ }
+
+ public static boolean useHardwareAcceleration() {
+ return getBoolean(R.string.pref_hardware_acceleration, !HW_ACCEL_DISABLED_BY_DEFAULT);
+ }
+
+ 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);
+ }
+
+ public static void setForceOrientationSensor(final boolean forceOrientationSensor) {
+ putBoolean(R.string.pref_force_orientation_sensor, forceOrientationSensor);
+ }
+
+ public static boolean useOrientationSensor(final Context context) {
+ return OrientationProvider.hasOrientationSensor(context) && (getBoolean(R.string.pref_force_orientation_sensor, false) || !RotationProvider.hasRotationSensor(context));
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/CryptUtils.java b/main/src/cgeo/geocaching/utils/CryptUtils.java
index 6ef8c25..4aec509 100644
--- a/main/src/cgeo/geocaching/utils/CryptUtils.java
+++ b/main/src/cgeo/geocaching/utils/CryptUtils.java
@@ -1,156 +1,153 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.lang3.CharEncoding;
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-
-import java.io.UnsupportedEncodingException;
-import java.math.BigInteger;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-public final class CryptUtils {
-
- private CryptUtils() {
- // utility class
- }
-
- 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;
- }
- for (char c = 'a'; c <= 'z'; c++) {
- BASE64MAP1[i++] = c;
- }
- for (char c = '0'; c <= '9'; c++) {
- BASE64MAP1[i++] = c;
- }
- BASE64MAP1[i++] = '+';
- BASE64MAP1[i++] = '/';
-
- for (i = 0; i < BASE64MAP2.length; i++) {
- BASE64MAP2[i] = -1;
- }
- for (i = 0; i < 64; i++) {
- BASE64MAP2[BASE64MAP1[i]] = (byte) i;
- }
- }
-
- private static class Rot13Encryption {
- private boolean plaintext = false;
-
- char getNextEncryptedCharacter(final char c) {
- int result = c;
- if (result == '[') {
- plaintext = true;
- } else if (result == ']') {
- plaintext = false;
- } else if (!plaintext) {
- final int capitalized = result & 32;
- result &= ~capitalized;
- result = ((result >= 'A') && (result <= 'Z') ? ((result - 'A' + 13) % 26 + 'A') : result)
- | capitalized;
- }
- return (char) result;
- }
- }
-
- @NonNull
- public static String rot13(final String text) {
- if (text == null) {
- return StringUtils.EMPTY;
- }
- final StringBuilder result = new StringBuilder();
- final Rot13Encryption rot13 = new Rot13Encryption();
-
- final int length = text.length();
- for (int index = 0; index < length; index++) {
- final char c = text.charAt(index);
- result.append(rot13.getNextEncryptedCharacter(c));
- }
- return result.toString();
- }
-
- @NonNull
- 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 | UnsupportedEncodingException e) {
- Log.e("CryptUtils.md5", e);
- }
-
- return StringUtils.EMPTY;
- }
-
- @NonNull
- 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);
- return mac.doFinal(text.getBytes(CharEncoding.UTF_8));
- } catch (GeneralSecurityException | UnsupportedEncodingException e) {
- Log.e("CryptUtils.hashHmac", e);
- return EMPTY;
- }
- }
-
- @NonNull
- public static CharSequence rot13(final Spannable span) {
- // I needed to re-implement the rot13(String) encryption here because we must work on
- // 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);
- final Rot13Encryption rot13 = new Rot13Encryption();
-
- final int length = span.length();
- for (int index = 0; index < length; index++) {
- final char c = span.charAt(index);
- buffer.replace(index, index + 1, String.valueOf(rot13.getNextEncryptedCharacter(c)));
- }
- return buffer;
- }
-
- @NonNull
- 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) {
- 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] : '=';
- op++;
- }
-
- return new String(out);
- }
-
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.lang3.CharEncoding;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public final class CryptUtils {
+
+ private CryptUtils() {
+ // utility class
+ }
+
+ 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;
+ }
+ for (char c = 'a'; c <= 'z'; c++) {
+ BASE64MAP1[i++] = c;
+ }
+ for (char c = '0'; c <= '9'; c++) {
+ BASE64MAP1[i++] = c;
+ }
+ BASE64MAP1[i++] = '+';
+ BASE64MAP1[i++] = '/';
+
+ for (i = 0; i < BASE64MAP2.length; i++) {
+ BASE64MAP2[i] = -1;
+ }
+ for (i = 0; i < 64; i++) {
+ BASE64MAP2[BASE64MAP1[i]] = (byte) i;
+ }
+ }
+
+ private static class Rot13Encryption {
+ private boolean plaintext = false;
+
+ char getNextEncryptedCharacter(final char c) {
+ int result = c;
+ if (result == '[') {
+ plaintext = true;
+ } else if (result == ']') {
+ plaintext = false;
+ } else if (!plaintext) {
+ final int capitalized = result & 32;
+ result &= ~capitalized;
+ result = ((result >= 'A') && (result <= 'Z') ? ((result - 'A' + 13) % 26 + 'A') : result)
+ | capitalized;
+ }
+ return (char) result;
+ }
+ }
+
+ @NonNull
+ public static String rot13(final String text) {
+ if (text == null) {
+ return StringUtils.EMPTY;
+ }
+ final StringBuilder result = new StringBuilder();
+ final Rot13Encryption rot13 = new Rot13Encryption();
+
+ final int length = text.length();
+ for (int index = 0; index < length; index++) {
+ final char c = text.charAt(index);
+ result.append(rot13.getNextEncryptedCharacter(c));
+ }
+ return result.toString();
+ }
+
+ 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 | UnsupportedEncodingException e) {
+ Log.e("CryptUtils.md5", e);
+ }
+
+ return StringUtils.EMPTY;
+ }
+
+ 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);
+ return mac.doFinal(text.getBytes(CharEncoding.UTF_8));
+ } catch (GeneralSecurityException | UnsupportedEncodingException e) {
+ Log.e("CryptUtils.hashHmac", e);
+ return EMPTY;
+ }
+ }
+
+ public static CharSequence rot13(final Spannable span) {
+ // I needed to re-implement the rot13(String) encryption here because we must work on
+ // 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);
+ final Rot13Encryption rot13 = new Rot13Encryption();
+
+ final int length = span.length();
+ for (int index = 0; index < length; index++) {
+ final char c = span.charAt(index);
+ buffer.replace(index, index + 1, String.valueOf(rot13.getNextEncryptedCharacter(c)));
+ }
+ return buffer;
+ }
+
+ 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) {
+ 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] : '=';
+ op++;
+ }
+
+ return new String(out);
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/utils/EnvironmentUtils.java b/main/src/cgeo/geocaching/utils/EnvironmentUtils.java
index 680cc46..90b4c5d 100644
--- a/main/src/cgeo/geocaching/utils/EnvironmentUtils.java
+++ b/main/src/cgeo/geocaching/utils/EnvironmentUtils.java
@@ -1,30 +1,28 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.os.Environment;
-
-public class EnvironmentUtils {
- private EnvironmentUtils() {
- // utility class
- }
-
- /**
- * Same as {@link Environment#getExternalStorageState()} but more stable. We have seen null pointers here, probably
- * when there are issues in the underlying mount.
- */
- @NonNull
- public static String getExternalStorageState() {
- try {
- return Environment.getExternalStorageState();
- } catch (final NullPointerException e) {
- Log.w("Could not get external storage state", e);
- }
- return StringUtils.EMPTY;
- }
-
- public static boolean isExternalStorageAvailable() {
- return EnvironmentUtils.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
- }
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+import android.os.Environment;
+
+public class EnvironmentUtils {
+ private EnvironmentUtils() {
+ // utility class
+ }
+
+ /**
+ * Same as {@link Environment#getExternalStorageState()} but more stable. We have seen null pointers here, probably
+ * when there are issues in the underlying mount.
+ */
+ public static String getExternalStorageState() {
+ try {
+ return Environment.getExternalStorageState();
+ } catch (final NullPointerException e) {
+ Log.w("Could not get external storage state", e);
+ }
+ return StringUtils.EMPTY;
+ }
+
+ public static boolean isExternalStorageAvailable() {
+ return EnvironmentUtils.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/FileUtils.java b/main/src/cgeo/geocaching/utils/FileUtils.java
index d26b416..cae94e4 100644
--- a/main/src/cgeo/geocaching/utils/FileUtils.java
+++ b/main/src/cgeo/geocaching/utils/FileUtils.java
@@ -1,215 +1,212 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.CharEncoding;
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.os.Handler;
-import android.os.Message;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.List;
-
-/**
- * Utility class for files
- *
- */
-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(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) {
- return;
- }
-
- final File[] files = directory.listFiles();
-
- if (ArrayUtils.isNotEmpty(files)) {
- for (final File file : files) {
- if (chooser.shouldEnd()) {
- return;
- }
- if (!file.canRead()) {
- continue;
- }
- String name = file.getName();
- if (file.isFile()) {
- if (chooser.isSelected(file)) {
- result.add(file); // add file to list
- }
- } else if (file.isDirectory()) {
- if (name.charAt(0) == '.') {
- continue; // skip hidden directories
- }
- if (name.length() > 16) {
- name = name.substring(0, 14) + '…';
- }
- if (feedBackHandler != null) {
- feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name));
- }
-
- 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 interface FileSelector {
- boolean isSelected(File file);
- boolean shouldEnd();
- }
-
- /**
- * Create a unique non existing file named like the given file name. If a file with the given name already exists,
- * add a number as suffix to the file name.<br>
- * Example: For the file name "file.ext" this will return the first file of the list
- * <ul>
- * <li>file.ext</li>
- * <li>file_2.ext</li>
- * <li>file_3.ext</li>
- * </ul>
- * which does not yet exist.
- */
- @NonNull
- public static File getUniqueNamedFile(final File file) {
- if (!file.exists()) {
- return file;
- }
- final String baseNameAndPath = file.getPath();
- final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_";
- final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, ".");
- for (int i = 2; 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");
- }
-
- /**
- * This usage of this method indicates that the return value of File.delete() can safely be ignored.
- */
- public static void deleteIgnoringFailure(final File file) {
- final boolean success = file.delete() || !file.exists();
- if (!success) {
- Log.i("Could not delete " + file.getAbsolutePath());
- }
- }
-
- /**
- * Deletes a file and logs deletion failures.
- *
- * @return <code> true</code> if this file was deleted, <code>false</code> otherwise.
- */
- public static boolean delete(final File file) {
- final boolean success = file.delete() || !file.exists();
- if (!success) {
- Log.e("Could not delete " + file.getAbsolutePath());
- }
- return success;
- }
-
- /**
- * Creates the directory named by the given file, creating any missing parent directories in the process.
- *
- * @return <code>true</code> if the directory was created, <code>false</code> on failure or if the directory already
- * existed.
- */
- 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());
- }
- return success;
- }
-
- 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;
- try {
- final OutputStream os = new FileOutputStream(file);
- buffer = new BufferedOutputStream(os);
- fileWriter = new OutputStreamWriter(buffer, CharEncoding.UTF_16);
- fileWriter.write(content);
- } catch (final IOException e) {
- Log.e("FieldnoteExport.ExportTask export", e);
- return false;
- } finally {
- IOUtils.closeQuietly(fileWriter);
- IOUtils.closeQuietly(buffer);
- }
- return true;
- }
-
- /**
- * Check if the URL represents a file on the local file system.
- *
- * @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
- */
- public static boolean isFileUrl(final String url) {
- return StringUtils.startsWith(url, FILE_PROTOCOL);
- }
-
- /**
- * Build an URL from a file name.
- *
- * @param file a local file name
- * @return an URL with the <tt>file</tt> scheme
- */
- @NonNull
- public static String fileToUrl(final File file) {
- return FILE_PROTOCOL + file.getAbsolutePath();
- }
-
- /**
- * Local file name when {@link #isFileUrl(String)} is <tt>true</tt>.
- *
- * @return the local file
- */
- @NonNull
- public static File urlToFile(final String url) {
- return new File(StringUtils.substring(url, FILE_PROTOCOL.length()));
- }
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.CharEncoding;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+
+import android.os.Handler;
+import android.os.Message;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * Utility class for files
+ *
+ */
+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(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) {
+ return;
+ }
+
+ final File[] files = directory.listFiles();
+
+ if (ArrayUtils.isNotEmpty(files)) {
+ for (final File file : files) {
+ if (chooser.shouldEnd()) {
+ return;
+ }
+ if (!file.canRead()) {
+ continue;
+ }
+ String name = file.getName();
+ if (file.isFile()) {
+ if (chooser.isSelected(file)) {
+ result.add(file); // add file to list
+ }
+ } else if (file.isDirectory()) {
+ if (name.charAt(0) == '.') {
+ continue; // skip hidden directories
+ }
+ if (name.length() > 16) {
+ name = name.substring(0, 14) + '…';
+ }
+ if (feedBackHandler != null) {
+ feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name));
+ }
+
+ 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 interface FileSelector {
+ boolean isSelected(File file);
+ boolean shouldEnd();
+ }
+
+ /**
+ * Create a unique non existing file named like the given file name. If a file with the given name already exists,
+ * add a number as suffix to the file name.<br>
+ * Example: For the file name "file.ext" this will return the first file of the list
+ * <ul>
+ * <li>file.ext</li>
+ * <li>file_2.ext</li>
+ * <li>file_3.ext</li>
+ * </ul>
+ * which does not yet exist.
+ */
+ public static File getUniqueNamedFile(final File file) {
+ if (!file.exists()) {
+ return file;
+ }
+ final String baseNameAndPath = file.getPath();
+ final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_";
+ final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, ".");
+ for (int i = 2; 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");
+ }
+
+ /**
+ * This usage of this method indicates that the return value of File.delete() can safely be ignored.
+ */
+ public static void deleteIgnoringFailure(final File file) {
+ final boolean success = file.delete() || !file.exists();
+ if (!success) {
+ Log.i("Could not delete " + file.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Deletes a file and logs deletion failures.
+ *
+ * @return <code> true</code> if this file was deleted, <code>false</code> otherwise.
+ */
+ public static boolean delete(final File file) {
+ final boolean success = file.delete() || !file.exists();
+ if (!success) {
+ Log.e("Could not delete " + file.getAbsolutePath());
+ }
+ return success;
+ }
+
+ /**
+ * Creates the directory named by the given file, creating any missing parent directories in the process.
+ *
+ * @return <code>true</code> if the directory was created, <code>false</code> on failure or if the directory already
+ * existed.
+ */
+ 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());
+ }
+ return success;
+ }
+
+ 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;
+ try {
+ final OutputStream os = new FileOutputStream(file);
+ buffer = new BufferedOutputStream(os);
+ fileWriter = new OutputStreamWriter(buffer, CharEncoding.UTF_16);
+ fileWriter.write(content);
+ } catch (final IOException e) {
+ Log.e("FieldnoteExport.ExportTask export", e);
+ return false;
+ } finally {
+ IOUtils.closeQuietly(fileWriter);
+ IOUtils.closeQuietly(buffer);
+ }
+ return true;
+ }
+
+ /**
+ * Check if the URL represents a file on the local file system.
+ *
+ * @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
+ */
+ public static boolean isFileUrl(final String url) {
+ return StringUtils.startsWith(url, FILE_PROTOCOL);
+ }
+
+ /**
+ * Build an URL from a file name.
+ *
+ * @param file a local file name
+ * @return an URL with the <tt>file</tt> scheme
+ */
+ public static String fileToUrl(final File file) {
+ return FILE_PROTOCOL + file.getAbsolutePath();
+ }
+
+ /**
+ * Local file name when {@link #isFileUrl(String)} is <tt>true</tt>.
+ *
+ * @return the local file
+ */
+ public static File urlToFile(final String url) {
+ return new File(StringUtils.substring(url, FILE_PROTOCOL.length()));
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/Formatter.java b/main/src/cgeo/geocaching/utils/Formatter.java
index afca300..ed9a72c 100644
--- a/main/src/cgeo/geocaching/utils/Formatter.java
+++ b/main/src/cgeo/geocaching/utils/Formatter.java
@@ -1,254 +1,238 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.CgeoApplication;
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.R;
-import cgeo.geocaching.Waypoint;
-import cgeo.geocaching.enumerations.CacheSize;
-import cgeo.geocaching.enumerations.WaypointType;
-
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-
-import android.content.Context;
-import android.text.format.DateUtils;
-
-import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
-public final class Formatter {
-
- /** Text separator used for formatting texts */
- public static final String SEPARATOR = " · ";
-
- private static final Context context = CgeoApplication.getInstance().getBaseContext();
-
- private Formatter() {
- // Utility class
- }
-
- /**
- * Generate a time string according to system-wide settings (locale, 12/24 hour)
- * such as "13:24".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- public static String formatTime(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME);
- }
-
- /**
- * Generate a date string according to system-wide settings (locale, date format)
- * such as "20 December" or "20 December 2010". The year will only be included when necessary.
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- public static String formatDate(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE);
- }
-
- /**
- * Generate a date string according to system-wide settings (locale, date format)
- * such as "20 December 2010". The year will always be included, making it suitable
- * to generate long-lived log entries.
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- public static String formatFullDate(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE
- | DateUtils.FORMAT_SHOW_YEAR);
- }
-
- /**
- * Generate a numeric date string according to system-wide settings (locale, date format)
- * such as "10/20/2010".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- 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".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- 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);
- case 1:
- return CgeoApplication.getInstance().getString(R.string.log_yesterday);
- default:
- return formatShortDate(date);
- }
- }
-
- /**
- * Generate a numeric date and time string according to system-wide settings (locale,
- * date format) such as "7 sept. at 12:35".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- public static String formatShortDateTime(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL);
- }
-
- /**
- * Generate a numeric date and time string according to system-wide settings (locale,
- * date format) such as "7 september at 12:35".
- *
- * @param date
- * milliseconds since the epoch
- * @return the formatted string
- */
- @NonNull
- public static String formatDateTime(final long date) {
- return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
- }
-
- @NonNull
- public static String formatCacheInfoLong(final Geocache cache) {
- final List<String> infos = new ArrayList<>();
- if (StringUtils.isNotBlank(cache.getGeocode())) {
- infos.add(cache.getGeocode());
- }
-
- addShortInfos(cache, infos);
-
- if (cache.isPremiumMembersOnly()) {
- infos.add(CgeoApplication.getInstance().getString(R.string.cache_premium));
- }
- return StringUtils.join(infos, SEPARATOR);
- }
-
- @NonNull
- public static String formatCacheInfoShort(final Geocache cache) {
- final List<String> infos = new ArrayList<>();
- addShortInfos(cache, infos);
- return StringUtils.join(infos, SEPARATOR);
- }
-
- private static void addShortInfos(final Geocache cache, final List<String> infos) {
- if (cache.hasDifficulty()) {
- infos.add("D " + formatDT(cache.getDifficulty()));
- }
- if (cache.hasTerrain()) {
- infos.add("T " + formatDT(cache.getTerrain()));
- }
-
- // don't show "not chosen" for events and virtuals, that should be the normal case
- if (cache.getSize() != CacheSize.UNKNOWN && cache.showSize()) {
- infos.add(cache.getSize().getL10n());
- } else if (cache.isEventCache()) {
- final Date hiddenDate = cache.getHiddenDate();
- if (hiddenDate != null) {
- infos.add(formatShortDateIncludingWeekday(hiddenDate.getTime()));
- }
- }
- }
-
- private static String formatDT(final float value) {
- return String.format(Locale.getDefault(), "%.1f", value);
- }
-
- @NonNull
- public static String formatCacheInfoHistory(final Geocache cache) {
- final List<String> infos = new ArrayList<>(3);
- infos.add(StringUtils.upperCase(cache.getGeocode()));
- infos.add(formatDate(cache.getVisitedDate()));
- infos.add(formatTime(cache.getVisitedDate()));
- return StringUtils.join(infos, SEPARATOR);
- }
-
- @NonNull
- public static String formatWaypointInfo(final Waypoint waypoint) {
- final List<String> infos = new ArrayList<>(3);
- final WaypointType waypointType = waypoint.getWaypointType();
- if (waypointType != WaypointType.OWN && waypointType != null) {
- infos.add(waypointType.getL10n());
- }
- if (Waypoint.PREFIX_OWN.equalsIgnoreCase(waypoint.getPrefix())) {
- infos.add(CgeoApplication.getInstance().getString(R.string.waypoint_custom));
- } else {
- if (StringUtils.isNotBlank(waypoint.getPrefix())) {
- infos.add(waypoint.getPrefix());
- }
- if (StringUtils.isNotBlank(waypoint.getLookup())) {
- infos.add(waypoint.getLookup());
- }
- }
- return StringUtils.join(infos, SEPARATOR);
- }
-
- @NonNull
- 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
- */
- @Nullable
- 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 = formatFullDate(time);
- if (cache.isEventCache()) {
- dateString = DateUtils.formatDateTime(CgeoApplication.getInstance().getBaseContext(), time, DateUtils.FORMAT_SHOW_WEEKDAY) + ", " + dateString;
- }
- return dateString;
- }
-
- @NonNull
- public static String formatMapSubtitle(final Geocache cache) {
- return "D " + formatDT(cache.getDifficulty()) + SEPARATOR + "T " + formatDT(cache.getTerrain()) + SEPARATOR + cache.getGeocode();
- }
-
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.R;
+import cgeo.geocaching.Waypoint;
+import cgeo.geocaching.enumerations.CacheSize;
+import cgeo.geocaching.enumerations.WaypointType;
+
+import org.apache.commons.lang3.StringUtils;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public final class Formatter {
+
+ /** Text separator used for formatting texts */
+ public static final String SEPARATOR = " · ";
+
+ private static final Context context = CgeoApplication.getInstance().getBaseContext();
+
+ private Formatter() {
+ // Utility class
+ }
+
+ /**
+ * Generate a time string according to system-wide settings (locale, 12/24 hour)
+ * such as "13:24".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ public static String formatTime(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_TIME);
+ }
+
+ /**
+ * Generate a date string according to system-wide settings (locale, date format)
+ * such as "20 December" or "20 December 2010". The year will only be included when necessary.
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ public static String formatDate(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE);
+ }
+
+ /**
+ * Generate a date string according to system-wide settings (locale, date format)
+ * such as "20 December 2010". The year will always be included, making it suitable
+ * to generate long-lived log entries.
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ public static String formatFullDate(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_YEAR);
+ }
+
+ /**
+ * Generate a numeric date string according to system-wide settings (locale, date format)
+ * such as "10/20/2010".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ 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".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ 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);
+ case 1:
+ return CgeoApplication.getInstance().getString(R.string.log_yesterday);
+ default:
+ return formatShortDate(date);
+ }
+ }
+
+ /**
+ * Generate a numeric date and time string according to system-wide settings (locale,
+ * date format) such as "7 sept. at 12:35".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ public static String formatShortDateTime(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL);
+ }
+
+ /**
+ * Generate a numeric date and time string according to system-wide settings (locale,
+ * date format) such as "7 september at 12:35".
+ *
+ * @param date
+ * milliseconds since the epoch
+ * @return the formatted string
+ */
+ public static String formatDateTime(final long date) {
+ return DateUtils.formatDateTime(context, date, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
+ }
+
+ public static String formatCacheInfoLong(final Geocache cache) {
+ final List<String> infos = new ArrayList<>();
+ if (StringUtils.isNotBlank(cache.getGeocode())) {
+ infos.add(cache.getGeocode());
+ }
+
+ addShortInfos(cache, infos);
+
+ if (cache.isPremiumMembersOnly()) {
+ infos.add(CgeoApplication.getInstance().getString(R.string.cache_premium));
+ }
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ public static String formatCacheInfoShort(final Geocache cache) {
+ final List<String> infos = new ArrayList<>();
+ addShortInfos(cache, infos);
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ private static void addShortInfos(final Geocache cache, final List<String> infos) {
+ if (cache.hasDifficulty()) {
+ infos.add("D " + formatDT(cache.getDifficulty()));
+ }
+ if (cache.hasTerrain()) {
+ infos.add("T " + formatDT(cache.getTerrain()));
+ }
+
+ // don't show "not chosen" for events and virtuals, that should be the normal case
+ if (cache.getSize() != CacheSize.UNKNOWN && cache.showSize()) {
+ infos.add(cache.getSize().getL10n());
+ } else if (cache.isEventCache()) {
+ final Date hiddenDate = cache.getHiddenDate();
+ if (hiddenDate != null) {
+ infos.add(formatShortDateIncludingWeekday(hiddenDate.getTime()));
+ }
+ }
+ }
+
+ private static String formatDT(final float value) {
+ return String.format(Locale.getDefault(), "%.1f", value);
+ }
+
+ public static String formatCacheInfoHistory(final Geocache cache) {
+ final List<String> infos = new ArrayList<>(3);
+ infos.add(StringUtils.upperCase(cache.getGeocode()));
+ infos.add(formatDate(cache.getVisitedDate()));
+ infos.add(formatTime(cache.getVisitedDate()));
+ return StringUtils.join(infos, SEPARATOR);
+ }
+
+ public static String formatWaypointInfo(final Waypoint waypoint) {
+ final List<String> infos = new ArrayList<>(3);
+ final WaypointType waypointType = waypoint.getWaypointType();
+ if (waypointType != WaypointType.OWN && waypointType != null) {
+ infos.add(waypointType.getL10n());
+ }
+ if (Waypoint.PREFIX_OWN.equalsIgnoreCase(waypoint.getPrefix())) {
+ infos.add(CgeoApplication.getInstance().getString(R.string.waypoint_custom));
+ } else {
+ if (StringUtils.isNotBlank(waypoint.getPrefix())) {
+ infos.add(waypoint.getPrefix());
+ }
+ if (StringUtils.isNotBlank(waypoint.getLookup())) {
+ infos.add(waypoint.getLookup());
+ }
+ }
+ return StringUtils.join(infos, 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 = 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 0748e84..8202cd4 100644
--- a/main/src/cgeo/geocaching/utils/HtmlUtils.java
+++ b/main/src/cgeo/geocaching/utils/HtmlUtils.java
@@ -1,76 +1,73 @@
-package cgeo.geocaching.utils;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.text.Html;
-import android.text.Spanned;
-import android.text.style.ImageSpan;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-public final class HtmlUtils {
-
- private HtmlUtils() {
- // utility class
- }
-
- /**
- * 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.
- *
- */
- @NonNull
- public static String extractText(final CharSequence html) {
- if (StringUtils.isBlank(html)) {
- return StringUtils.EMPTY;
- }
- String result = html.toString();
-
- // recognize images in textview HTML contents
- if (html instanceof Spanned) {
- final Spanned text = (Spanned) html;
- final Object[] styles = text.getSpans(0, text.length(), Object.class);
- final List<Pair<Integer, Integer>> removals = new ArrayList<>();
- for (final Object style : styles) {
- if (style instanceof ImageSpan) {
- final int start = text.getSpanStart(style);
- final int end = text.getSpanEnd(style);
- removals.add(Pair.of(start, end));
- }
- }
-
- // sort reversed and delete image spans
- Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() {
-
- @Override
- public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) {
- return rhs.getRight().compareTo(lhs.getRight());
- }
- });
- result = text.toString();
- for (final Pair<Integer, Integer> removal : removals) {
- result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight());
- }
- }
-
- // now that images are gone, do a normal html to text conversion
- return Html.fromHtml(result).toString().trim();
- }
-
- @NonNull
- 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;
- }
-}
+package cgeo.geocaching.utils;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.ImageSpan;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public final class HtmlUtils {
+
+ private HtmlUtils() {
+ // utility class
+ }
+
+ /**
+ * 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.
+ *
+ */
+ public static String extractText(final CharSequence html) {
+ if (StringUtils.isBlank(html)) {
+ return StringUtils.EMPTY;
+ }
+ String result = html.toString();
+
+ // recognize images in textview HTML contents
+ if (html instanceof Spanned) {
+ final Spanned text = (Spanned) html;
+ final Object[] styles = text.getSpans(0, text.length(), Object.class);
+ final List<Pair<Integer, Integer>> removals = new ArrayList<>();
+ for (final Object style : styles) {
+ if (style instanceof ImageSpan) {
+ final int start = text.getSpanStart(style);
+ final int end = text.getSpanEnd(style);
+ removals.add(Pair.of(start, end));
+ }
+ }
+
+ // sort reversed and delete image spans
+ Collections.sort(removals, new Comparator<Pair<Integer, Integer>>() {
+
+ @Override
+ public int compare(final Pair<Integer, Integer> lhs, final Pair<Integer, Integer> rhs) {
+ return rhs.getRight().compareTo(lhs.getRight());
+ }
+ });
+ result = text.toString();
+ for (final Pair<Integer, Integer> removal : removals) {
+ result = result.substring(0, removal.getLeft()) + result.substring(removal.getRight());
+ }
+ }
+
+ // 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 000966a..269791a 100644
--- a/main/src/cgeo/geocaching/utils/ImageUtils.java
+++ b/main/src/cgeo/geocaching/utils/ImageUtils.java
@@ -1,490 +1,486 @@
-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 android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-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;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.ref.WeakReference;
-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.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.LinkedBlockingQueue;
-
-import rx.Observable;
-import rx.Scheduler.Worker;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.functions.Action0;
-import rx.functions.Action1;
-
-public final class ImageUtils {
- private static final int[] ORIENTATIONS = {
- ExifInterface.ORIENTATION_ROTATE_90,
- ExifInterface.ORIENTATION_ROTATE_180,
- ExifInterface.ORIENTATION_ROTATE_270
- };
-
- private static final int[] ROTATION = { 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 = { "geocheck.org" };
-
- private ImageUtils() {
- // Do not let this class be instantiated, this is a utility class.
- }
-
- /**
- * Scales a bitmap to the device display size.
- *
- * @param image
- * The image Bitmap representation to scale
- * @return BitmapDrawable The scaled image
- */
- @NonNull
- public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) {
- final Point displaySize = Compatibility.getDisplaySize();
- final int maxWidth = displaySize.x - 25;
- final int maxHeight = displaySize.y - 25;
- return scaleBitmapTo(image, maxWidth, maxHeight);
- }
-
- /**
- * Reads and scales an image file to the device display size.
- *
- * @param filename
- * The image file to read and scale
- * @return Bitmap The scaled image or Null if source image can't be read
- */
- @Nullable
- public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) {
- 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);
- final Bitmap image = readDownsampledImage(filename, maxWidth, maxHeight);
- if (image == null) {
- return null;
- }
- final BitmapDrawable scaledImage = scaleBitmapTo(image, maxWidth, maxHeight);
- return scaledImage.getBitmap();
- }
-
- /**
- * Scales a bitmap to the given bounds if it is larger, otherwise returns the original bitmap.
- *
- * @param image
- * The bitmap to scale
- * @return BitmapDrawable The scaled image
- */
- @NonNull
- private static BitmapDrawable scaleBitmapTo(@NonNull final Bitmap image, final int maxWidth, final int maxHeight) {
- final CgeoApplication app = CgeoApplication.getInstance();
- Bitmap result = image;
- int width = image.getWidth();
- int height = image.getHeight();
-
- if (width > maxWidth || height > maxHeight) {
- final double ratio = Math.min((double) maxHeight / (double) height, (double) maxWidth / (double) width);
- width = (int) Math.ceil(width * ratio);
- height = (int) Math.ceil(height * ratio);
- result = Bitmap.createScaledBitmap(image, width, height, true);
- }
-
- final BitmapDrawable resultDrawable = new BitmapDrawable(app.getResources(), result);
- resultDrawable.setBounds(new Rect(0, 0, width, height));
- return resultDrawable;
- }
-
- /**
- * Store a bitmap to file.
- *
- * @param bitmap
- * The bitmap to store
- * @param format
- * The image format
- * @param quality
- * The image quality
- * @param pathOfOutputImage
- * Path to store to
- */
- public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) {
- try {
- final FileOutputStream out = new FileOutputStream(pathOfOutputImage);
- final BufferedOutputStream bos = new BufferedOutputStream(out);
- bitmap.compress(format, quality, bos);
- bos.flush();
- bos.close();
- } catch (final IOException e) {
- Log.e("ImageHelper.storeBitmap", e);
- }
- }
-
- /**
- * Scales an image to the desired bounds and encodes to file.
- *
- * @param filePath
- * Image to read
- * @param maxXY
- * bounds
- * @return filename and path, <tt>null</tt> if something fails
- */
- @Nullable
- public static String readScaleAndWriteImage(@NonNull final String filePath, final int maxXY) {
- if (maxXY <= 0) {
- return filePath;
- }
- final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY);
- if (image == null) {
- return null;
- }
- final BitmapDrawable scaledImage = scaleBitmapTo(image, maxXY, maxXY);
- final File tempImageFile = getOutputImageFile();
- if (tempImageFile == null) {
- Log.e("ImageUtils.readScaleAndWriteImage: unable to write scaled image");
- return null;
- }
- final String uploadFilename = tempImageFile.getPath();
- storeBitmap(scaledImage.getBitmap(), Bitmap.CompressFormat.JPEG, 75, uploadFilename);
- return uploadFilename;
- }
-
- /**
- * Reads and scales an image file with downsampling in one step to prevent memory consumption.
- *
- * @param filePath
- * The file to read
- * @param maxX
- * The desired width
- * @param maxY
- * The desired height
- * @return Bitmap the image or null if file can't be read
- */
- @Nullable
- public static Bitmap readDownsampledImage(@NonNull final String filePath, final int maxX, final int maxY) {
- int orientation = ExifInterface.ORIENTATION_NORMAL;
- try {
- final ExifInterface exif = new ExifInterface(filePath);
- orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
- } catch (final IOException e) {
- Log.e("ImageUtils.readDownsampledImage", e);
- }
- final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options();
- sizeOnlyOptions.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(filePath, sizeOnlyOptions);
- final int myMaxXY = Math.max(sizeOnlyOptions.outHeight, sizeOnlyOptions.outWidth);
- final int maxXY = Math.max(maxX, maxY);
- final int sampleSize = myMaxXY / maxXY;
- final BitmapFactory.Options sampleOptions = new BitmapFactory.Options();
- if (sampleSize > 1) {
- sampleOptions.inSampleSize = sampleSize;
- }
- final Bitmap decodedImage = BitmapFactory.decodeFile(filePath, sampleOptions);
- if (decodedImage != null) {
- for (int i = 0; i < ORIENTATIONS.length; i++) {
- if (orientation == ORIENTATIONS[i]) {
- final Matrix matrix = new Matrix();
- matrix.postRotate(ROTATION[i]);
- return Bitmap.createBitmap(decodedImage, 0, 0, decodedImage.getWidth(), decodedImage.getHeight(), matrix, true);
- }
- }
- }
- return decodedImage;
- }
-
- /** Create a File for saving an image or video
- *
- * @return the temporary image file to use, or <tt>null</tt> if the media directory could
- * not be created.
- * */
- @Nullable
- public static File getOutputImageFile() {
- // To be safe, you should check that the SDCard is mounted
- // using Environment.getExternalStorageState() before doing this.
- final File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "cgeo");
-
- // This location works best if you want the created images to be shared
- // between applications and persist after your app has been uninstalled.
-
- // Create the storage directory if it does not exist
- if (!mediaStorageDir.exists()) {
- if (!FileUtils.mkdirs(mediaStorageDir)) {
- Log.e("ImageUtils.getOutputImageFile: cannot create media storage directory");
- return null;
- }
- }
-
- // Create a media file name
- final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
- return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
- }
-
- @Nullable
- public static Uri getOutputImageFileUri() {
- final File file = getOutputImageFile();
- if (file == null) {
- return null;
- }
- return Uri.fromFile(file);
- }
-
- /**
- * Check if the URL contains one of the given substrings.
- *
- * @param url the URL to check
- * @param patterns a list of substrings to check against
- * @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 (final String entry : patterns) {
- if (StringUtils.containsIgnoreCase(url, entry)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Decode a base64-encoded string and save the result into a file.
- *
- * @param inString the encoded string
- * @param outFile the file to save the decoded result into
- */
- public static void decodeBase64ToFile(final String inString, final File outFile) {
- FileOutputStream out = null;
- try {
- out = new FileOutputStream(outFile);
- decodeBase64ToStream(inString, out);
- } catch (final IOException e) {
- Log.e("HtmlImage.decodeBase64ToFile: cannot write file for decoded inline image", e);
- } finally {
- IOUtils.closeQuietly(out);
- }
- }
-
- /**
- * Decode a base64-encoded string and save the result into a stream.
- *
- * @param inString
- * the encoded string
- * @param out
- * the stream to save the decoded result into
- */
- public static void decodeBase64ToStream(final String inString, final OutputStream out) throws IOException {
- Base64InputStream in = null;
- try {
- in = new Base64InputStream(new ByteArrayInputStream(inString.getBytes(TextUtils.CHARSET_ASCII)), Base64.DEFAULT);
- IOUtils.copy(in, out);
- } finally {
- IOUtils.closeQuietly(in);
- }
- }
-
- @NonNull
- public static BitmapDrawable getTransparent1x1Drawable(final Resources res) {
- return new BitmapDrawable(res, BitmapFactory.decodeResource(res, R.drawable.image_no_placement));
- }
-
- /**
- * 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.Builder()
- .setUrl(source)
- .setTitle(StringUtils.defaultString(geocode))
- .build());
- 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.
- */
- 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 protected WeakReference<TextView> viewRef;
-
- @SuppressWarnings("deprecation")
- public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
- viewRef = new WeakReference<>(view);
- drawable = null;
- setBounds(0, 0, 0, 0);
- drawableObservable.subscribe(this);
- }
-
- @Override
- public final void draw(final Canvas canvas) {
- if (drawable != null) {
- drawable.draw(canvas);
- }
- }
-
- @Override
- 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 or <tt>null</tt> if the view is not alive anymore
- */
- protected TextView updateDrawable(final Drawable newDrawable) {
- setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
- drawable = newDrawable;
- return viewRef.get();
- }
-
- 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 List<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) {
- final TextView view = redrawable.left.updateDrawable(redrawable.right);
- if (view != null) {
- VIEWS.add(view);
- }
- }
- 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);
- }
-
- @Override
- protected TextView updateDrawable(final Drawable newDrawable) {
- final TextView view = super.updateDrawable(newDrawable);
- if (view != null) {
- setBounds(scaleImageToLineHeight(newDrawable, view));
- }
- return view;
- }
- }
-
- public static boolean canBeOpenedExternally(final String source) {
- return !containsPattern(source, NO_EXTERNAL);
- }
-
- @NonNull
- 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);
- }
-
- @Nullable
- 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;
- }
-}
+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;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+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;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+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.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public final class ImageUtils {
+ private static final int[] ORIENTATIONS = {
+ ExifInterface.ORIENTATION_ROTATE_90,
+ ExifInterface.ORIENTATION_ROTATE_180,
+ ExifInterface.ORIENTATION_ROTATE_270
+ };
+
+ private static final int[] ROTATION = { 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 = { "geocheck.org" };
+
+ private ImageUtils() {
+ // Do not let this class be instantiated, this is a utility class.
+ }
+
+ /**
+ * Scales a bitmap to the device display size.
+ *
+ * @param image
+ * The image Bitmap representation to scale
+ * @return BitmapDrawable The scaled image
+ */
+ public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) {
+ final Point displaySize = Compatibility.getDisplaySize();
+ final int maxWidth = displaySize.x - 25;
+ final int maxHeight = displaySize.y - 25;
+ return scaleBitmapTo(image, maxWidth, maxHeight);
+ }
+
+ /**
+ * Reads and scales an image file to the device display size.
+ *
+ * @param filename
+ * The image file to read and scale
+ * @return Bitmap The scaled image or Null if source image can't be read
+ */
+ @Nullable
+ public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) {
+ 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);
+ final Bitmap image = readDownsampledImage(filename, maxWidth, maxHeight);
+ if (image == null) {
+ return null;
+ }
+ final BitmapDrawable scaledImage = scaleBitmapTo(image, maxWidth, maxHeight);
+ return scaledImage.getBitmap();
+ }
+
+ /**
+ * Scales a bitmap to the given bounds if it is larger, otherwise returns the original bitmap.
+ *
+ * @param image
+ * The bitmap to scale
+ * @return BitmapDrawable The scaled image
+ */
+ @NonNull
+ private static BitmapDrawable scaleBitmapTo(@NonNull final Bitmap image, final int maxWidth, final int maxHeight) {
+ final CgeoApplication app = CgeoApplication.getInstance();
+ Bitmap result = image;
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ if (width > maxWidth || height > maxHeight) {
+ final double ratio = Math.min((double) maxHeight / (double) height, (double) maxWidth / (double) width);
+ width = (int) Math.ceil(width * ratio);
+ height = (int) Math.ceil(height * ratio);
+ result = Bitmap.createScaledBitmap(image, width, height, true);
+ }
+
+ final BitmapDrawable resultDrawable = new BitmapDrawable(app.getResources(), result);
+ resultDrawable.setBounds(new Rect(0, 0, width, height));
+ return resultDrawable;
+ }
+
+ /**
+ * Store a bitmap to file.
+ *
+ * @param bitmap
+ * The bitmap to store
+ * @param format
+ * The image format
+ * @param quality
+ * The image quality
+ * @param pathOfOutputImage
+ * Path to store to
+ */
+ public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) {
+ try {
+ final FileOutputStream out = new FileOutputStream(pathOfOutputImage);
+ final BufferedOutputStream bos = new BufferedOutputStream(out);
+ bitmap.compress(format, quality, bos);
+ bos.flush();
+ bos.close();
+ } catch (final IOException e) {
+ Log.e("ImageHelper.storeBitmap", e);
+ }
+ }
+
+ /**
+ * Scales an image to the desired bounds and encodes to file.
+ *
+ * @param filePath
+ * Image to read
+ * @param maxXY
+ * bounds
+ * @return filename and path, <tt>null</tt> if something fails
+ */
+ @Nullable
+ public static String readScaleAndWriteImage(@NonNull final String filePath, final int maxXY) {
+ if (maxXY <= 0) {
+ return filePath;
+ }
+ final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY);
+ if (image == null) {
+ return null;
+ }
+ final BitmapDrawable scaledImage = scaleBitmapTo(image, maxXY, maxXY);
+ final File tempImageFile = getOutputImageFile();
+ if (tempImageFile == null) {
+ Log.e("ImageUtils.readScaleAndWriteImage: unable to write scaled image");
+ return null;
+ }
+ final String uploadFilename = tempImageFile.getPath();
+ storeBitmap(scaledImage.getBitmap(), Bitmap.CompressFormat.JPEG, 75, uploadFilename);
+ return uploadFilename;
+ }
+
+ /**
+ * Reads and scales an image file with downsampling in one step to prevent memory consumption.
+ *
+ * @param filePath
+ * The file to read
+ * @param maxX
+ * The desired width
+ * @param maxY
+ * The desired height
+ * @return Bitmap the image or null if file can't be read
+ */
+ @Nullable
+ public static Bitmap readDownsampledImage(@NonNull final String filePath, final int maxX, final int maxY) {
+ int orientation = ExifInterface.ORIENTATION_NORMAL;
+ try {
+ final ExifInterface exif = new ExifInterface(filePath);
+ orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ } catch (final IOException e) {
+ Log.e("ImageUtils.readDownsampledImage", e);
+ }
+ final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options();
+ sizeOnlyOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(filePath, sizeOnlyOptions);
+ final int myMaxXY = Math.max(sizeOnlyOptions.outHeight, sizeOnlyOptions.outWidth);
+ final int maxXY = Math.max(maxX, maxY);
+ final int sampleSize = myMaxXY / maxXY;
+ final BitmapFactory.Options sampleOptions = new BitmapFactory.Options();
+ if (sampleSize > 1) {
+ sampleOptions.inSampleSize = sampleSize;
+ }
+ final Bitmap decodedImage = BitmapFactory.decodeFile(filePath, sampleOptions);
+ if (decodedImage != null) {
+ for (int i = 0; i < ORIENTATIONS.length; i++) {
+ if (orientation == ORIENTATIONS[i]) {
+ final Matrix matrix = new Matrix();
+ matrix.postRotate(ROTATION[i]);
+ return Bitmap.createBitmap(decodedImage, 0, 0, decodedImage.getWidth(), decodedImage.getHeight(), matrix, true);
+ }
+ }
+ }
+ return decodedImage;
+ }
+
+ /** Create a File for saving an image or video
+ *
+ * @return the temporary image file to use, or <tt>null</tt> if the media directory could
+ * not be created.
+ * */
+ @Nullable
+ public static File getOutputImageFile() {
+ // To be safe, you should check that the SDCard is mounted
+ // using Environment.getExternalStorageState() before doing this.
+ final File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "cgeo");
+
+ // This location works best if you want the created images to be shared
+ // between applications and persist after your app has been uninstalled.
+
+ // Create the storage directory if it does not exist
+ if (!mediaStorageDir.exists()) {
+ if (!FileUtils.mkdirs(mediaStorageDir)) {
+ Log.e("ImageUtils.getOutputImageFile: cannot create media storage directory");
+ return null;
+ }
+ }
+
+ // Create a media file name
+ final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
+ return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
+ }
+
+ @Nullable
+ public static Uri getOutputImageFileUri() {
+ final File file = getOutputImageFile();
+ if (file == null) {
+ return null;
+ }
+ return Uri.fromFile(file);
+ }
+
+ /**
+ * Check if the URL contains one of the given substrings.
+ *
+ * @param url the URL to check
+ * @param patterns a list of substrings to check against
+ * @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 (final String entry : patterns) {
+ if (StringUtils.containsIgnoreCase(url, entry)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Decode a base64-encoded string and save the result into a file.
+ *
+ * @param inString the encoded string
+ * @param outFile the file to save the decoded result into
+ */
+ public static void decodeBase64ToFile(final String inString, final File outFile) {
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(outFile);
+ decodeBase64ToStream(inString, out);
+ } catch (final IOException e) {
+ Log.e("HtmlImage.decodeBase64ToFile: cannot write file for decoded inline image", e);
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ /**
+ * Decode a base64-encoded string and save the result into a stream.
+ *
+ * @param inString
+ * the encoded string
+ * @param out
+ * the stream to save the decoded result into
+ */
+ public static void decodeBase64ToStream(final String inString, final OutputStream out) throws IOException {
+ Base64InputStream in = null;
+ try {
+ in = new Base64InputStream(new ByteArrayInputStream(inString.getBytes(TextUtils.CHARSET_ASCII)), Base64.DEFAULT);
+ IOUtils.copy(in, out);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ public static BitmapDrawable getTransparent1x1Drawable(final Resources res) {
+ return new BitmapDrawable(res, BitmapFactory.decodeResource(res, R.drawable.image_no_placement));
+ }
+
+ /**
+ * 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.Builder()
+ .setUrl(source)
+ .setTitle(StringUtils.defaultString(geocode))
+ .build());
+ 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.
+ */
+ 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 protected WeakReference<TextView> viewRef;
+
+ @SuppressWarnings("deprecation")
+ public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) {
+ viewRef = new WeakReference<>(view);
+ drawable = null;
+ setBounds(0, 0, 0, 0);
+ drawableObservable.subscribe(this);
+ }
+
+ @Override
+ public final void draw(final Canvas canvas) {
+ if (drawable != null) {
+ drawable.draw(canvas);
+ }
+ }
+
+ @Override
+ 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 or <tt>null</tt> if the view is not alive anymore
+ */
+ protected TextView updateDrawable(final Drawable newDrawable) {
+ setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight());
+ drawable = newDrawable;
+ return viewRef.get();
+ }
+
+ 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 List<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) {
+ final TextView view = redrawable.left.updateDrawable(redrawable.right);
+ if (view != null) {
+ VIEWS.add(view);
+ }
+ }
+ 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);
+ }
+
+ @Override
+ protected TextView updateDrawable(final Drawable newDrawable) {
+ final TextView view = super.updateDrawable(newDrawable);
+ if (view != null) {
+ setBounds(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/LogTemplateProvider.java b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
index b54f5d4..97e2187 100644
--- a/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
+++ b/main/src/cgeo/geocaching/utils/LogTemplateProvider.java
@@ -1,277 +1,274 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.LogEntry;
-import cgeo.geocaching.R;
-import cgeo.geocaching.Trackable;
-import cgeo.geocaching.connector.ConnectorFactory;
-import cgeo.geocaching.connector.IConnector;
-import cgeo.geocaching.connector.capability.ILogin;
-import cgeo.geocaching.settings.Settings;
-
-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.List;
-
-/**
- * Provides all the available templates for logging.
- *
- */
-public final class LogTemplateProvider {
-
- private LogTemplateProvider() {
- // utility class
- }
-
- /**
- * Context aware data container for log templates.
- * <p>
- * Some log templates need additional information. To provide that information, it can be encapsulated in this log
- * context.
- * </p>
- *
- */
- public static class LogContext {
- private Geocache cache;
- private Trackable trackable;
- private boolean offline = false;
- private final LogEntry logEntry;
-
- public LogContext(final Geocache cache, final LogEntry logEntry) {
- this(cache, logEntry, false);
- }
-
- public LogContext(final Trackable trackable, final LogEntry logEntry) {
- this.trackable = trackable;
- this.logEntry = logEntry;
- }
-
- public LogContext(final Geocache cache, final LogEntry logEntry, final boolean offline) {
- this.cache = cache;
- this.offline = offline;
- this.logEntry = logEntry;
- }
-
- public final Geocache getCache() {
- return cache;
- }
-
- public final Trackable getTrackable() {
- return trackable;
- }
-
- public final boolean isOffline() {
- return offline;
- }
-
- public final LogEntry getLogEntry() {
- return logEntry;
- }
- }
-
- public abstract static class LogTemplate {
- private final String template;
- private final int resourceId;
-
- protected LogTemplate(final String template, final int resourceId) {
- this.template = template;
- this.resourceId = resourceId;
- }
-
- public abstract String getValue(LogContext context);
-
- public final int getResourceId() {
- return resourceId;
- }
-
- public final int getItemId() {
- return template.hashCode();
- }
-
- public final String getTemplateString() {
- return template;
- }
-
- @NonNull
- private final String apply(@NonNull final String input, final LogContext context) {
- final String bracketedTemplate = "[" + template + "]";
-
- // check containment first to not unconditionally call the getValue(...) method
- if (input.contains(bracketedTemplate)) {
- return StringUtils.replace(input, bracketedTemplate, getValue(context));
- }
- return input;
- }
- }
-
- /**
- * @return all templates, but not the signature template itself
- */
- @NonNull
- public static List<LogTemplate> getTemplatesWithoutSignature() {
- final List<LogTemplate> templates = new ArrayList<>();
- templates.add(new LogTemplate("DATE", R.string.init_signature_template_date) {
-
- @Override
- public String getValue(final LogContext context) {
- return Formatter.formatFullDate(System.currentTimeMillis());
- }
- });
- templates.add(new LogTemplate("TIME", R.string.init_signature_template_time) {
-
- @Override
- public String getValue(final LogContext context) {
- return Formatter.formatTime(System.currentTimeMillis());
- }
- });
- templates.add(new LogTemplate("DATETIME", R.string.init_signature_template_datetime) {
-
- @Override
- public String getValue(final LogContext context) {
- final long currentTime = System.currentTimeMillis();
- return Formatter.formatFullDate(currentTime) + " " + Formatter.formatTime(currentTime);
- }
- });
- templates.add(new LogTemplate("USER", R.string.init_signature_template_user) {
-
- @Override
- public String getValue(final LogContext context) {
- final Geocache cache = context.getCache();
- if (cache != null) {
- final IConnector connector = ConnectorFactory.getConnector(cache);
- if (connector instanceof ILogin) {
- return ((ILogin) connector).getUserName();
- }
- }
- return Settings.getUsername();
- }
- });
- templates.add(new LogTemplate("NUMBER", R.string.init_signature_template_number) {
-
- @Override
- public String getValue(final LogContext context) {
- final Geocache cache = context.getCache();
- if (cache == null) {
- return StringUtils.EMPTY;
- }
-
- int current = 0;
- final IConnector connector = ConnectorFactory.getConnector(cache);
- if (connector instanceof ILogin) {
- current = ((ILogin) connector).getCachesFound();
- }
-
- // try updating the login information, if the counter is zero
- if (current == 0) {
- if (context.isOffline()) {
- return StringUtils.EMPTY;
- }
- if (connector instanceof ILogin) {
- ((ILogin) connector).login(null, null);
- current = ((ILogin) connector).getCachesFound();
- }
- }
-
- if (current >= 0) {
- return String.valueOf(current + 1);
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("OWNER", R.string.init_signature_template_owner) {
-
- @Override
- public String getValue(final LogContext context) {
- final Trackable trackable = context.getTrackable();
- if (trackable != null) {
- return trackable.getOwner();
- }
- final Geocache cache = context.getCache();
- if (cache != null) {
- return cache.getOwnerDisplayName();
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("NAME", R.string.init_signature_template_name) {
- @Override
- public String getValue(final LogContext context) {
- final Trackable trackable = context.getTrackable();
- if (trackable != null) {
- return trackable.getName();
- }
- final Geocache cache = context.getCache();
- if (cache != null) {
- return cache.getName();
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("URL", R.string.init_signature_template_url) {
-
- @Override
- public String getValue(final LogContext context) {
- final Trackable trackable = context.getTrackable();
- if (trackable != null) {
- return trackable.getUrl();
- }
- final Geocache cache = context.getCache();
- if (cache != null) {
- return StringUtils.defaultString(cache.getUrl());
- }
- return StringUtils.EMPTY;
- }
- });
- templates.add(new LogTemplate("LOG", R.string.init_signature_template_log) {
- @Override
- public String getValue(final LogContext context) {
- final LogEntry logEntry = context.getLogEntry();
- if (logEntry != null) {
- return logEntry.getDisplayText();
- }
- return StringUtils.EMPTY;
- }
- });
- return templates;
- }
-
- /**
- * @return all templates, including the signature template
- */
- @NonNull
- public static List<LogTemplate> getTemplatesWithSignature() {
- final List<LogTemplate> templates = getTemplatesWithoutSignature();
- templates.add(new LogTemplate("SIGNATURE", R.string.init_signature) {
- @Override
- public String getValue(final LogContext context) {
- final String nestedTemplate = Settings.getSignature();
- if (StringUtils.contains(nestedTemplate, "SIGNATURE")) {
- return "invalid signature template";
- }
- return applyTemplates(nestedTemplate, context);
- }
- });
- return templates;
- }
-
- @Nullable
- public static LogTemplate getTemplate(final int itemId) {
- for (final LogTemplate template : getTemplatesWithSignature()) {
- if (template.getItemId() == itemId) {
- return template;
- }
- }
- return null;
- }
-
- public static String applyTemplates(@NonNull final String signature, final LogContext context) {
- String result = signature;
- for (final LogTemplate template : getTemplatesWithSignature()) {
- result = template.apply(result, context);
- }
- return result;
- }
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.LogEntry;
+import cgeo.geocaching.R;
+import cgeo.geocaching.Trackable;
+import cgeo.geocaching.connector.ConnectorFactory;
+import cgeo.geocaching.connector.IConnector;
+import cgeo.geocaching.connector.capability.ILogin;
+import cgeo.geocaching.settings.Settings;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides all the available templates for logging.
+ *
+ */
+public final class LogTemplateProvider {
+
+ private LogTemplateProvider() {
+ // utility class
+ }
+
+ /**
+ * Context aware data container for log templates.
+ * <p>
+ * Some log templates need additional information. To provide that information, it can be encapsulated in this log
+ * context.
+ * </p>
+ *
+ */
+ public static class LogContext {
+ private Geocache cache;
+ private Trackable trackable;
+ private boolean offline = false;
+ private final LogEntry logEntry;
+
+ public LogContext(final Geocache cache, final LogEntry logEntry) {
+ this(cache, logEntry, false);
+ }
+
+ public LogContext(final Trackable trackable, final LogEntry logEntry) {
+ this.trackable = trackable;
+ this.logEntry = logEntry;
+ }
+
+ public LogContext(final Geocache cache, final LogEntry logEntry, final boolean offline) {
+ this.cache = cache;
+ this.offline = offline;
+ this.logEntry = logEntry;
+ }
+
+ public final Geocache getCache() {
+ return cache;
+ }
+
+ public final Trackable getTrackable() {
+ return trackable;
+ }
+
+ public final boolean isOffline() {
+ return offline;
+ }
+
+ public final LogEntry getLogEntry() {
+ return logEntry;
+ }
+ }
+
+ public abstract static class LogTemplate {
+ private final String template;
+ private final int resourceId;
+
+ protected LogTemplate(final String template, final int resourceId) {
+ this.template = template;
+ this.resourceId = resourceId;
+ }
+
+ public abstract String getValue(LogContext context);
+
+ public final int getResourceId() {
+ return resourceId;
+ }
+
+ public final int getItemId() {
+ return template.hashCode();
+ }
+
+ public final String getTemplateString() {
+ return template;
+ }
+
+ private final String apply(final String input, final LogContext context) {
+ final String bracketedTemplate = "[" + template + "]";
+
+ // check containment first to not unconditionally call the getValue(...) method
+ if (input.contains(bracketedTemplate)) {
+ return StringUtils.replace(input, bracketedTemplate, getValue(context));
+ }
+ return input;
+ }
+ }
+
+ /**
+ * @return all templates, but not the signature template itself
+ */
+ public static List<LogTemplate> getTemplatesWithoutSignature() {
+ final List<LogTemplate> templates = new ArrayList<>();
+ templates.add(new LogTemplate("DATE", R.string.init_signature_template_date) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ return Formatter.formatFullDate(System.currentTimeMillis());
+ }
+ });
+ templates.add(new LogTemplate("TIME", R.string.init_signature_template_time) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ return Formatter.formatTime(System.currentTimeMillis());
+ }
+ });
+ templates.add(new LogTemplate("DATETIME", R.string.init_signature_template_datetime) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final long currentTime = System.currentTimeMillis();
+ return Formatter.formatFullDate(currentTime) + " " + Formatter.formatTime(currentTime);
+ }
+ });
+ templates.add(new LogTemplate("USER", R.string.init_signature_template_user) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ final IConnector connector = ConnectorFactory.getConnector(cache);
+ if (connector instanceof ILogin) {
+ return ((ILogin) connector).getUserName();
+ }
+ }
+ return Settings.getUsername();
+ }
+ });
+ templates.add(new LogTemplate("NUMBER", R.string.init_signature_template_number) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Geocache cache = context.getCache();
+ if (cache == null) {
+ return StringUtils.EMPTY;
+ }
+
+ int current = 0;
+ final IConnector connector = ConnectorFactory.getConnector(cache);
+ if (connector instanceof ILogin) {
+ current = ((ILogin) connector).getCachesFound();
+ }
+
+ // try updating the login information, if the counter is zero
+ if (current == 0) {
+ if (context.isOffline()) {
+ return StringUtils.EMPTY;
+ }
+ if (connector instanceof ILogin) {
+ ((ILogin) connector).login(null, null);
+ current = ((ILogin) connector).getCachesFound();
+ }
+ }
+
+ if (current >= 0) {
+ return String.valueOf(current + 1);
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("OWNER", R.string.init_signature_template_owner) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Trackable trackable = context.getTrackable();
+ if (trackable != null) {
+ return trackable.getOwner();
+ }
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ return cache.getOwnerDisplayName();
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("NAME", R.string.init_signature_template_name) {
+ @Override
+ public String getValue(final LogContext context) {
+ final Trackable trackable = context.getTrackable();
+ if (trackable != null) {
+ return trackable.getName();
+ }
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ return cache.getName();
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("URL", R.string.init_signature_template_url) {
+
+ @Override
+ public String getValue(final LogContext context) {
+ final Trackable trackable = context.getTrackable();
+ if (trackable != null) {
+ return trackable.getUrl();
+ }
+ final Geocache cache = context.getCache();
+ if (cache != null) {
+ return StringUtils.defaultString(cache.getUrl());
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ templates.add(new LogTemplate("LOG", R.string.init_signature_template_log) {
+ @Override
+ public String getValue(final LogContext context) {
+ final LogEntry logEntry = context.getLogEntry();
+ if (logEntry != null) {
+ return logEntry.getDisplayText();
+ }
+ return StringUtils.EMPTY;
+ }
+ });
+ return templates;
+ }
+
+ /**
+ * @return all templates, including the signature template
+ */
+ public static List<LogTemplate> getTemplatesWithSignature() {
+ final List<LogTemplate> templates = getTemplatesWithoutSignature();
+ templates.add(new LogTemplate("SIGNATURE", R.string.init_signature) {
+ @Override
+ public String getValue(final LogContext context) {
+ final String nestedTemplate = StringUtils.defaultString(Settings.getSignature());
+ if (StringUtils.contains(nestedTemplate, "SIGNATURE")) {
+ return "invalid signature template";
+ }
+ return applyTemplates(nestedTemplate, context);
+ }
+ });
+ return templates;
+ }
+
+ public static LogTemplate getTemplate(final int itemId) {
+ for (final LogTemplate template : getTemplatesWithSignature()) {
+ if (template.getItemId() == itemId) {
+ return template;
+ }
+ }
+ return null;
+ }
+
+ public static String applyTemplates(final String signature, final LogContext context) {
+ if (signature == null) {
+ return StringUtils.EMPTY;
+ }
+ String result = signature;
+ for (final LogTemplate template : getTemplatesWithSignature()) {
+ result = template.apply(result, context);
+ }
+ return result;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/MapUtils.java b/main/src/cgeo/geocaching/utils/MapUtils.java
index 9f15bf3..11a705d 100644
--- a/main/src/cgeo/geocaching/utils/MapUtils.java
+++ b/main/src/cgeo/geocaching/utils/MapUtils.java
@@ -1,293 +1,287 @@
-package cgeo.geocaching.utils;
-
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.R;
-import cgeo.geocaching.Waypoint;
-import cgeo.geocaching.compatibility.Compatibility;
-import cgeo.geocaching.enumerations.CacheListType;
-import cgeo.geocaching.enumerations.LogType;
-
-import org.apache.commons.lang3.builder.HashCodeBuilder;
-import org.eclipse.jdt.annotation.NonNull;
-
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.support.annotation.Nullable;
-import android.util.SparseArray;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-public final class MapUtils {
-
- // data for overlays
- private static final int[][] INSET_RELIABLE = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }; // center, 33x40 / 45x51 / 60x68 / 90x102 / 120x136
- private static final int[][] INSET_TYPE = { { 5, 8, 6, 10 }, { 4, 4, 4, 11 }, { 6, 6, 6, 14 }, { 9, 9, 9, 21 }, { 12, 12, 12, 28 } }; // center, 22x22 / 36x36
- private static final int[][] INSET_OWN = { { 21, 0, 0, 28 }, { 29, 0, 0, 35 }, { 40, 0, 0, 48 }, { 58, 0, 0, 70 }, { 80, 0, 0, 96 } }; // top right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
- private static final int[][] INSET_FOUND = { { 0, 0, 21, 28 }, { 0, 0, 29, 35 }, { 0, 0, 40, 48 }, { 0, 0, 58, 70 }, { 0, 0, 80, 96 } }; // top left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
- private static final int[][] INSET_USERMODIFIEDCOORDS = { { 21, 28, 0, 0 }, { 29, 35, 0, 0 }, { 40, 48, 0, 0 }, { 58, 70, 0, 0 }, { 80, 96, 0, 0 } }; // bottom right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
- private static final int[][] INSET_PERSONALNOTE = { { 0, 28, 21, 0 }, { 0, 35, 29, 0 }, { 0, 48, 40, 0 }, { 0, 70, 58, 0 }, { 0, 96, 80, 0 } }; // bottom left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
-
- private static final SparseArray<LayerDrawable> overlaysCache = new SparseArray<>();
-
- private MapUtils() {
- // Do not instantiate
- }
-
- /**
- * Obtain the drawable for a given cache, with background circle.
- *
- * @param res
- * the resources to use
- * @param cache
- * the cache to build the drawable for
- * @return
- * a drawable representing the current cache status
- */
- @NonNull
- public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache) {
- return getCacheMarker(res, cache, null);
- }
-
- /**
- * Obtain the drawable for a given cache.
- * Return a drawable from the cache, if a similar drawable was already generated.
- *
- * cacheListType should be Null if the requesting activity is Map.
- *
- * @param res
- * the resources to use
- * @param cache
- * the cache to build the drawable for
- * @param cacheListType
- * the current CacheListType or Null
- * @return
- * a drawable representing the current cache status
- */
- @NonNull
- public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
- final int hashcode = new HashCodeBuilder()
- .append(cache.isReliableLatLon())
- .append(cache.getType().id)
- .append(cache.isDisabled() || cache.isArchived())
- .append(cache.getMapMarkerId())
- .append(cache.isOwner())
- .append(cache.isFound())
- .append(cache.hasUserModifiedCoords())
- .append(cache.getPersonalNote())
- .append(cache.isLogOffline())
- .append(cache.getListId() > 0)
- .append(cache.getOfflineLogType())
- .append(showBackground(cacheListType))
- .append(showFloppyOverlay(cacheListType))
- .toHashCode();
-
- synchronized (overlaysCache) {
- LayerDrawable drawable = overlaysCache.get(hashcode);
- if (drawable == null) {
- drawable = createCacheMarker(res, cache, cacheListType);
- overlaysCache.put(hashcode, drawable);
- }
- return drawable;
- }
- }
-
- /**
- * Obtain the drawable for a given waypoint.
- * Return a drawable from the cache, if a similar drawable was already generated.
- *
- * @param res
- * the resources to use
- * @param waypoint
- * the waypoint to build the drawable for
- * @return
- * a drawable representing the current waypoint status
- */
- @NonNull
- public static LayerDrawable getWaypointMarker(final Resources res, final Waypoint waypoint) {
- final int hashcode = new HashCodeBuilder()
- .append(waypoint.isVisited())
- .append(waypoint.getWaypointType().id)
- .toHashCode();
-
- synchronized (overlaysCache) {
- LayerDrawable drawable = overlaysCache.get(hashcode);
- if (drawable == null) {
- drawable = createWaypointMarker(res, waypoint);
- overlaysCache.put(hashcode, drawable);
- }
- return drawable;
- }
- }
-
- /**
- * Build the drawable for a given waypoint.
- *
- * @param res
- * the resources to use
- * @param waypoint
- * the waypoint to build the drawable for
- * @return
- * a drawable representing the current waypoint status
- */
- @NonNull
- private static LayerDrawable createWaypointMarker(final Resources res, final Waypoint waypoint) {
- final Drawable marker = Compatibility.getDrawable(res, !waypoint.isVisited() ? R.drawable.marker : R.drawable.marker_transparent);
- final Drawable[] layers = {
- marker,
- Compatibility.getDrawable(res, waypoint.getWaypointType().markerId)
- };
- final LayerDrawable drawable = new LayerDrawable(layers);
- final int resolution = calculateResolution(marker);
- drawable.setLayerInset(1, INSET_TYPE[resolution][0], INSET_TYPE[resolution][1], INSET_TYPE[resolution][2], INSET_TYPE[resolution][3]);
- return drawable;
- }
-
- /**
- * Clear the cache of drawable items.
- */
- public static void clearCachedItems() {
- synchronized (overlaysCache) {
- overlaysCache.clear();
- }
- }
-
- /**
- * Build the drawable for a given cache.
- *
- * @param res
- * the resources to use
- * @param cache
- * the cache to build the drawable for
- * @param cacheListType
- * the current CacheListType or Null
- * @return
- * a drawable representing the current cache status
- */
- @NonNull
- private static LayerDrawable createCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
- // Set initial capacities to the maximum of layers and insets to avoid dynamic reallocation
- final List<Drawable> layers = new ArrayList<>(9);
- final List<int[]> insets = new ArrayList<>(8);
-
- // background: disabled or not
- final Drawable marker = Compatibility.getDrawable(res, cache.getMapMarkerId());
- // Show the background circle only on map
- if (showBackground(cacheListType)) {
- layers.add(marker);
- }
- final int resolution = calculateResolution(marker);
- // reliable or not
- if (!cache.isReliableLatLon()) {
- insets.add(INSET_RELIABLE[resolution]);
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_notreliable));
- }
- // cache type
- layers.add(Compatibility.getDrawable(res, cache.getType().markerId));
- insets.add(INSET_TYPE[resolution]);
- // own
- if (cache.isOwner()) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_own));
- insets.add(INSET_OWN[resolution]);
- // if not, checked if stored
- } else if (cache.getListId() > 0 && showFloppyOverlay(cacheListType)) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_stored));
- insets.add(INSET_OWN[resolution]);
- }
- // found
- if (cache.isFound()) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_found));
- insets.add(INSET_FOUND[resolution]);
- // if not, perhaps logged offline
- } else if (cache.isLogOffline()) {
- final LogType offlineLogType = cache.getOfflineLogType();
- if (offlineLogType == null) {
- // Default, backward compatible
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_found_offline));
- } else {
- layers.add(Compatibility.getDrawable(res, offlineLogType.getOfflineLogOverlay()));
- }
- insets.add(INSET_FOUND[resolution]);
- }
- // user modified coords
- if (cache.hasUserModifiedCoords()) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_usermodifiedcoords));
- insets.add(driftBottomItems(INSET_USERMODIFIEDCOORDS, resolution, cacheListType));
- }
- // personal note
- if (cache.getPersonalNote() != null) {
- layers.add(Compatibility.getDrawable(res, R.drawable.marker_personalnote));
- insets.add(driftBottomItems(INSET_PERSONALNOTE, resolution, cacheListType));
- }
-
- final LayerDrawable ld = new LayerDrawable(layers.toArray(new Drawable[layers.size()]));
-
- int index = showBackground(cacheListType) ? 1 : 0;
- for (final int[] inset : insets) {
- ld.setLayerInset(index++, inset[0], inset[1], inset[2], inset[3]);
- }
-
- return ld;
- }
-
- /**
- * Get the resolution index used for positioning the overlays elements.
- *
- * @param marker
- * The Drawable reference
- * @return
- * an index for the overlays positions
- */
- private static int calculateResolution(final Drawable marker) {
- return marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0;
- }
-
- /**
- * Calculate a new position for the bottom line overlay items, when there is no background circle.
- *
- * @param inset
- * Original inset position
- * @param resolution
- * The current item resolution
- * @param cacheListType
- * The current CacheListType
- * @return
- * The new drifted inset position
- */
- private static int[] driftBottomItems(final int[][] inset, final int resolution, @Nullable final CacheListType cacheListType) {
- // Do not drift in when background is displayed
- if (showBackground(cacheListType)) {
- return inset[resolution];
- }
- final int[] newPosition = Arrays.copyOf(inset[resolution], 4);
- newPosition[1] -= INSET_TYPE[resolution][3];
- newPosition[3] += INSET_TYPE[resolution][3];
- return newPosition;
- }
-
- /**
- * Conditional expression to choose if we need the background circle or not.
- *
- * @param cacheListType
- * The cache list currently used
- * @return
- * True if the background circle should be displayed
- */
- private static boolean showBackground(final CacheListType cacheListType) {
- return cacheListType == null;
- }
-
- /**
- * Conditional expression to choose if we need the floppy overlay or not.
- *
- * @param cacheListType
- * The cache list currently used
- * @return
- * True if the floppy overlay should be displayed
- */
- private static boolean showFloppyOverlay(final CacheListType cacheListType) {
- return cacheListType == null || cacheListType != CacheListType.OFFLINE;
- }
-}
+package cgeo.geocaching.utils;
+
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.R;
+import cgeo.geocaching.Waypoint;
+import cgeo.geocaching.compatibility.Compatibility;
+import cgeo.geocaching.enumerations.CacheListType;
+import cgeo.geocaching.enumerations.LogType;
+
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.support.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class MapUtils {
+
+ // data for overlays
+ private static final int[][] INSET_RELIABLE = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }; // center, 33x40 / 45x51 / 60x68 / 90x102 / 120x136
+ private static final int[][] INSET_TYPE = { { 5, 8, 6, 10 }, { 4, 4, 4, 11 }, { 6, 6, 6, 14 }, { 9, 9, 9, 21 }, { 12, 12, 12, 28 } }; // center, 22x22 / 36x36
+ private static final int[][] INSET_OWN = { { 21, 0, 0, 28 }, { 29, 0, 0, 35 }, { 40, 0, 0, 48 }, { 58, 0, 0, 70 }, { 80, 0, 0, 96 } }; // top right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+ private static final int[][] INSET_FOUND = { { 0, 0, 21, 28 }, { 0, 0, 29, 35 }, { 0, 0, 40, 48 }, { 0, 0, 58, 70 }, { 0, 0, 80, 96 } }; // top left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+ private static final int[][] INSET_USERMODIFIEDCOORDS = { { 21, 28, 0, 0 }, { 29, 35, 0, 0 }, { 40, 48, 0, 0 }, { 58, 70, 0, 0 }, { 80, 96, 0, 0 } }; // bottom right, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+ private static final int[][] INSET_PERSONALNOTE = { { 0, 28, 21, 0 }, { 0, 35, 29, 0 }, { 0, 48, 40, 0 }, { 0, 70, 58, 0 }, { 0, 96, 80, 0 } }; // bottom left, 12x12 / 16x16 / 20x20 / 32x32 / 40x40
+
+ private static final SparseArray<LayerDrawable> overlaysCache = new SparseArray<>();
+
+ private MapUtils() {
+ // Do not instantiate
+ }
+
+ /**
+ * Obtain the drawable for a given cache, with background circle.
+ *
+ * @param res
+ * the resources to use
+ * @param cache
+ * the cache to build the drawable for
+ * @return
+ * a drawable representing the current cache status
+ */
+ public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache) {
+ return getCacheMarker(res, cache, null);
+ }
+
+ /**
+ * Obtain the drawable for a given cache.
+ * Return a drawable from the cache, if a similar drawable was already generated.
+ *
+ * cacheListType should be Null if the requesting activity is Map.
+ *
+ * @param res
+ * the resources to use
+ * @param cache
+ * the cache to build the drawable for
+ * @param cacheListType
+ * the current CacheListType or Null
+ * @return
+ * a drawable representing the current cache status
+ */
+ public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
+ final int hashcode = new HashCodeBuilder()
+ .append(cache.isReliableLatLon())
+ .append(cache.getType().id)
+ .append(cache.isDisabled() || cache.isArchived())
+ .append(cache.getMapMarkerId())
+ .append(cache.isOwner())
+ .append(cache.isFound())
+ .append(cache.hasUserModifiedCoords())
+ .append(cache.getPersonalNote())
+ .append(cache.isLogOffline())
+ .append(cache.getListId() > 0)
+ .append(cache.getOfflineLogType())
+ .append(showBackground(cacheListType))
+ .append(showFloppyOverlay(cacheListType))
+ .toHashCode();
+
+ synchronized (overlaysCache) {
+ LayerDrawable drawable = overlaysCache.get(hashcode);
+ if (drawable == null) {
+ drawable = createCacheMarker(res, cache, cacheListType);
+ overlaysCache.put(hashcode, drawable);
+ }
+ return drawable;
+ }
+ }
+
+ /**
+ * Obtain the drawable for a given waypoint.
+ * Return a drawable from the cache, if a similar drawable was already generated.
+ *
+ * @param res
+ * the resources to use
+ * @param waypoint
+ * the waypoint to build the drawable for
+ * @return
+ * a drawable representing the current waypoint status
+ */
+ public static LayerDrawable getWaypointMarker(final Resources res, final Waypoint waypoint) {
+ final int hashcode = new HashCodeBuilder()
+ .append(waypoint.isVisited())
+ .append(waypoint.getWaypointType().id)
+ .toHashCode();
+
+ synchronized (overlaysCache) {
+ LayerDrawable drawable = overlaysCache.get(hashcode);
+ if (drawable == null) {
+ drawable = createWaypointMarker(res, waypoint);
+ overlaysCache.put(hashcode, drawable);
+ }
+ return drawable;
+ }
+ }
+
+ /**
+ * Build the drawable for a given waypoint.
+ *
+ * @param res
+ * the resources to use
+ * @param waypoint
+ * the waypoint to build the drawable for
+ * @return
+ * a drawable representing the current waypoint status
+ */
+ private static LayerDrawable createWaypointMarker(final Resources res, final Waypoint waypoint) {
+ final Drawable marker = Compatibility.getDrawable(res, !waypoint.isVisited() ? R.drawable.marker : R.drawable.marker_transparent);
+ final Drawable[] layers = {
+ marker,
+ Compatibility.getDrawable(res, waypoint.getWaypointType().markerId)
+ };
+ final LayerDrawable drawable = new LayerDrawable(layers);
+ final int resolution = calculateResolution(marker);
+ drawable.setLayerInset(1, INSET_TYPE[resolution][0], INSET_TYPE[resolution][1], INSET_TYPE[resolution][2], INSET_TYPE[resolution][3]);
+ return drawable;
+ }
+
+ /**
+ * Clear the cache of drawable items.
+ */
+ public static void clearCachedItems() {
+ synchronized (overlaysCache) {
+ overlaysCache.clear();
+ }
+ }
+
+ /**
+ * Build the drawable for a given cache.
+ *
+ * @param res
+ * the resources to use
+ * @param cache
+ * the cache to build the drawable for
+ * @param cacheListType
+ * the current CacheListType or Null
+ * @return
+ * a drawable representing the current cache status
+ */
+ private static LayerDrawable createCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) {
+ // Set initial capacities to the maximum of layers and insets to avoid dynamic reallocation
+ final List<Drawable> layers = new ArrayList<>(9);
+ final List<int[]> insets = new ArrayList<>(8);
+
+ // background: disabled or not
+ final Drawable marker = Compatibility.getDrawable(res, cache.getMapMarkerId());
+ // Show the background circle only on map
+ if (showBackground(cacheListType)) {
+ layers.add(marker);
+ }
+ final int resolution = calculateResolution(marker);
+ // reliable or not
+ if (!cache.isReliableLatLon()) {
+ insets.add(INSET_RELIABLE[resolution]);
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_notreliable));
+ }
+ // cache type
+ layers.add(Compatibility.getDrawable(res, cache.getType().markerId));
+ insets.add(INSET_TYPE[resolution]);
+ // own
+ if (cache.isOwner()) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_own));
+ insets.add(INSET_OWN[resolution]);
+ // if not, checked if stored
+ } else if (cache.getListId() > 0 && showFloppyOverlay(cacheListType)) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_stored));
+ insets.add(INSET_OWN[resolution]);
+ }
+ // found
+ if (cache.isFound()) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_found));
+ insets.add(INSET_FOUND[resolution]);
+ // if not, perhaps logged offline
+ } else if (cache.isLogOffline()) {
+ final LogType offlineLogType = cache.getOfflineLogType();
+ if (offlineLogType == null) {
+ // Default, backward compatible
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_found_offline));
+ } else {
+ layers.add(Compatibility.getDrawable(res, offlineLogType.getOfflineLogOverlay()));
+ }
+ insets.add(INSET_FOUND[resolution]);
+ }
+ // user modified coords
+ if (cache.hasUserModifiedCoords()) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_usermodifiedcoords));
+ insets.add(driftBottomItems(INSET_USERMODIFIEDCOORDS, resolution, cacheListType));
+ }
+ // personal note
+ if (cache.getPersonalNote() != null) {
+ layers.add(Compatibility.getDrawable(res, R.drawable.marker_personalnote));
+ insets.add(driftBottomItems(INSET_PERSONALNOTE, resolution, cacheListType));
+ }
+
+ final LayerDrawable ld = new LayerDrawable(layers.toArray(new Drawable[layers.size()]));
+
+ int index = showBackground(cacheListType) ? 1 : 0;
+ for (final int[] inset : insets) {
+ ld.setLayerInset(index++, inset[0], inset[1], inset[2], inset[3]);
+ }
+
+ return ld;
+ }
+
+ /**
+ * Get the resolution index used for positionning the overlays elements.
+ *
+ * @param marker
+ * The Drawable reference
+ * @return
+ * an index for the overlays positions
+ */
+ private static int calculateResolution(final Drawable marker) {
+ return marker.getIntrinsicWidth() > 40 ? (marker.getIntrinsicWidth() > 50 ? (marker.getIntrinsicWidth() > 70 ? (marker.getIntrinsicWidth() > 100 ? 4 : 3) : 2) : 1) : 0;
+ }
+
+ /**
+ * Calculate a new position for the bottom line overlay items, when there is no background circle.
+ *
+ * @param inset
+ * Original inset position
+ * @param resolution
+ * The current item resolution
+ * @param cacheListType
+ * The current CacheListType
+ * @return
+ * The new drifted inset position
+ */
+ private static int[] driftBottomItems(final int[][] inset, final int resolution, @Nullable final CacheListType cacheListType) {
+ // Do not drift in when background is displayed
+ if (showBackground(cacheListType)) {
+ return inset[resolution];
+ }
+ final int[] newPosition = Arrays.copyOf(inset[resolution], 4);
+ newPosition[1] -= INSET_TYPE[resolution][3];
+ newPosition[3] += INSET_TYPE[resolution][3];
+ return newPosition;
+ }
+
+ /**
+ * Conditionnal expression to choose if we need the background circle or not.
+ *
+ * @param cacheListType
+ * The cache list currently used
+ * @return
+ * True if the background circle should be displayed
+ */
+ private static boolean showBackground(final CacheListType cacheListType) {
+ return cacheListType == null;
+ }
+
+ /**
+ * Conditionnal expression to choose if we need the floppy overlay or not.
+ *
+ * @param cacheListType
+ * The cache list currently used
+ * @return
+ * True if the floppy overlay should be displayed
+ */
+ private static boolean showFloppyOverlay(final CacheListType cacheListType) {
+ return cacheListType == null || cacheListType != CacheListType.OFFLINE;
+ }
+}
diff --git a/main/src/cgeo/geocaching/utils/MatcherWrapper.java b/main/src/cgeo/geocaching/utils/MatcherWrapper.java
index b3a361b..733a18e 100644
--- a/main/src/cgeo/geocaching/utils/MatcherWrapper.java
+++ b/main/src/cgeo/geocaching/utils/MatcherWrapper.java
@@ -1,13 +1,12 @@
package cgeo.geocaching.utils;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
/**
* Wrapper around the regex {@link Matcher} class. This implementation optimizes the memory usage of the matched
* Strings.
@@ -47,7 +46,6 @@ public class MatcherWrapper {
* Do not change this method, even if Findbugs and other tools will report a violation for that line!
*
*/
- @Nullable
@SuppressFBWarnings("DM_STRING_CTOR")
private static String newString(final String input) {
if (input == null) {
diff --git a/tests/src/cgeo/geocaching/ImageTest.java b/tests/src/cgeo/geocaching/ImageTest.java
index 493525f..2befc83 100644
--- a/tests/src/cgeo/geocaching/ImageTest.java
+++ b/tests/src/cgeo/geocaching/ImageTest.java
@@ -1,191 +1,190 @@
-package cgeo.geocaching;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import cgeo.CGeoTestCase;
-import cgeo.geocaching.utils.FileUtils;
-
-import android.net.Uri;
-import android.os.Parcel;
-
-import java.io.File;
-
-/**
- * ImageTest unit tests
- */
-public class ImageTest extends CGeoTestCase {
- private static final String URL1 = "https://nowhe.re";
- private static final String URL2 = "https://nowhe.re/image.png";
-
- private static final String FILE1 = "file:///dev/null";
- private static final String FILE2 = "file:///tmp/image.png";
-
- public static void testNullConstructor() throws Exception {
- final Image nullImage1 = new Image.Builder().build();
-
- assertThat(nullImage1).isEqualTo(Image.NONE);
- }
-
- public static void testStringConstructor() throws Exception {
- final Image image1 = new Image.Builder().setUrl("").build();
- final Image image2 = new Image.Builder().setUrl(FILE1).build();
- final Image image3 = new Image.Builder().setUrl(URL1).build();
-
- assertThat(image1).isEqualTo(Image.NONE);
- assertThat(image2).isNotEqualTo(Image.NONE);
- assertThat(image3).isNotEqualTo(Image.NONE);
- }
-
- public static void testUriConstructor() throws Exception {
- final Image image1 = new Image.Builder().setUrl(Uri.parse(URL1)).build();
- final Image image2 = new Image.Builder().setUrl(Uri.parse(URL2)).build();
-
- assertThat(image1).isNotEqualTo(Image.NONE);
- assertThat(image1.getUrl()).isEqualTo(URL1);
- assertThat(image2.getUrl()).isEqualTo(URL2);
- }
-
- public static void testFileConstructor() throws Exception {
- final Image image1 = new Image.Builder().setUrl(FileUtils.urlToFile(FILE1)).build();
- final Image image2 = new Image.Builder().setUrl(FileUtils.urlToFile(FILE2)).build();
-
- assertThat(image1).isNotEqualTo(Image.NONE);
- assertThat(image2).isNotEqualTo(Image.NONE);
- assertThat(image1.getUrl()).isEqualTo(FILE1);
- assertThat(image2.getUrl()).isEqualTo(FILE2);
- assertThat(image2.getUrl()).isEqualTo(FILE2);
- }
-
- public static void testTitle() throws Exception {
- final Image image1 = new Image.Builder().setTitle("Title").build();
-
- assertThat(image1).isNotEqualTo(Image.NONE);
- assertThat(image1.getTitle()).isEqualTo("Title");
- assertThat(image1.getDescription()).isNull();
- }
-
- public static void testDescription() throws Exception {
- final Image image1 = new Image.Builder().setDescription("Description").build();
-
- assertThat(image1).isNotEqualTo(Image.NONE);
- assertThat(image1.getTitle()).isNull();
- assertThat(image1.getDescription()).isEqualTo("Description");
- }
-
- public static void testIsEmpty() throws Exception {
- final Image image1 = new Image.Builder().build();
- final Image image2 = new Image.Builder().setUrl("").build();
- final Image image3 = new Image.Builder().setUrl(FILE1).build();
- final Image image4 = new Image.Builder().setUrl(URL1).build();
-
- assertThat(image1.isEmpty()).isTrue();
- assertThat(image2.isEmpty()).isTrue();
- assertThat(image3.isEmpty()).isFalse();
- assertThat(image4.isEmpty()).isFalse();
- }
-
- public static void testIsLocalFile() throws Exception {
- final Image image1 = new Image.Builder().build();
- final Image image2 = new Image.Builder().setUrl("").build();
- final Image image3 = new Image.Builder().setUrl(FILE1).build();
- final Image image4 = new Image.Builder().setUrl(URL1).build();
-
- assertThat(image1.isLocalFile()).isFalse();
- assertThat(image2.isLocalFile()).isFalse();
- assertThat(image3.isLocalFile()).isTrue();
- assertThat(image4.isLocalFile()).isFalse();
- }
-
- public static void testLocalFile() throws Exception {
- final Image image1 = new Image.Builder().setUrl(FILE1).build();
-
- assertThat(image1.localFile()).isEqualTo(FileUtils.urlToFile(FILE1));
- }
-
- public static void testGetUrl() throws Exception {
- final Image image1 = new Image.Builder().build();
- final Image image2 = new Image.Builder().setUrl("").build();
- final Image image3 = new Image.Builder().setUrl(FILE1).build();
- final Image image4 = new Image.Builder().setUrl(URL1).build();
- final Image image5 = new Image.Builder().setUrl(URL2).build();
-
- assertThat(image1.getUrl()).isEqualTo("");
- assertThat(image2.getUrl()).isEqualTo("");
- assertThat(image3.getUrl()).isEqualTo(FILE1);
- assertThat(image4.getUrl()).isEqualTo(URL1);
- assertThat(image5.getUrl()).isEqualTo(URL2);
- }
-
- public static void testGetPath() throws Exception {
- final Image image1 = new Image.Builder().build();
- final Image image2 = new Image.Builder().setUrl("").build();
- final Image image3 = new Image.Builder().setUrl(FILE1).build();
- final Image image4 = new Image.Builder().setUrl(FILE2).build();
- final Image image5 = new Image.Builder().setUrl(URL1).build();
- final Image image6 = new Image.Builder().setUrl(URL2).build();
-
- assertThat(image1.getPath()).isEqualTo("");
- assertThat(image2.getPath()).isEqualTo("");
- assertThat(image3.getPath()).isEqualTo("/dev/null");
- assertThat(image4.getPath()).isEqualTo("/tmp/image.png");
- assertThat(image5.getPath()).isEqualTo("");
- assertThat(image6.getPath()).isEqualTo("");
- }
-
- public static void testGetTitle() throws Exception {
- final Image image1 = new Image.Builder().setTitle("Title").build();
-
- assertThat(image1.getTitle()).isEqualTo("Title");
- }
-
- public static void testGetDescription() throws Exception {
- final Image image1 = new Image.Builder().setDescription("Description").build();
-
- assertThat(image1.getDescription()).isEqualTo("Description");
- }
-
- public static void testGetFile() throws Exception {
- final Image image1 = new Image.Builder().build();
- final Image image2 = new Image.Builder().setUrl(FILE1).build();
-
- assertThat(image1.getFile()).isEqualTo(null);
- assertThat(image2.getFile()).isEqualTo(new File(FILE1));
- }
-
- public static void testEquals() throws Exception {
- final Image image1 = new Image.Builder().setUrl(FILE1).setTitle("Title1").setDescription("Description1").build();
- final Image image2 = new Image.Builder().build();
- final Image image3 = new Image.Builder().setTitle("Title1").setDescription("Description1").build();
- final Image image4 = new Image.Builder().setUrl("").setTitle("Title1").setDescription("Description1").build();
- final Image image5 = new Image.Builder().setUrl(FILE1).setTitle("Title1").setDescription("Description2").build();
- final Image image6 = new Image.Builder().setUrl(FILE1).setTitle("FOO").setDescription("BAR").build();
- final Image image7 = new Image.Builder().setUrl(image6).build();
- final Image image8 = new Image.Builder().setUrl(image5).setDescription("Description1").build();
- final Image image9 = new Image.Builder().setUrl(image5).setTitle("Title1").setDescription("Description1").build();
-
- assertThat(image1).isEqualTo(image1);
- assertThat(image2).isEqualTo(image2);
- assertThat(image1).isNotEqualTo(image2);
- assertThat(image1).isNotEqualTo(image3);
- assertThat(image1).isNotEqualTo(image4);
- assertThat(image1).isNotEqualTo(image5);
- assertThat(image1).isNotEqualTo(image6);
- assertThat(image1).isNotEqualTo(image7);
- assertThat(image1).isNotEqualTo(image8);
- assertThat(image1).isEqualTo(image9);
- assertThat(image6).isNotEqualTo(image7);
- }
-
- public static void testParcel() throws Exception {
- final Image image1 = new Image.Builder().setUrl(FILE1).setTitle("Title1").setDescription("Description1").build();
-
- final Parcel parcel = Parcel.obtain();
- image1.writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
-
- final Image image2 = Image.CREATOR.createFromParcel(parcel);
-
- assertThat(image1).isEqualTo(image2);
- parcel.recycle();
- }
+package cgeo.geocaching;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import cgeo.CGeoTestCase;
+import cgeo.geocaching.utils.FileUtils;
+
+import android.net.Uri;
+import android.os.Parcel;
+
+import java.io.File;
+
+/**
+ * ImageTest unit tests
+ */
+public class ImageTest extends CGeoTestCase {
+ private static final String URL1 = "https://nowhe.re";
+ private static final String URL2 = "https://nowhe.re/image.png";
+
+ private static final String FILE1 = "file:///dev/null";
+ private static final String FILE2 = "file:///tmp/image.png";
+
+ public void testNullConstructor() throws Exception {
+ final Image nullImage1 = new Image.Builder().build();
+
+ assertThat(nullImage1).isEqualTo(Image.NONE);
+ }
+
+ public void testStringConstructor() throws Exception {
+ final Image image1 = new Image.Builder().setUrl("").build();
+ final Image image2 = new Image.Builder().setUrl(FILE1).build();
+ final Image image3 = new Image.Builder().setUrl(URL1).build();
+
+ assertThat(image1).isEqualTo(Image.NONE);
+ assertThat(image2).isNotEqualTo(Image.NONE);
+ assertThat(image3).isNotEqualTo(Image.NONE);
+ }
+
+ public void testUriConstructor() throws Exception {
+ final Image image1 = new Image.Builder().setUrl(Uri.parse(URL1)).build();
+ final Image image2 = new Image.Builder().setUrl(Uri.parse(URL2)).build();
+
+ assertThat(image1).isNotEqualTo(Image.NONE);
+ assertThat(image1.getUrl()).isEqualTo(URL1);
+ assertThat(image2.getUrl()).isEqualTo(URL2);
+ }
+
+ public void testFileConstructor() throws Exception {
+ final Image image1 = new Image.Builder().setUrl(FileUtils.urlToFile(FILE1)).build();
+ final Image image2 = new Image.Builder().setUrl(FileUtils.urlToFile(FILE2)).build();
+
+ assertThat(image1).isNotEqualTo(Image.NONE);
+ assertThat(image2).isNotEqualTo(Image.NONE);
+ assertThat(image1.getUrl()).isEqualTo(FILE1);
+ assertThat(image2.getUrl()).isEqualTo(FILE2);
+ assertThat(image2.getUrl()).isEqualTo(FILE2);
+ }
+
+ public void testTitle() throws Exception {
+ final Image image1 = new Image.Builder().setTitle("Title").build();
+
+ assertThat(image1).isNotEqualTo(Image.NONE);
+ assertThat(image1.getTitle()).isEqualTo("Title");
+ assertThat(image1.getDescription()).isNull();
+ }
+
+ public void testDescription() throws Exception {
+ final Image image1 = new Image.Builder().setDescription("Description").build();
+
+ assertThat(image1).isNotEqualTo(Image.NONE);
+ assertThat(image1.getTitle()).isNull();
+ assertThat(image1.getDescription()).isEqualTo("Description");
+ }
+
+ public void testIsEmpty() throws Exception {
+ final Image image1 = new Image.Builder().build();
+ final Image image2 = new Image.Builder().setUrl("").build();
+ final Image image3 = new Image.Builder().setUrl(FILE1).build();
+ final Image image4 = new Image.Builder().setUrl(URL1).build();
+
+ assertThat(image1.isEmpty()).isTrue();
+ assertThat(image2.isEmpty()).isTrue();
+ assertThat(image3.isEmpty()).isFalse();
+ assertThat(image4.isEmpty()).isFalse();
+ }
+
+ public void testIsLocalFile() throws Exception {
+ final Image image1 = new Image.Builder().build();
+ final Image image2 = new Image.Builder().setUrl("").build();
+ final Image image3 = new Image.Builder().setUrl(FILE1).build();
+ final Image image4 = new Image.Builder().setUrl(URL1).build();
+
+ assertThat(image1.isLocalFile()).isFalse();
+ assertThat(image2.isLocalFile()).isFalse();
+ assertThat(image3.isLocalFile()).isTrue();
+ assertThat(image4.isLocalFile()).isFalse();
+ }
+
+ public void testLocalFile() throws Exception {
+ final Image image1 = new Image.Builder().setUrl(FILE1).build();
+
+ assertThat(image1.localFile()).isEqualTo(FileUtils.urlToFile(FILE1));
+ }
+
+ public void testGetUrl() throws Exception {
+ final Image image1 = new Image.Builder().build();
+ final Image image2 = new Image.Builder().setUrl("").build();
+ final Image image3 = new Image.Builder().setUrl(FILE1).build();
+ final Image image4 = new Image.Builder().setUrl(URL1).build();
+ final Image image5 = new Image.Builder().setUrl(URL2).build();
+
+ assertThat(image1.getUrl()).isEqualTo("");
+ assertThat(image2.getUrl()).isEqualTo("");
+ assertThat(image3.getUrl()).isEqualTo(FILE1);
+ assertThat(image4.getUrl()).isEqualTo(URL1);
+ assertThat(image5.getUrl()).isEqualTo(URL2);
+ }
+
+ public void testGetPath() throws Exception {
+ final Image image1 = new Image.Builder().build();
+ final Image image2 = new Image.Builder().setUrl("").build();
+ final Image image3 = new Image.Builder().setUrl(FILE1).build();
+ final Image image4 = new Image.Builder().setUrl(FILE2).build();
+ final Image image5 = new Image.Builder().setUrl(URL1).build();
+ final Image image6 = new Image.Builder().setUrl(URL2).build();
+
+ assertThat(image1.getPath()).isEqualTo("");
+ assertThat(image2.getPath()).isEqualTo("");
+ assertThat(image3.getPath()).isEqualTo("/dev/null");
+ assertThat(image4.getPath()).isEqualTo("/tmp/image.png");
+ assertThat(image5.getPath()).isEqualTo("");
+ assertThat(image6.getPath()).isEqualTo("");
+ }
+
+ public void testGetTitle() throws Exception {
+ final Image image1 = new Image.Builder().setTitle("Title").build();
+
+ assertThat(image1.getTitle()).isEqualTo("Title");
+ }
+
+ public void testGetDescription() throws Exception {
+ final Image image1 = new Image.Builder().setDescription("Description").build();
+
+ assertThat(image1.getDescription()).isEqualTo("Description");
+ }
+
+ public void testGetFile() throws Exception {
+ final Image image1 = new Image.Builder().build();
+ final Image image2 = new Image.Builder().setUrl(FILE1).build();
+
+ assertThat(image1.getFile()).isEqualTo(null);
+ assertThat(image2.getFile()).isEqualTo(new File(FILE1));
+ }
+
+ public void testEquals() throws Exception {
+ final Image image1 = new Image.Builder().setUrl(FILE1).setTitle("Title1").setDescription("Description1").build();
+ final Image image2 = new Image.Builder().build();
+ final Image image3 = new Image.Builder().setTitle("Title1").setDescription("Description1").build();
+ final Image image4 = new Image.Builder().setUrl("").setTitle("Title1").setDescription("Description1").build();
+ final Image image5 = new Image.Builder().setUrl(FILE1).setTitle("Title1").setDescription("Description2").build();
+ final Image image6 = new Image.Builder().setUrl(FILE1).setTitle("FOO").setDescription("BAR").build();
+ final Image image7 = new Image.Builder().setUrl(image6).build();
+ final Image image8 = new Image.Builder().setUrl(image5).setDescription("Description1").build();
+ final Image image9 = new Image.Builder().setUrl(image5).setTitle("Title1").setDescription("Description1").build();
+
+ assertThat(image1).isEqualTo(image1);
+ assertThat(image2).isEqualTo(image2);
+ assertThat(image1).isNotEqualTo(image2);
+ assertThat(image1).isNotEqualTo(image3);
+ assertThat(image1).isNotEqualTo(image4);
+ assertThat(image1).isNotEqualTo(image5);
+ assertThat(image1).isNotEqualTo(image6);
+ assertThat(image1).isNotEqualTo(image7);
+ assertThat(image1).isNotEqualTo(image8);
+ assertThat(image1).isEqualTo(image9);
+ assertThat(image6).isNotEqualTo(image7);
+ }
+
+ public void testParcel() throws Exception {
+ final Image image1 = new Image.Builder().setUrl(FILE1).setTitle("Title1").setDescription("Description1").build();
+
+ final Parcel parcel = Parcel.obtain();
+ image1.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ Image image2 = Image.CREATOR.createFromParcel(parcel);
+
+ assertThat(image1).isEqualTo(image2);
+ }
} \ No newline at end of file
diff --git a/tests/src/cgeo/test/Compare.java b/tests/src/cgeo/test/Compare.java
index 31d3086..788a191 100644
--- a/tests/src/cgeo/test/Compare.java
+++ b/tests/src/cgeo/test/Compare.java
@@ -1,64 +1,66 @@
-package cgeo.test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import cgeo.geocaching.Geocache;
-import cgeo.geocaching.enumerations.LogType;
-import cgeo.geocaching.utils.CryptUtils;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Date;
-
-public abstract class Compare {
-
- public static void assertCompareCaches(final Geocache expected, final Geocache actual, final boolean all) {
- final String geocode = expected.getGeocode();
- final String cacheStr = "Cache " + geocode + ": ";
- assertThat(actual).isNotNull();
- assertThat(actual.getGeocode()).as(cacheStr + "geocode").isEqualTo(expected.getGeocode());
- assertThat(actual.getType()).as(cacheStr + "type").isEqualTo(expected.getType());
- assertThat(actual.getOwnerDisplayName()).as(cacheStr + "OwnerDisplayName").isEqualTo(expected.getOwnerDisplayName());
- assertThat(actual.getDifficulty()).as(cacheStr + "difficulty").isEqualTo(expected.getDifficulty());
- assertThat(actual.getTerrain()).as(cacheStr + "terrain").isEqualTo(expected.getTerrain());
- assertThat(actual.isDisabled()).as(cacheStr + "disabled").isEqualTo(expected.isDisabled());
- assertThat(actual.isArchived()).as(cacheStr + "archived").isEqualTo(expected.isArchived());
- assertThat(actual.getSize()).overridingErrorMessage(cacheStr + "expected size", expected.getSize()).isEqualTo(expected.getSize());
- assertThat(actual.getName()).as(cacheStr + "name").isEqualTo(expected.getName());
- assertThat(actual.getGuid()).as(cacheStr + "guid").isEqualTo(expected.getGuid());
- assertThat(actual.getFavoritePoints()).as(cacheStr + "fav points").isGreaterThanOrEqualTo(expected.getFavoritePoints());
- final Date hiddenDate = actual.getHiddenDate();
- assertThat(hiddenDate).isNotNull();
- assert hiddenDate != null; // silence the eclipse compiler in the next line
- assertThat(hiddenDate).as(cacheStr + " hidden date").isEqualTo(expected.getHiddenDate());
- assertThat(actual.isPremiumMembersOnly()).as(cacheStr + "premium only").isEqualTo(expected.isPremiumMembersOnly());
-
- if (all) {
- assertThat(actual.getCoords()).as(cacheStr + "coords").isEqualTo(expected.getCoords());
- assertThat(actual.isReliableLatLon()).as(cacheStr + "reliable latlon").isTrue();
- assertThat(actual.isOwner()).as(cacheStr + "owning status").isEqualTo(expected.isOwner());
- assertThat(actual.getOwnerUserId()).as(cacheStr + "owner user id").isEqualTo(expected.getOwnerUserId());
- assertThat(StringUtils.equals(expected.getHint(), actual.getHint()) || StringUtils.equals(expected.getHint(), CryptUtils.rot13(actual.getHint()))).isTrue();
- assertThat(actual.getDescription()).as("description").startsWith(expected.getDescription());
- assertThat(actual.getShortDescription()).as(cacheStr + "short description").isEqualTo(expected.getShortDescription());
- assertThat(actual.getCacheId()).as(cacheStr + "cache id").isEqualTo(expected.getCacheId());
- assertThat(actual.getLocation()).as(cacheStr + "location").isEqualTo(expected.getLocation());
- assertThat(actual.isFound()).as(cacheStr + "found status").isEqualTo(expected.isFound());
- assertThat(actual.isFavorite()).as(cacheStr + "favorite status").isEqualTo(expected.isFavorite());
- assertThat(actual.isOnWatchlist()).as(cacheStr + "watchlist status").isEqualTo(expected.isOnWatchlist());
-
- for (final String attribute : expected.getAttributes()) {
- assertThat(actual.getAttributes()).as("attributes of " + actual.getGeocode()).contains(attribute);
- }
- for (final LogType logType : expected.getLogCounts().keySet()) {
- assertThat(actual.getLogCounts().get(logType)).as("logcount of " + geocode + " for type " + logType.toString()).isGreaterThanOrEqualTo(expected.getLogCounts().get(logType));
- }
-
- // The inventories can differ too often, therefore we don't compare them. Also, the personal note
- // cannot be expected to match with different tester accounts.
-
- assertThat(actual.getSpoilers()).as("spoilers").hasSameSizeAs(expected.getSpoilers());
- }
- }
-
-}
+package cgeo.test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import cgeo.geocaching.Geocache;
+import cgeo.geocaching.enumerations.LogType;
+import cgeo.geocaching.utils.CryptUtils;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Date;
+
+public abstract class Compare {
+
+ public static void assertCompareCaches(final Geocache expected, final Geocache actual, final boolean all) {
+ final String geocode = expected.getGeocode();
+ final String cacheStr = "Cache " + geocode + ": ";
+ assertThat(actual).isNotNull();
+ assertThat(actual.getGeocode()).as(cacheStr + "geocode").isEqualTo(expected.getGeocode());
+ assertThat(actual.getType()).as(cacheStr + "type").isEqualTo(expected.getType());
+ assertThat(actual.getOwnerDisplayName()).as(cacheStr + "OwnerDisplayName").isEqualTo(expected.getOwnerDisplayName());
+ assertThat(actual.getDifficulty()).as(cacheStr + "difficulty").isEqualTo(expected.getDifficulty());
+ assertThat(actual.getTerrain()).as(cacheStr + "terrain").isEqualTo(expected.getTerrain());
+ assertThat(actual.isDisabled()).as(cacheStr + "disabled").isEqualTo(expected.isDisabled());
+ assertThat(actual.isArchived()).as(cacheStr + "archived").isEqualTo(expected.isArchived());
+ assertThat(actual.getSize()).overridingErrorMessage(cacheStr + "expected size", expected.getSize()).isEqualTo(expected.getSize());
+ assertThat(actual.getName()).as(cacheStr + "name").isEqualTo(expected.getName());
+ assertThat(actual.getGuid()).as(cacheStr + "guid").isEqualTo(expected.getGuid());
+ assertThat(actual.getFavoritePoints()).as(cacheStr + "fav points").isGreaterThanOrEqualTo(expected.getFavoritePoints());
+ final Date hiddenDate = actual.getHiddenDate();
+ assertThat(hiddenDate).isNotNull();
+ assert hiddenDate != null; // silence the eclipse compiler in the next line
+ assertThat(hiddenDate).as(cacheStr + " hidden date").isEqualTo(expected.getHiddenDate());
+ assertThat(actual.isPremiumMembersOnly()).as(cacheStr + "premium only").isEqualTo(expected.isPremiumMembersOnly());
+
+ if (all) {
+ assertThat(actual.getCoords()).as(cacheStr + "coords").isEqualTo(expected.getCoords());
+ assertThat(actual.isReliableLatLon()).as(cacheStr + "reliable latlon").isTrue();
+ assertThat(actual.isOwner()).as(cacheStr + "owning status").isEqualTo(expected.isOwner());
+ assertThat(actual.getOwnerUserId()).as(cacheStr + "owner user id").isEqualTo(expected.getOwnerUserId());
+ assertThat(StringUtils.equals(expected.getHint(), actual.getHint()) || StringUtils.equals(expected.getHint(), CryptUtils.rot13(actual.getHint()))).isTrue();
+ assertThat(actual.getDescription()).as("description").startsWith(expected.getDescription());
+ assertThat(actual.getShortDescription()).as(cacheStr + "short description").isEqualTo(expected.getShortDescription());
+ assertThat(actual.getCacheId()).as(cacheStr + "cache id").isEqualTo(expected.getCacheId());
+ assertThat(actual.getLocation()).as(cacheStr + "location").isEqualTo(expected.getLocation());
+ assertThat(actual.isFound()).as(cacheStr + "found status").isEqualTo(expected.isFound());
+ assertThat(actual.isFavorite()).as(cacheStr + "favorite status").isEqualTo(expected.isFavorite());
+ assertThat(actual.isOnWatchlist()).as(cacheStr + "watchlist status").isEqualTo(expected.isOnWatchlist());
+
+ for (final String attribute : expected.getAttributes()) {
+ assertThat(actual.getAttributes()).as("attributes of " + actual.getGeocode()).contains(attribute);
+ }
+ for (final LogType logType : expected.getLogCounts().keySet()) {
+ assertThat(actual.getLogCounts().get(logType)).as("logcount of " + geocode + " for type " + logType.toString()).isGreaterThanOrEqualTo(expected.getLogCounts().get(logType));
+ }
+
+ // The inventories can differ too often, therefore we don't compare them. Also, the personal note
+ // cannot be expected to match with different tester accounts.
+
+ final int actualSpoilersSize = actual.getSpoilers().size();
+ final int expectedSpoilersSize = expected.getSpoilers().size();
+ assertThat(actualSpoilersSize).as(cacheStr + "spoiler count").isEqualTo(expectedSpoilersSize);
+ }
+ }
+
+}