diff options
author | Samuel Tardieu <sam@rfc1149.net> | 2015-08-29 12:59:10 +0200 |
---|---|---|
committer | Samuel Tardieu <sam@rfc1149.net> | 2015-08-29 12:59:47 +0200 |
commit | 0b86d40eeb5e9ae1c5ee63b5fc8361f69f5d29af (patch) | |
tree | 2a9c3961ca8022662ba58428f4dc2f96b05e61ac | |
parent | 1bd7c325ce8d296ef50f0f57a29c9497d042f1d3 (diff) | |
download | cgeo-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.
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); + } + } + +} |