/* * Copyright (C) 2009 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.odk.collect.android.widgets; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.utilities.FileUtils; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.provider.MediaStore.Images; import android.util.Log; import android.util.TypedValue; import android.view.Display; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; import com.radicaldynamic.groupinform.R; import com.radicaldynamic.groupinform.activities.FormEntryActivity; import com.radicaldynamic.groupinform.application.Collect; import com.radicaldynamic.groupinform.utilities.FileUtilsExtended; /** * Widget that allows user to take pictures, sounds or video and add them to the form. * * @author Carl Hartung (carlhartung@gmail.com) * @author Yaw Anokwa (yanokwa@gmail.com) */ public class ImageWidget extends QuestionWidget implements IBinaryWidget { private final static String t = "MediaWidget"; private Button mCaptureButton; private Button mChooseButton; private ImageView mImageView; private String mBinaryName; private String mInstanceFolder; private boolean mWaitingForData; private TextView mErrorTextView; public ImageWidget(Context context, FormEntryPrompt prompt) { super(context, prompt); // BEGIN custom // Try and avoid out-of-memory errors if at all possible System.gc(); // END custom mWaitingForData = false; mInstanceFolder = FormEntryActivity.mInstancePath.substring(0, FormEntryActivity.mInstancePath.lastIndexOf("/") + 1); setOrientation(LinearLayout.VERTICAL); TableLayout.LayoutParams params = new TableLayout.LayoutParams(); params.setMargins(7, 5, 7, 5); mErrorTextView = new TextView(context); mErrorTextView.setText("Selected file is not a valid image"); // setup capture button mCaptureButton = new Button(getContext()); mCaptureButton.setText(getContext().getString(R.string.capture_image)); mCaptureButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mAnswerFontsize); mCaptureButton.setPadding(20, 20, 20, 20); mCaptureButton.setEnabled(!prompt.isReadOnly()); mCaptureButton.setLayoutParams(params); // launch capture intent on click mCaptureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mErrorTextView.setVisibility(View.GONE); Intent i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); // We give the camera an absolute filename/path where to put the // picture because of bug: // http://code.google.com/p/android/issues/detail?id=1480 // The bug appears to be fixed in Android 2.0+, but as of feb 2, // 2010, G1 phones only run 1.6. Without specifying the path the // images returned by the camera in 1.6 (and earlier) are ~1/4 // the size. boo. // if this gets modified, the onActivityResult in // FormEntyActivity will also need to be updated. // BEGIN custom // i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, // Uri.fromFile(new File(Collect.TMPFILE_PATH))); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(FileUtilsExtended.EXTERNAL_CACHE + File.separator + FileUtilsExtended.CAPTURED_IMAGE_FILE))); // END custom try { ((Activity) getContext()).startActivityForResult(i, FormEntryActivity.IMAGE_CAPTURE); mWaitingForData = true; } catch (ActivityNotFoundException e) { Toast.makeText(getContext(), getContext().getString(R.string.activity_not_found, "image capture"), Toast.LENGTH_SHORT); } } }); // setup chooser button mChooseButton = new Button(getContext()); mChooseButton.setText(getContext().getString(R.string.choose_image)); mChooseButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mAnswerFontsize); mChooseButton.setPadding(20, 20, 20, 20); mChooseButton.setEnabled(!prompt.isReadOnly()); mChooseButton.setLayoutParams(params); // launch capture intent on click mChooseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mErrorTextView.setVisibility(View.GONE); Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.setType("image/*"); try { ((Activity) getContext()).startActivityForResult(i, FormEntryActivity.IMAGE_CHOOSER); mWaitingForData = true; } catch (ActivityNotFoundException e) { Toast.makeText(getContext(), getContext().getString(R.string.activity_not_found, "choose image"), Toast.LENGTH_SHORT); } } }); // finish complex layout addView(mCaptureButton); addView(mChooseButton); addView(mErrorTextView); mErrorTextView.setVisibility(View.GONE); // retrieve answer from data model and update ui mBinaryName = prompt.getAnswerText(); // Only add the imageView if the user has taken a picture if (mBinaryName != null) { mImageView = new ImageView(getContext()); Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); int screenWidth = display.getWidth(); int screenHeight = display.getHeight(); File f = new File(mInstanceFolder + "/" + mBinaryName); if (f.exists()) { Bitmap bmp = FileUtils.getBitmapScaledToDisplay(f, screenHeight, screenWidth); if (bmp == null) { mErrorTextView.setVisibility(View.VISIBLE); } mImageView.setImageBitmap(bmp); } else { mImageView.setImageBitmap(null); } mImageView.setPadding(10, 10, 10, 10); mImageView.setAdjustViewBounds(true); mImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent i = new Intent("android.intent.action.VIEW"); String[] projection = { "_id" }; Cursor c = getContext().getContentResolver() .query( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, "_data='" + mInstanceFolder + mBinaryName + "'", null, null); if (c.getCount() > 0) { c.moveToFirst(); String id = c.getString(c.getColumnIndex("_id")); Log.i( t, "setting view path to: " + Uri.withAppendedPath( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)); i.setDataAndType(Uri.withAppendedPath( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), "image/*"); try { getContext().startActivity(i); } catch (ActivityNotFoundException e) { Toast.makeText(getContext(), getContext().getString(R.string.activity_not_found, "view image"), Toast.LENGTH_SHORT); } } c.close(); } }); addView(mImageView); } } private void deleteMedia() { // get the file path and delete the file // There's only 1 in this case, but android 1.6 doesn't implement delete on // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI only on // android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI + a # String[] projection = { Images.ImageColumns._ID }; Cursor c = getContext().getContentResolver().query( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, "_data='" + mInstanceFolder + mBinaryName + "'", null, null); int del = 0; if (c.getCount() > 0) { c.moveToFirst(); String id = c.getString(c.getColumnIndex(Images.ImageColumns._ID)); Log.i( t, "attempting to delete: " + Uri.withAppendedPath( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)); del = getContext().getContentResolver().delete( Uri.withAppendedPath( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), null, null); } c.close(); // clean up variables mBinaryName = null; Log.i(t, "Deleted " + del + " rows from media content provider"); } @Override public void clearAnswer() { // remove the file deleteMedia(); mImageView.setImageBitmap(null); mErrorTextView.setVisibility(View.GONE); // reset buttons mCaptureButton.setText(getContext().getString(R.string.capture_image)); } @Override public IAnswerData getAnswer() { if (mBinaryName != null) { return new StringData(mBinaryName.toString()); } else { return null; } } private String getPathFromUri(Uri uri) { if (uri.toString().startsWith("file")) { return uri.toString().substring(6); } else { // find entry in content provider Cursor c = getContext().getContentResolver().query(uri, null, null, null, null); c.moveToFirst(); // get data path String colString = c.getString(c.getColumnIndex("_data")); c.close(); return colString; } } @Override public void setBinaryData(Object binaryuri) { // you are replacing an answer. delete the previous image using the // content provider. if (mBinaryName != null) { deleteMedia(); } String binarypath = getPathFromUri((Uri) binaryuri); File f = new File(binarypath); mBinaryName = f.getName(); Log.i(t, "Setting current answer to " + f.getName()); // BEGIN custom // Resize image (full sized images are too large for the system) Bitmap bmp = null; try { bmp = FileUtilsExtended.getBitmapResizedToStore(f, FileUtilsExtended.IMAGE_WIDGET_MAX_WIDTH, FileUtilsExtended.IMAGE_WIDGET_MAX_HEIGHT); FileOutputStream out = new FileOutputStream(new File(binarypath)); bmp.compress(Bitmap.CompressFormat.JPEG, FileUtilsExtended.IMAGE_WIDGET_QUALITY, out); bmp.recycle(); out.close(); } catch (FileNotFoundException e) { Log.e(Collect.LOGTAG, t + "failed to find file: " + e.toString()); e.printStackTrace(); } catch (IOException e) { Log.e(Collect.LOGTAG, t + "failed to close output stream: " + e.toString()); e.printStackTrace(); } catch (OutOfMemoryError e) { Log.e(Collect.LOGTAG, t + "out of memory while resizing image: " + e.toString()); e.printStackTrace(); // Cleanup if (bmp == null) System.gc(); else bmp.recycle(); deleteMedia(); if (mImageView != null) mImageView.setImageBitmap(null); // Show error dialog AlertDialog errorDialog = new AlertDialog.Builder(getContext()) .setIcon(R.drawable.ic_dialog_alert) .setTitle(getContext().getString(R.string.tf_unable_to_capture_image)) .setMessage(getContext().getString(R.string.tf_unable_to_resize_image_out_of_memory)) .setNeutralButton(getContext().getString(R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }) .create(); errorDialog.show(); } // END custom mWaitingForData = false; } @Override public void setFocus(Context context) { // Hide the soft keyboard if it's showing. InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.hideSoftInputFromWindow(this.getWindowToken(), 0); } @Override public boolean isWaitingForBinaryData() { return mWaitingForData; } @Override public void setOnLongClickListener(OnLongClickListener l) { mCaptureButton.setOnLongClickListener(l); mChooseButton.setOnLongClickListener(l); if (mImageView != null) { mImageView.setOnLongClickListener(l); } } @Override public void cancelLongPress() { super.cancelLongPress(); mCaptureButton.cancelLongPress(); mChooseButton.cancelLongPress(); if (mImageView != null) { mImageView.cancelLongPress(); } } }