package cgeo.geocaching; import cgeo.geocaching.activity.AbstractActivity; import cgeo.geocaching.activity.AbstractViewPagerActivity; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.trackable.TrackableBrand; import cgeo.geocaching.connector.trackable.TrackableTrackingCode; import cgeo.geocaching.location.Units; import cgeo.geocaching.log.LogEntry; import cgeo.geocaching.log.LogTrackableActivity; import cgeo.geocaching.log.LogType; import cgeo.geocaching.log.TrackableLogsViewCreator; import cgeo.geocaching.models.Trackable; import cgeo.geocaching.network.AndroidBeam; import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.ui.AbstractCachingPageViewCreator; import cgeo.geocaching.ui.AnchorAwareLinkMovementMethod; import cgeo.geocaching.ui.CacheDetailsCreator; import cgeo.geocaching.ui.ImagesList; import cgeo.geocaching.ui.UserActionsClickListener; import cgeo.geocaching.ui.UserNameClickListener; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.TextUtils; import cgeo.geocaching.utils.UnknownTagsHandler; import android.app.ProgressDialog; import android.content.Intent; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v7.app.ActionBar; import android.support.v7.view.ActionMode; import android.text.Html; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import butterknife.BindView; import butterknife.ButterKnife; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.functions.Action; import io.reactivex.functions.Consumer; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; public class TrackableActivity extends AbstractViewPagerActivity<TrackableActivity.Page> implements AndroidBeam.ActivitySharingInterface { public enum Page { DETAILS(R.string.detail), LOGS(R.string.cache_logs), IMAGES(R.string.cache_images); @StringRes private final int resId; Page(@StringRes final int resId) { this.resId = resId; } } private Trackable trackable = null; private String geocode = null; private String name = null; private String guid = null; private String id = null; private String geocache = null; private String trackingCode = null; private TrackableBrand brand = null; private LayoutInflater inflater = null; private ProgressDialog waitDialog = null; private CharSequence clickedItemText = null; private ImagesList imagesList = null; private final CompositeDisposable createDisposables = new CompositeDisposable(); private final CompositeDisposable geoDataDisposable = new CompositeDisposable(); private static final GeoDirHandler locationUpdater = new GeoDirHandler() { @Override public void updateGeoData(final GeoData geoData) { // Do not do anything, as we just want to maintain the GPS on } }; /** * Action mode of the current contextual action bar (e.g. for copy and share actions). */ private ActionMode currentActionMode; @Override public void onCreate(final Bundle savedInstanceState) { onCreate(savedInstanceState, R.layout.viewpager_activity); // set title in code, as the activity needs a hard coded title due to the intent filters setTitle(res.getString(R.string.trackable)); // get parameters final Bundle extras = getIntent().getExtras(); final Uri uri = AndroidBeam.getUri(getIntent()); if (extras != null) { // try to get data from extras geocode = extras.getString(Intents.EXTRA_GEOCODE); name = extras.getString(Intents.EXTRA_NAME); guid = extras.getString(Intents.EXTRA_GUID); id = extras.getString(Intents.EXTRA_ID); geocache = extras.getString(Intents.EXTRA_GEOCACHE); brand = TrackableBrand.getById(extras.getInt(Intents.EXTRA_BRAND)); trackingCode = extras.getString(Intents.EXTRA_TRACKING_CODE); } // try to get data from URI if (geocode == null && guid == null && id == null && uri != null) { geocode = ConnectorFactory.getTrackableFromURL(uri.toString()); final TrackableTrackingCode tbTrackingCode = ConnectorFactory.getTrackableTrackingCodeFromURL(uri.toString()); final String uriHost = uri.getHost().toLowerCase(Locale.US); if (uriHost.endsWith("geocaching.com")) { geocode = uri.getQueryParameter("tracker"); guid = uri.getQueryParameter("guid"); id = uri.getQueryParameter("id"); if (StringUtils.isNotBlank(geocode)) { geocode = geocode.toUpperCase(Locale.US); guid = null; id = null; } else if (StringUtils.isNotBlank(guid)) { geocode = null; guid = guid.toLowerCase(Locale.US); id = null; } else if (StringUtils.isNotBlank(id)) { geocode = null; guid = null; id = id.toLowerCase(Locale.US); } else { showToast(res.getString(R.string.err_tb_details_open)); finish(); return; } } else if (uriHost.endsWith("geokrety.org")) { brand = TrackableBrand.GEOKRETY; // If geocode isn't found, try to find by Tracking Code if (geocode == null && !tbTrackingCode.isEmpty()) { trackingCode = tbTrackingCode.trackingCode; geocode = tbTrackingCode.trackingCode; } } } // no given data if (geocode == null && guid == null && id == null) { showToast(res.getString(R.string.err_tb_display)); finish(); return; } final String message; if (StringUtils.isNotBlank(name)) { message = TextUtils.stripHtml(name); } else if (StringUtils.isNotBlank(geocode)) { message = geocode; } else { message = res.getString(R.string.trackable); } // If we have a newer Android device setup Android Beam for easy cache sharing AndroidBeam.enable(this, this); createViewPager(0, new OnPageSelectedListener() { @Override public void onPageSelected(final int position) { lazyLoadTrackableImages(); } }); refreshTrackable(message); } @Override public void onResume() { super.onResume(); if (!Settings.useLowPowerMode()) { geoDataDisposable.add(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA)); } } @Override public void onPause() { geoDataDisposable.clear(); super.onPause(); } private void act(final Trackable newTrackable) { trackable = newTrackable; displayTrackable(); // reset imagelist imagesList = null; lazyLoadTrackableImages(); } private void refreshTrackable(final String message) { waitDialog = ProgressDialog.show(this, message, res.getString(R.string.trackable_details_loading), true, true); createDisposables.add(AndroidRxUtils.bindActivity(this, ConnectorFactory.loadTrackable(geocode, guid, id, brand)).subscribe( new Consumer<Trackable>() { @Override public void accept(final Trackable newTrackable) throws Exception { if (trackingCode != null) { newTrackable.setTrackingcode(trackingCode); } act(newTrackable); } }, new Consumer<Throwable>() { @Override public void accept(final Throwable throwable) throws Exception { Log.w("unable to retrieve trackable information", throwable); showToast(res.getString(R.string.err_tb_find_that)); finish(); } }, new Action() { @Override public void run() throws Exception { act(null); } })); } @Nullable @Override public String getAndroidBeamUri() { return trackable != null ? trackable.getUrl() : null; } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.trackable_activity, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_log_touch: startActivityForResult(LogTrackableActivity.getIntent(this, trackable, geocache), LogTrackableActivity.LOG_TRACKABLE); return true; case R.id.menu_browser_trackable: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(trackable.getUrl()))); return true; case R.id.menu_refresh_trackable: refreshTrackable(StringUtils.defaultIfBlank(trackable.getName(), trackable.getGeocode())); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onPrepareOptionsMenu(final Menu menu) { if (trackable != null) { menu.findItem(R.id.menu_log_touch).setVisible(StringUtils.isNotBlank(geocode) && trackable.isLoggable()); menu.findItem(R.id.menu_browser_trackable).setVisible(trackable.hasUrl()); menu.findItem(R.id.menu_refresh_trackable).setVisible(true); } return super.onPrepareOptionsMenu(menu); } public void displayTrackable() { if (trackable == null) { Dialogs.dismiss(waitDialog); if (StringUtils.isNotBlank(geocode)) { showToast(res.getString(R.string.err_tb_find) + " " + geocode + "."); } else { showToast(res.getString(R.string.err_tb_find_that)); } finish(); return; } try { inflater = getLayoutInflater(); geocode = trackable.getGeocode(); if (StringUtils.isNotBlank(trackable.getName())) { setTitle(TextUtils.stripHtml(trackable.getName())); } else { setTitle(trackable.getName()); } invalidateOptionsMenuCompatible(); reinitializeViewPager(); } catch (final Exception e) { Log.e("TrackableActivity.loadTrackableHandler: ", e); } Dialogs.dismiss(waitDialog); } private void setupIcon(final ActionBar actionBar, final String url) { final HtmlImage imgGetter = new HtmlImage(HtmlImage.SHARED, false, false, false); AndroidRxUtils.bindActivity(this, imgGetter.fetchDrawable(url)).subscribe(new Consumer<BitmapDrawable>() { @Override public void accept(final BitmapDrawable image) { if (actionBar != null) { final int height = actionBar.getHeight(); //noinspection SuspiciousNameCombination image.setBounds(0, 0, height, height); actionBar.setIcon(image); } } }); } private static void setupIcon(final ActionBar actionBar, @DrawableRes final int resId) { if (actionBar != null) { actionBar.setIcon(resId); } } public static void startActivity(final AbstractActivity fromContext, final String guid, final String geocode, final String name, final String geocache, final int brandId) { final Intent trackableIntent = new Intent(fromContext, TrackableActivity.class); trackableIntent.putExtra(Intents.EXTRA_GUID, guid); trackableIntent.putExtra(Intents.EXTRA_GEOCODE, geocode); trackableIntent.putExtra(Intents.EXTRA_NAME, name); trackableIntent.putExtra(Intents.EXTRA_GEOCACHE, geocache); trackableIntent.putExtra(Intents.EXTRA_BRAND, brandId); fromContext.startActivity(trackableIntent); } @Override protected PageViewCreator createViewCreator(final Page page) { switch (page) { case DETAILS: return new DetailsViewCreator(); case LOGS: return new TrackableLogsViewCreator(this); case IMAGES: return new ImagesViewCreator(); } throw new IllegalStateException(); // cannot happen as long as switch case is enum complete } private class ImagesViewCreator extends AbstractCachingPageViewCreator<View> { @Override public View getDispatchedView(final ViewGroup parentView) { view = getLayoutInflater().inflate(R.layout.cachedetail_images_page, parentView, false); return view; } } private void loadTrackableImages() { if (imagesList != null) { return; } final PageViewCreator creator = getViewCreator(Page.IMAGES); if (creator == null) { return; } final View imageView = creator.getView(null); if (imageView == null) { return; } imagesList = new ImagesList(this, trackable.getGeocode(), null); createDisposables.add(imagesList.loadImages(imageView, trackable.getImages())); } /** * Start loading images only when on images tab */ private void lazyLoadTrackableImages() { if (isCurrentPage(Page.IMAGES)) { loadTrackableImages(); } } @Override protected String getTitle(final Page page) { return res.getString(page.resId); } @Override protected Pair<List<? extends Page>, Integer> getOrderedPages() { final List<Page> pages = new ArrayList<>(); pages.add(Page.DETAILS); if (CollectionUtils.isNotEmpty(trackable.getLogs())) { pages.add(Page.LOGS); } if (CollectionUtils.isNotEmpty(trackable.getImages())) { pages.add(Page.IMAGES); } return new ImmutablePair<List<? extends Page>, Integer>(pages, 0); } public class DetailsViewCreator extends AbstractCachingPageViewCreator<ScrollView> { @BindView(R.id.goal_box) protected LinearLayout goalBox; @BindView(R.id.goal) protected TextView goalTextView; @BindView(R.id.details_box) protected LinearLayout detailsBox; @BindView(R.id.details) protected TextView detailsTextView; @BindView(R.id.image_box) protected LinearLayout imageBox; @BindView(R.id.details_list) protected LinearLayout detailsList; @BindView(R.id.image) protected LinearLayout imageView; @Override public ScrollView getDispatchedView(final ViewGroup parentView) { view = (ScrollView) getLayoutInflater().inflate(R.layout.trackable_details_view, parentView, false); ButterKnife.bind(this, view); final CacheDetailsCreator details = new CacheDetailsCreator(TrackableActivity.this, detailsList); // action bar icon if (StringUtils.isNotBlank(trackable.getIconUrl())) { setupIcon(getSupportActionBar(), trackable.getIconUrl()); } else { setupIcon(getSupportActionBar(), trackable.getIconBrand()); } // trackable name final TextView nameTxtView = details.add(R.string.trackable_name, StringUtils.isNotBlank(trackable.getName()) ? TextUtils.stripHtml(trackable.getName()) : res.getString(R.string.trackable_unknown)).right; addContextMenu(nameTxtView); // missing status if (trackable.isMissing()) { nameTxtView.setPaintFlags(nameTxtView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); } // trackable type final String tbType; if (StringUtils.isNotBlank(trackable.getType())) { tbType = TextUtils.stripHtml(trackable.getType()); } else { tbType = res.getString(R.string.trackable_unknown); } details.add(R.string.trackable_brand, trackable.getBrand().getLabel()); details.add(R.string.trackable_type, tbType); // trackable geocode addContextMenu(details.add(R.string.trackable_code, trackable.getGeocode()).right); // trackable owner final TextView owner = details.add(R.string.trackable_owner, res.getString(R.string.trackable_unknown)).right; if (StringUtils.isNotBlank(trackable.getOwner())) { owner.setText(Html.fromHtml(trackable.getOwner()), TextView.BufferType.SPANNABLE); owner.setOnClickListener(new UserActionsClickListener(trackable)); } // trackable spotted if (StringUtils.isNotBlank(trackable.getSpottedName()) || trackable.getSpottedType() == Trackable.SPOTTED_UNKNOWN || trackable.getSpottedType() == Trackable.SPOTTED_OWNER || trackable.getSpottedType() == Trackable.SPOTTED_ARCHIVED) { final StringBuilder text; boolean showTimeSpan = true; switch (trackable.getSpottedType()) { case Trackable.SPOTTED_CACHE: // TODO: the whole sentence fragment should not be constructed, but taken from the resources text = new StringBuilder(res.getString(R.string.trackable_spotted_in_cache)).append(' ').append(Html.fromHtml(trackable.getSpottedName())); break; case Trackable.SPOTTED_USER: // TODO: the whole sentence fragment should not be constructed, but taken from the resources text = new StringBuilder(res.getString(R.string.trackable_spotted_at_user)).append(' ').append(Html.fromHtml(trackable.getSpottedName())); break; case Trackable.SPOTTED_UNKNOWN: text = new StringBuilder(res.getString(R.string.trackable_spotted_unknown_location)); break; case Trackable.SPOTTED_OWNER: text = new StringBuilder(res.getString(R.string.trackable_spotted_owner)); break; case Trackable.SPOTTED_ARCHIVED: text = new StringBuilder(res.getString(R.string.trackable_spotted_archived)); break; default: text = new StringBuilder("N/A"); showTimeSpan = false; break; } // days since last spotting if (showTimeSpan) { for (final LogEntry log : trackable.getLogs()) { if (log.getType() == LogType.RETRIEVED_IT || log.getType() == LogType.GRABBED_IT || log.getType() == LogType.DISCOVERED_IT || log.getType() == LogType.PLACED_IT) { text.append(" (").append(Formatter.formatDaysAgo(log.date)).append(')'); break; } } } final TextView spotted = details.add(R.string.trackable_spotted, text.toString()).right; spotted.setClickable(true); if (trackable.getSpottedType() == Trackable.SPOTTED_CACHE) { spotted.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View arg0) { if (StringUtils.isNotBlank(trackable.getSpottedGuid())) { CacheDetailActivity.startActivityGuid(TrackableActivity.this, trackable.getSpottedGuid(), trackable.getSpottedName()); } else { // for GeoKrety we only know the cache geocode final String cacheCode = trackable.getSpottedName(); if (ConnectorFactory.canHandle(cacheCode)) { CacheDetailActivity.startActivity(TrackableActivity.this, cacheCode); } } } }); } else if (trackable.getSpottedType() == Trackable.SPOTTED_USER) { spotted.setOnClickListener(new UserNameClickListener(trackable, TextUtils.stripHtml(trackable.getSpottedName()))); } else if (trackable.getSpottedType() == Trackable.SPOTTED_OWNER) { spotted.setOnClickListener(new UserNameClickListener(trackable, TextUtils.stripHtml(trackable.getOwner()))); } } // trackable origin if (StringUtils.isNotBlank(trackable.getOrigin())) { final TextView origin = details.add(R.string.trackable_origin, "").right; origin.setText(Html.fromHtml(trackable.getOrigin()), TextView.BufferType.SPANNABLE); addContextMenu(origin); } // trackable released final Date releasedDate = trackable.getReleased(); if (releasedDate != null) { addContextMenu(details.add(R.string.trackable_released, Formatter.formatDate(releasedDate.getTime())).right); } // trackable distance if (trackable.getDistance() >= 0) { addContextMenu(details.add(R.string.trackable_distance, Units.getDistanceFromKilometers(trackable.getDistance())).right); } // trackable goal if (StringUtils.isNotBlank(HtmlUtils.extractText(trackable.getGoal()))) { goalBox.setVisibility(View.VISIBLE); goalTextView.setVisibility(View.VISIBLE); goalTextView.setText(Html.fromHtml(trackable.getGoal(), new HtmlImage(geocode, true, false, goalTextView, false), null), TextView.BufferType.SPANNABLE); goalTextView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); addContextMenu(goalTextView); } // trackable details if (StringUtils.isNotBlank(HtmlUtils.extractText(trackable.getDetails()))) { detailsBox.setVisibility(View.VISIBLE); detailsTextView.setVisibility(View.VISIBLE); detailsTextView.setText(Html.fromHtml(trackable.getDetails(), new HtmlImage(geocode, true, false, detailsTextView, false), new UnknownTagsHandler()), TextView.BufferType.SPANNABLE); detailsTextView.setMovementMethod(AnchorAwareLinkMovementMethod.getInstance()); addContextMenu(detailsTextView); } // trackable image if (StringUtils.isNotBlank(trackable.getImage())) { imageBox.setVisibility(View.VISIBLE); final ImageView trackableImage = (ImageView) inflater.inflate(R.layout.trackable_image, imageView, false); trackableImage.setImageResource(R.drawable.image_not_loaded); trackableImage.setClickable(true); trackableImage.setOnClickListener(new OnClickListener() { @Override public void onClick(final View view) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(trackable.getImage()))); } }); AndroidRxUtils.bindActivity(TrackableActivity.this, new HtmlImage(geocode, true, false, false).fetchDrawable(trackable.getImage())).subscribe(new Consumer<BitmapDrawable>() { @Override public void accept(final BitmapDrawable bitmapDrawable) { trackableImage.setImageDrawable(bitmapDrawable); } }); imageView.addView(trackableImage); } return view; } } @Override public void addContextMenu(final View view) { view.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(final View v) { return startContextualActionBar(view); } }); view.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { startContextualActionBar(view); } }); } private boolean startContextualActionBar(final View view) { if (currentActionMode != null) { return false; } currentActionMode = startSupportActionMode(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) { return prepareClipboardActionMode(view, actionMode, menu); } private boolean prepareClipboardActionMode(final View view, final ActionMode actionMode, final Menu menu) { final int viewId = view.getId(); clickedItemText = ((TextView) view).getText(); switch (viewId) { case R.id.value: // name, TB-code, origin, released, distance final TextView textView = ButterKnife.findById((View) view.getParent(), R.id.name); final CharSequence itemTitle = textView.getText(); buildDetailsContextMenu(actionMode, menu, itemTitle, true); return true; case R.id.goal: buildDetailsContextMenu(actionMode, menu, res.getString(R.string.trackable_goal), false); return true; case R.id.details: buildDetailsContextMenu(actionMode, menu, res.getString(R.string.trackable_details), false); return true; case R.id.log: buildDetailsContextMenu(actionMode, menu, res.getString(R.string.cache_logs), false); return true; } return false; } @Override public void onDestroyActionMode(final ActionMode actionMode) { currentActionMode = null; } @Override public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) { actionMode.getMenuInflater().inflate(R.menu.details_context, menu); prepareClipboardActionMode(view, actionMode, menu); return true; } @Override public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) { return onClipboardItemSelected(actionMode, menuItem, clickedItemText, null); } }); return false; } @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { // Refresh the logs view after coming back from logging a trackable if (requestCode == LogTrackableActivity.LOG_TRACKABLE && resultCode == RESULT_OK) { refreshTrackable(StringUtils.defaultIfBlank(trackable.getName(), trackable.getGeocode())); } } @Override protected void onDestroy() { createDisposables.clear(); super.onDestroy(); } public Trackable getTrackable() { return trackable; } @Override public void finish() { Dialogs.dismiss(waitDialog); super.finish(); } }