package com.vaguehope.onosendai.ui; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import local.apache.ByteArrayOutputStream; import android.app.ProgressDialog; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Rect; import android.net.Uri; import android.os.AsyncTask; import android.view.LayoutInflater; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.TextView; import com.vaguehope.onosendai.R; import com.vaguehope.onosendai.storage.AttachmentStorage; import com.vaguehope.onosendai.util.DialogHelper.Listener; import com.vaguehope.onosendai.util.ImageMetadata; import com.vaguehope.onosendai.util.IoHelper; import com.vaguehope.onosendai.util.LogWrapper; import com.vaguehope.onosendai.util.Result; import com.vaguehope.onosendai.util.Titleable; public class PictureResizeDialog implements Titleable { private static final List<Scale> SCALES = Scale.setOf(100, 50, 25); private static final int DEFAULT_SCALES_POS = 1; private static final List<Scale> QUALITIES = Scale.setOf(100, 95, 90, 80, 70, 50, 30, 10); private static final int DEFAULT_QUALITY_POS = 2; private static final LogWrapper LOG = new LogWrapper("PRD"); private final Context context; private final ImageMetadata srcMetadata; private final View llParent; private final ImageView imgPreview; private final Spinner spnScale; private final Spinner spnQuality; private final ProgressBar prgRedrawing; private final TextView txtSummary; public PictureResizeDialog (final Context context, final Uri pictureUri) { this.context = context; this.srcMetadata = new ImageMetadata(this.context, pictureUri); final LayoutInflater inflater = LayoutInflater.from(context); this.llParent = inflater.inflate(R.layout.pictureresizedialog, null); this.imgPreview = (ImageView) this.llParent.findViewById(R.id.imgPreview); this.spnScale = (Spinner) this.llParent.findViewById(R.id.spnScale); this.spnQuality = (Spinner) this.llParent.findViewById(R.id.spnQuality); this.prgRedrawing = (ProgressBar) this.llParent.findViewById(R.id.prgRedrawing); this.txtSummary = (TextView) this.llParent.findViewById(R.id.txtSummary); final ArrayAdapter<Scale> scaleAdapter = new ArrayAdapter<Scale>(context, R.layout.numberspinneritem); scaleAdapter.addAll(SCALES); this.spnScale.setAdapter(scaleAdapter); this.spnScale.setSelection(DEFAULT_SCALES_POS); this.spnScale.setOnItemSelectedListener(this.spnChangeListener); final ArrayAdapter<Scale> qualityAdapter = new ArrayAdapter<Scale>(context, R.layout.numberspinneritem); qualityAdapter.addAll(QUALITIES); this.spnQuality.setAdapter(qualityAdapter); this.spnQuality.setSelection(DEFAULT_QUALITY_POS); this.spnQuality.setOnItemSelectedListener(this.spnChangeListener); } public View getRootView () { return this.llParent; } @Override public String getUiTitle () { return "Shrink " + this.srcMetadata.getName(); //ES } public Scale getScale () { return (Scale) this.spnScale.getSelectedItem(); } public Scale getQuality () { return (Scale) this.spnQuality.getSelectedItem(); } public Uri resizeToTempFile () throws IOException { final Bitmap scaled = this.srcMetadata.getBitmap(getScale().getPercentage()); final File tgt = AttachmentStorage.getTempFile(this.context, "shrunk_" + this.srcMetadata.getName()); final OutputStream tgtOut = new FileOutputStream(tgt); try { scaled.compress(Bitmap.CompressFormat.JPEG, getQuality().getPercentage(), tgtOut); return Uri.fromFile(tgt); } finally { IoHelper.closeQuietly(tgtOut); } } public void recycle () { this.srcMetadata.recycle(); } protected Context getContext () { return this.context; } private final OnItemSelectedListener spnChangeListener = new OnItemSelectedListener() { @Override public void onItemSelected (final AdapterView<?> parent, final View view, final int position, final long id) { updateSummary(); } @Override public void onNothingSelected (final AdapterView<?> arg0) {/**/} }; private Scale lastScale = null; private Scale lastQuality = null; protected void updateSummary () { if (this.lastScale == getScale() && this.lastQuality == getQuality()) return; this.lastScale = getScale(); this.lastQuality = getQuality(); new PreviewResizeTask(this.txtSummary, this.imgPreview, this.prgRedrawing, this.srcMetadata, this).execute(); } protected int scaleDimension (final int from) { final Scale s = getScale(); if (s.getPercentage() == 100) return from; // NOSONAR 100 not magic number, its how percentage works. return (int) (from * s.getPercentage() / 100d); // NOSONAR 100 not magic number, its how percentage works. } public static class Scale { private final int percentage; private final String title; public Scale (final int percentage, final String title) { this.percentage = percentage; this.title = title; } public int getPercentage () { return this.percentage; } @Override public String toString () { return this.title; } public static List<Scale> setOf (final int... a) { final List<Scale> ret = new ArrayList<Scale>(); for (final int i : a) { ret.add(new Scale(i, i + "%")); } return Collections.unmodifiableList(ret); } } private static class PreviewResizeTask extends AsyncTask<Void, Void, Bitmap> { private final TextView txtSummary; private final ImageView imgPreview; private final ProgressBar prgRedrawing; private final StringBuilder summary; private final ImageMetadata srcMetadata; private final PictureResizeDialog dlg; public PreviewResizeTask (final TextView txtSummary, final ImageView imgPreview, final ProgressBar prgRedrawing, final ImageMetadata srcMetadata, final PictureResizeDialog dlg) { this.txtSummary = txtSummary; this.imgPreview = imgPreview; this.prgRedrawing = prgRedrawing; this.srcMetadata = srcMetadata; this.dlg = dlg; this.summary = new StringBuilder(); } @Override protected void onPreExecute () { this.prgRedrawing.setVisibility(View.VISIBLE); } @Override protected Bitmap doInBackground (final Void... params) { LOG.i("Generating preview: s=%s q=%s.", this.dlg.getScale(), this.dlg.getQuality()); try { final Bitmap scaled = this.srcMetadata.getBitmap(this.dlg.getScale().getPercentage()); final ByteArrayOutputStream compOut = new ByteArrayOutputStream(512 * 1024); if (scaled.compress(Bitmap.CompressFormat.JPEG, this.dlg.getQuality().getPercentage(), compOut)) { this.summary .append(this.srcMetadata.getWidth()).append(" x ").append(this.srcMetadata.getHeight()) .append(" (").append(IoHelper.readableFileSize(this.srcMetadata.getSize())).append(")") .append(" --> ").append(scaled.getWidth()).append(" x ").append(scaled.getHeight()) .append(" (").append(IoHelper.readableFileSize(compOut.size())).append(")"); final BitmapRegionDecoder dec = BitmapRegionDecoder.newInstance(compOut.toBufferedInputStream(), true); try { final int srcW = dec.getWidth(); final int srcH = dec.getHeight(); final int tgtW = this.dlg.getRootView().getWidth(); // FIXME Workaround for ImageView width issue. Fix properly with something like FixedWidthImageView. final int tgtH = this.imgPreview.getHeight(); final int left = srcW > tgtW ? (srcW - tgtW) / 2 : 0; final int top = srcH > tgtH ? (srcH - tgtH) / 2 : 0; final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPurgeable = true; options.inInputShareable = true; return dec.decodeRegion(new Rect(left, top, left + tgtW, top + tgtH), options); } finally { dec.recycle(); } } this.summary.append("Failed to compress image."); //ES return null; } // XXX Many cases of OutOfMemoryError have been reported, particularly on low end hardware. // Try not to upset the user too much by not dying completely if possible. catch (final Throwable e) { LOG.e("Failed to generate preview image.", e); this.summary.append(e.toString()); return null; } } @Override protected void onPostExecute (final Bitmap result) { this.txtSummary.setText(this.summary.toString()); if (result != null) { this.imgPreview.setImageBitmap(result); } else { this.imgPreview.setImageResource(R.drawable.exclamation_red); } this.prgRedrawing.setVisibility(View.INVISIBLE); } } public static class ResizeToTempFileTask extends AsyncTask<Void, Void, Result<Uri>> { private final PictureResizeDialog prd; private final Listener<Result<Uri>> resultListener; private ProgressDialog dialog; public ResizeToTempFileTask (final PictureResizeDialog prd, final Listener<Result<Uri>> resultListener) { this.prd = prd; this.resultListener = resultListener; } @Override protected void onPreExecute () { this.dialog = ProgressDialog.show(this.prd.getContext(), "Shrink image", "Resizing image...", true); //ES } @Override protected Result<Uri> doInBackground (final Void... params) { try { return new Result<Uri>(this.prd.resizeToTempFile()); } catch (final Exception e) { // NOSONAR show user all errors. return new Result<Uri>(e); } // XXX Cases of OutOfMemoryError have been reported, particularly on low end hardware. // Try not to upset the user too much by not dying completely if possible. catch (final Throwable e) { return new Result<Uri>(new ExecutionException("Failed to resize image.", e)); } } @Override protected void onPostExecute (final Result<Uri> result) { this.dialog.dismiss(); this.resultListener.onAnswer(result); } } }