package cgeo.geocaching.ui; import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.apps.navi.NavigationAppFactory; import cgeo.geocaching.enumerations.WaypointType; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Image; import cgeo.geocaching.models.Waypoint; import cgeo.geocaching.network.HtmlImage; import cgeo.geocaching.storage.LocalStorage; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.Log; import android.app.Activity; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.content.LocalBroadcastManager; import android.text.Html; import android.util.SparseArray; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.webkit.MimeTypeMap; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.Collection; import java.util.LinkedList; import butterknife.ButterKnife; import com.drew.imaging.ImageMetadataReader; import com.drew.lang.GeoLocation; import com.drew.metadata.Metadata; import com.drew.metadata.exif.GpsDirectory; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Cancellable; import io.reactivex.functions.Consumer; import io.reactivex.internal.disposables.CancellableDisposable; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; public class ImagesList { private ImageView currentView; private Image currentImage; public enum ImageType { LogImages(R.string.cache_log_images_title), SpoilerImages(R.string.cache_spoiler_images_title); @StringRes private final int titleResId; ImageType(@StringRes final int title) { this.titleResId = title; } @StringRes public int getTitle() { return titleResId; } } private LayoutInflater inflater = null; private final Activity activity; // We could use a Set here, but we will insert no duplicates, so there is no need to check for uniqueness. private final Collection<Bitmap> bitmaps = new LinkedList<>(); /** * map image view id to image */ private final SparseArray<Image> images = new SparseArray<>(); private final SparseArray<Geopoint> geoPoints = new SparseArray<>(); private final String geocode; private final Geocache geocache; private LinearLayout imagesView; public ImagesList(final Activity activity, final String geocode, final Geocache geocache) { this.activity = activity; this.geocode = geocode; this.geocache = geocache; inflater = activity.getLayoutInflater(); } /** * Load images into a view. * * @param parentView * a view to load the images into * @param images * the images to load * @return a disposable which, when disposed, interrupts the loading and clears up resources */ public Disposable loadImages(final View parentView, final Collection<Image> images) { // Start with a fresh disposable because of this method can be called several times if the // enclosing activity is stopped/restarted. final CompositeDisposable disposables = new CompositeDisposable(new CancellableDisposable(new Cancellable() { @Override public void cancel() throws Exception { removeAllViews(); } })); imagesView = ButterKnife.findById(parentView, R.id.spoiler_list); final HtmlImage imgGetter = new HtmlImage(geocode, true, false, false); for (final Image img : images) { final LinearLayout rowView = (LinearLayout) inflater.inflate(R.layout.cache_image_item, imagesView, false); assert rowView != null; if (StringUtils.isNotBlank(img.getTitle())) { final TextView titleView = ButterKnife.findById(rowView, R.id.title); titleView.setText(Html.fromHtml(img.getTitle())); } if (StringUtils.isNotBlank(img.getDescription())) { final TextView descView = ButterKnife.findById(rowView, R.id.description); descView.setText(Html.fromHtml(img.getDescription()), TextView.BufferType.SPANNABLE); descView.setVisibility(View.VISIBLE); } final RelativeLayout imageView = (RelativeLayout) inflater.inflate(R.layout.image_item, rowView, false); assert imageView != null; rowView.addView(imageView); imagesView.addView(rowView); disposables.add(AndroidRxUtils.bindActivity(activity, imgGetter.fetchDrawable(img.getUrl())).subscribe(new Consumer<BitmapDrawable>() { @Override public void accept(final BitmapDrawable image) { display(imageView, image, img, rowView); } })); } return disposables; } private void display(final RelativeLayout imageViewLayout, final BitmapDrawable image, final Image img, final LinearLayout view) { final ImageView imageView = (ImageView) imageViewLayout.findViewById(R.id.map_image); // In case of a failed download happening fast, the imageView seems to not have been added to the layout yet if (image != null && imageView != null) { bitmaps.add(image.getBitmap()); final Rect bounds = image.getBounds(); imageView.setImageResource(R.drawable.image_not_loaded); imageView.setClickable(true); imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View view) { viewImageInStandardApp(img, image); } }); activity.registerForContextMenu(imageView); imageView.setImageDrawable(image); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setLayoutParams(new RelativeLayout.LayoutParams(bounds.width(), bounds.height())); view.findViewById(R.id.progress_bar).setVisibility(View.GONE); imageView.setId(image.hashCode()); images.put(imageView.getId(), img); final Geopoint geoPoint = getImageLocation(img); if (geoPoint != null) { addGeoOverlay(imageViewLayout, geoPoint); geoPoints.put(imageView.getId(), geoPoint); } view.invalidate(); } } @Nullable private Geopoint getImageLocation(final Image image) { try { final File file = LocalStorage.getGeocacheDataFile(geocode, image.getUrl(), true, false); final Metadata metadata = ImageMetadataReader.readMetadata(file); final Collection<GpsDirectory> gpsDirectories = metadata.getDirectoriesOfType(GpsDirectory.class); if (gpsDirectories == null) { return null; } for (final GpsDirectory gpsDirectory : gpsDirectories) { // Try to read out the location, making sure it's non-zero final GeoLocation geoLocation = gpsDirectory.getGeoLocation(); if (geoLocation != null && !geoLocation.isZero()) { return new Geopoint(geoLocation.getLatitude(), geoLocation.getLongitude()); } } } catch (final Exception e) { Log.i("ImagesList.getImageLocation", e); } return null; } private void addGeoOverlay(final RelativeLayout imageViewLayout, final Geopoint gpt) { final ImageView geoOverlay = (ImageView) imageViewLayout.findViewById(R.id.geo_overlay); geoOverlay.setVisibility(View.VISIBLE); geoOverlay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View wpNavView) { wpNavView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { NavigationAppFactory.startDefaultNavigationApplication(1, activity, gpt); } }); wpNavView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { NavigationAppFactory.startDefaultNavigationApplication(2, activity, gpt); return true; } }); } }); } private void removeAllViews() { for (final Bitmap b : bitmaps) { b.recycle(); } bitmaps.clear(); images.clear(); geoPoints.clear(); imagesView.removeAllViews(); } public void onCreateContextMenu(final ContextMenu menu, final View v) { activity.getMenuInflater().inflate(R.menu.images_list_context, menu); final Resources res = activity.getResources(); menu.setHeaderTitle(res.getString(R.string.cache_image)); currentView = (ImageView) v; currentImage = images.get(currentView.getId()); final boolean hasCoordinates = geoPoints.get(currentView.getId()) != null; menu.findItem(R.id.image_add_waypoint).setVisible(hasCoordinates && geocache != null); menu.findItem(R.id.menu_navigate).setVisible(hasCoordinates); } public boolean onContextItemSelected(final MenuItem item) { final BitmapDrawable currentDrawable = (BitmapDrawable) currentView.getDrawable(); switch (item.getItemId()) { case R.id.image_open_file: viewImageInStandardApp(currentImage, currentDrawable); return true; case R.id.image_open_browser: if (currentImage != null) { currentImage.openInBrowser(activity); } return true; case R.id.image_add_waypoint: final Geopoint coords = geoPoints.get(currentView.getId()); if (geocache != null && coords != null) { final Waypoint waypoint = new Waypoint(currentImage.getTitle(), WaypointType.WAYPOINT, true); waypoint.setCoords(coords); geocache.addOrChangeWaypoint(waypoint, true); final Intent intent = new Intent(Intents.INTENT_CACHE_CHANGED); intent.putExtra(Intents.EXTRA_WPT_PAGE_UPDATE, true); LocalBroadcastManager.getInstance(activity).sendBroadcast(intent); } return true; case R.id.menu_navigate: final Geopoint geopoint = geoPoints.get(currentView.getId()); if (geopoint != null) { NavigationAppFactory.showNavigationMenu(activity, null, null, geopoint); } return true; default: return false; } } private static String mimeTypeForUrl(final String url) { return StringUtils.defaultString(MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(url)), "image/*"); } private static File saveToTemporaryJPGFile(final BitmapDrawable image) throws FileNotFoundException { final File file = LocalStorage.getGeocacheDataFile(HtmlImage.SHARED, "temp.jpg", false, true); BufferedOutputStream stream = null; try { stream = new BufferedOutputStream(new FileOutputStream(file)); image.getBitmap().compress(CompressFormat.JPEG, 100, stream); } finally { IOUtils.closeQuietly(stream); } file.deleteOnExit(); return file; } private void viewImageInStandardApp(final Image img, final BitmapDrawable image) { try { final Intent intent = new Intent().setAction(Intent.ACTION_VIEW); final File file = img.isLocalFile() ? img.localFile() : LocalStorage.getGeocacheDataFile(geocode, img.getUrl(), true, true); if (file.exists()) { intent.setDataAndType(Uri.fromFile(file), mimeTypeForUrl(img.getUrl())); } else { intent.setDataAndType(Uri.fromFile(saveToTemporaryJPGFile(image)), "image/jpeg"); } activity.startActivity(intent); } catch (final Exception e) { Log.e("ImagesList.viewImageInStandardApp", e); } } }