TestingCamera: Improve snapshot dialog, notify MediaScanner
- Fix bug with large snapshots not appearing
- Show extracted EXIF information
- Allow saving/viewing snapshots
- Notify MediaScanner about new videos and still images
- Clean up imports
Change-Id: Iea569da97b073a6a384f2d75439d49facf944b9a
diff --git a/apps/TestingCamera/res/layout/fragment_snapshot.xml b/apps/TestingCamera/res/layout/fragment_snapshot.xml
index faf141f..29fcc6e 100644
--- a/apps/TestingCamera/res/layout/fragment_snapshot.xml
+++ b/apps/TestingCamera/res/layout/fragment_snapshot.xml
@@ -1,30 +1,57 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:padding="10dp" >
- <ImageView
- android:id="@+id/snapshot_image"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginRight="10dp"
- android:layout_weight="4"
- android:contentDescription="@string/still_image_description" />
+ android:layout_height="0dp"
+ android:layout_weight="1" >
- <TextView
- android:id="@+id/snapshot_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="@string/snapshot_text_default"
- android:textColor="#FFF" />
+ <ImageView
+ android:id="@+id/snapshot_image"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:contentDescription="@string/still_image_description"
+ android:minWidth="320dp"
+ tools:ignore="NestedWeights" />
- <Button
- android:id="@+id/snapshot_ok"
+ <TextView
+ android:id="@+id/snapshot_text"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:minLines="4"
+ android:text="@string/snapshot_text_default"
+ android:textColor="#FFF" />
+ </LinearLayout>
+
+ <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/snapshot_ok_label" />
+ android:layout_gravity="center_horizontal" >
+
+ <Button
+ android:id="@+id/snapshot_ok"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/snapshot_ok_label" />
+
+ <Button
+ android:id="@+id/snapshot_save"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/snapshot_save_label" />
+
+ <Button
+ android:id="@+id/snapshot_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/snapshot_view_label" />
+ </LinearLayout>
</LinearLayout>
diff --git a/apps/TestingCamera/res/layout/main.xml b/apps/TestingCamera/res/layout/main.xml
index 4478379..eb8b404 100644
--- a/apps/TestingCamera/res/layout/main.xml
+++ b/apps/TestingCamera/res/layout/main.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-
-<!-- Copyright (C) 2012 The Android Open Source Project
+<!--
+ Copyright (C) 2012 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,36 +15,36 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<LinearLayout
android:id="@+id/preview_column"
- android:layout_width="0px"
+ android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="6"
android:orientation="vertical" >
<SurfaceView
- android:id="@+id/preview"
- android:layout_width="fill_parent"
- android:layout_height="0px"
- android:layout_weight="6"
- />
+ android:id="@+id/preview"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="6"
+ tools:ignore="NestedWeights" />
<TextView
android:id="@+id/log"
android:layout_width="fill_parent"
- android:layout_height="0px"
+ android:layout_height="0dp"
android:layout_weight="1.5"
android:freezesText="true" />
-
</LinearLayout>
<ScrollView
android:id="@+id/control_bar"
- android:layout_width="0px"
+ android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:fadingEdgeLength="100dp"
diff --git a/apps/TestingCamera/res/values/strings.xml b/apps/TestingCamera/res/values/strings.xml
index 5ce8262..00cccc0 100644
--- a/apps/TestingCamera/res/values/strings.xml
+++ b/apps/TestingCamera/res/values/strings.xml
@@ -24,7 +24,7 @@
<string name="preview_resolution_prompt">Preview size</string>
<string name="camera_selection_prompt">Active camera</string>
<string name="default_camera_entry">No cameras found</string>
- <string name="snapshot_text_default">Snapshot text</string>
+ <string name="snapshot_text_default">Save to view EXIF.</string>
<string name="snapshot_size_spinner_label">Still capture size</string>
<string name="record_on_label">Recording on</string>
<string name="record_off_label">Recording off</string>
@@ -37,4 +37,6 @@
<string name="af_mode_prompt">Autofocus mode</string>
<string name="trigger_autofocus">Trigger autofocus</string>
<string name="cancel_autofocus">Cancel Autofocus</string>
+ <string name="snapshot_save_label">Save</string>
+ <string name="snapshot_view_label">Save and view</string>
</resources>
diff --git a/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java b/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
index 9b13b95..d22f751 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
@@ -1,24 +1,40 @@
package com.android.testingcamera;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.ExifInterface;
+import android.media.MediaScannerConnection.OnScanCompletedListener;
+import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.app.DialogFragment;
+import android.content.Intent;
+import android.text.method.ScrollingMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
-public class SnapshotDialogFragment extends DialogFragment implements View.OnClickListener{
+public class SnapshotDialogFragment extends DialogFragment
+ implements OnScanCompletedListener{
private ImageView mInfoImage;
private TextView mInfoText;
private Button mOkButton;
+ private Button mSaveButton;
+ private Button mSaveAndViewButton;
- private Bitmap mImage;
- private String mImageString;
-
+ private byte[] mJpegImage;
+ private boolean mSaved = false;
+ private boolean mViewWhenReady = false;
+ private Uri mSavedUri = null;
public SnapshotDialogFragment() {
// Empty constructor required for DialogFragment
@@ -30,22 +46,208 @@
View view = inflater.inflate(R.layout.fragment_snapshot, container);
mOkButton = (Button) view.findViewById(R.id.snapshot_ok);
- mOkButton.setOnClickListener(this);
+ mOkButton.setOnClickListener(mOkButtonListener);
+
+ mSaveButton = (Button) view.findViewById(R.id.snapshot_save);
+ mSaveButton.setOnClickListener(mSaveButtonListener);
+
+ mSaveAndViewButton = (Button) view.findViewById(R.id.snapshot_view);
+ mSaveAndViewButton.setOnClickListener(mSaveAndViewButtonListener);
mInfoImage = (ImageView) view.findViewById(R.id.snapshot_image);
- mInfoImage.setImageBitmap(mImage);
mInfoText= (TextView) view.findViewById(R.id.snapshot_text);
- mInfoText.setText(mImageString);
+ mInfoText.setMovementMethod(new ScrollingMovementMethod());
+
+ if (mJpegImage != null) {
+ new AsyncTask<byte[], Integer, Bitmap>() {
+ @Override
+ protected Bitmap doInBackground(byte[]... params) {
+ byte[] jpegImage = params[0];
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(jpegImage, 0,
+ jpegImage.length, opts);
+ // Keep image at less than 1 MP.
+ if (opts.outWidth > 1024 || opts.outHeight > 1024) {
+ int scaleFactorX = opts.outWidth / 1024 + 1;
+ int scaleFactorY = opts.outHeight / 1024 + 1;
+ int scaleFactor = scaleFactorX > scaleFactorY ?
+ scaleFactorX : scaleFactorY;
+ opts.inSampleSize = scaleFactor;
+ }
+ opts.inJustDecodeBounds = false;
+ Bitmap img = BitmapFactory.decodeByteArray(jpegImage, 0,
+ jpegImage.length, opts);
+ return img;
+ }
+
+ protected void onPostExecute(Bitmap img) {
+ mInfoImage.setImageBitmap(img);
+ }
+ }.execute(mJpegImage);
+ }
getDialog().setTitle("Snapshot result");
return view;
}
- public void onClick(View v) {
- this.dismiss();
+ public OnClickListener mOkButtonListener = new OnClickListener() {
+ public void onClick(View v) {
+ dismiss();
+ }
+ };
+
+ public OnClickListener mSaveButtonListener = new OnClickListener() {
+ public void onClick(View v) {
+ saveFile();
+ }
+ };
+
+ public OnClickListener mSaveAndViewButtonListener = new OnClickListener() {
+ public void onClick(View v) {
+ saveFile();
+ viewFile();
+ }
+ };
+
+ public void updateImage(byte[] image) {
+ mJpegImage = image;
}
- public void updateImage(Bitmap image) {
- mImage = image;
+ private String getAttrib(ExifInterface exif, String tag) {
+ String attribute = exif.getAttribute(tag);
+ return (attribute == null) ? "???" : attribute;
+ }
+
+ private void saveFile() {
+ if (!mSaved) {
+ TestingCamera parent = (TestingCamera) getActivity();
+ parent.log("Saving image");
+
+ File targetFile = parent.getOutputMediaFile(TestingCamera.MEDIA_TYPE_IMAGE);
+ if (targetFile == null) {
+ parent.logE("Unable to create file name");
+ return;
+ }
+ parent.logIndent(1);
+ parent.log("File name: " + targetFile.toString());
+ try {
+ FileOutputStream out = new FileOutputStream(targetFile);
+ out.write(mJpegImage);
+ out.close();
+ mSaved = true;
+ parent.notifyMediaScannerOfFile(targetFile, this);
+ updateExif(targetFile);
+ } catch (IOException e) {
+ parent.logE("Unable to save file: " + e.getMessage());
+ }
+ parent.logIndent(-1);
+ }
+ }
+
+ private void updateExif(File targetFile) {
+ ((TestingCamera) getActivity()).log("Extracting EXIF");
+ try {
+ ExifInterface exif = new ExifInterface(targetFile.toString());
+
+ String aperture = getAttrib(exif, ExifInterface.TAG_APERTURE);
+
+ String dateTime = getAttrib(exif, ExifInterface.TAG_DATETIME);
+ String exposureTime = getAttrib(exif, ExifInterface.TAG_EXPOSURE_TIME);
+ int flash = exif.getAttributeInt(ExifInterface.TAG_FLASH, 0);
+ double focalLength = exif.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0);
+
+ double gpsAltitude = exif.getAltitude(Double.NaN);
+ String gpsDatestamp = getAttrib(exif, ExifInterface.TAG_GPS_DATESTAMP);
+ float[] gpsCoords = new float[2];
+ if (!exif.getLatLong(gpsCoords)) {
+ gpsCoords[0] = Float.NaN;
+ gpsCoords[1] = Float.NaN;
+ }
+ String gpsProcessingMethod = getAttrib(exif, ExifInterface.TAG_GPS_PROCESSING_METHOD);
+ String gpsTimestamp = getAttrib(exif, ExifInterface.TAG_GPS_TIMESTAMP);
+
+ int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
+ int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
+ String iso = getAttrib(exif, ExifInterface.TAG_ISO);
+ String make = getAttrib(exif, ExifInterface.TAG_MAKE);
+ String model = getAttrib(exif, ExifInterface.TAG_MODEL);
+ int orientationVal = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
+ int whiteBalance = exif.getAttributeInt(ExifInterface.TAG_WHITE_BALANCE, 0);
+
+ final String[] orientationStrings= new String[] {
+ "UNDEFINED",
+ "NORMAL",
+ "FLIP_HORIZONTAL",
+ "ROTATE_180",
+ "FLIP_VERTICAL",
+ "TRANSPOSE",
+ "ROTATE_90",
+ "TRANSVERSE",
+ "ROTATE_270"
+ };
+ if (orientationVal >= orientationStrings.length) {
+ orientationVal = 0;
+ }
+ String orientation = orientationStrings[orientationVal];
+
+ StringBuilder exifInfo = new StringBuilder();
+ exifInfo.append("EXIF information for ").
+ append(targetFile.toString()).append("\n\n");
+ exifInfo.append("Size: ").
+ append(width).append(" x ").append(height).append("\n");
+ exifInfo.append("Make: ").
+ append(make).append("\n");
+ exifInfo.append("Model: ").
+ append(model).append("\n");
+ exifInfo.append("Orientation: ").
+ append(orientation).append("\n");
+ exifInfo.append("Aperture: ").
+ append(aperture).append("\n");
+ exifInfo.append("Focal length: ").
+ append(focalLength).append("\n");
+ exifInfo.append("Exposure time: ").
+ append(exposureTime).append("\n");
+ exifInfo.append("ISO: ").
+ append(iso).append("\n");
+ exifInfo.append("Flash: ").
+ append(flash).append("\n");
+ exifInfo.append("White balance: ").
+ append(whiteBalance).append("\n");
+ exifInfo.append("Date/Time: ").
+ append(dateTime).append("\n");
+ exifInfo.append("GPS altitude: ").
+ append(gpsAltitude).append("\n");
+ exifInfo.append("GPS latitude: ").
+ append(gpsCoords[0]).append("\n");
+ exifInfo.append("GPS longitude: ").
+ append(gpsCoords[1]).append("\n");
+ exifInfo.append("GPS datestamp: ").
+ append(gpsDatestamp).append("\n");
+ exifInfo.append("GPS timestamp: ").
+ append(gpsTimestamp).append("\n");
+ exifInfo.append("GPS processing method: ").
+ append(gpsProcessingMethod).append("\n");
+ mInfoText.setText(exifInfo.toString());
+
+ } catch (IOException e) {
+ ((TestingCamera) getActivity()).logE("Unable to extract EXIF: " + e.getMessage());
+ }
+ }
+
+ private synchronized void viewFile() {
+ if (!mSaved) return;
+ if (mSavedUri != null) {
+ ((TestingCamera) getActivity()).log("Viewing file");
+ mViewWhenReady = false;
+ getActivity().startActivity(new Intent(Intent.ACTION_VIEW, mSavedUri));
+ } else {
+ mViewWhenReady = true;
+ }
+ }
+
+ public synchronized void onScanCompleted(String path, Uri uri) {
+ mSavedUri = uri;
+ if (mViewWhenReady) viewFile();
}
}
diff --git a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
index d3403c7..ceb0c4f 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
@@ -17,16 +17,16 @@
package com.android.testingcamera;
import android.app.Activity;
-import android.app.Dialog;
import android.app.FragmentManager;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
+import android.os.Handler;
import android.view.View;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -35,10 +35,8 @@
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
-import android.widget.ImageView;
import android.widget.LinearLayout.LayoutParams;
import android.widget.Spinner;
-import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.ToggleButton;
import android.text.Layout;
@@ -46,19 +44,15 @@
import android.util.Log;
import java.io.File;
-import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.TreeSet;
/**
* A simple test application for the camera API.
@@ -102,6 +96,7 @@
private int mCamcorderProfile = 0;
private MediaRecorder mRecorder;
+ private File mRecordingFile;
private static final int CAMERA_UNINITIALIZED = 0;
private static final int CAMERA_OPEN = 1;
@@ -412,7 +407,7 @@
if (mState == CAMERA_PREVIEW) {
startRecording();
} else if (mState == CAMERA_RECORD) {
- stopRecording();
+ stopRecording(false);
} else {
logE("Can't toggle recording in current state!");
}
@@ -444,8 +439,7 @@
FragmentManager fm = getFragmentManager();
SnapshotDialogFragment snapshotDialog = new SnapshotDialogFragment();
- Bitmap img = BitmapFactory.decodeByteArray(data, 0, data.length);
- snapshotDialog.updateImage(img);
+ snapshotDialog.updateImage(data);
snapshotDialog.show(fm, "snapshot_dialog_fragment");
mPreviewToggle.setEnabled(true);
@@ -507,6 +501,8 @@
log("Starting preview" );
mCamera.startPreview();
mState = CAMERA_PREVIEW;
+ } else {
+ mState = CAMERA_OPEN;
}
logIndent(-1);
}
@@ -642,7 +638,7 @@
static final int MEDIA_TYPE_IMAGE = 0;
static final int MEDIA_TYPE_VIDEO = 1;
- private File getOutputMediaFile(int type){
+ File getOutputMediaFile(int type){
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
@@ -652,7 +648,7 @@
}
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
- Environment.DIRECTORY_PICTURES), "TestingCamera");
+ Environment.DIRECTORY_DCIM), "TestingCamera");
// This location works best if you want the created images to be shared
// between applications and persist after your app has been uninstalled.
@@ -680,11 +676,42 @@
return mediaFile;
}
+ void notifyMediaScannerOfFile(File newFile,
+ final MediaScannerConnection.OnScanCompletedListener listener) {
+ final Handler h = new Handler();
+ MediaScannerConnection.scanFile(this,
+ new String[] { newFile.toString() },
+ null,
+ new MediaScannerConnection.OnScanCompletedListener() {
+ public void onScanCompleted(final String path, final Uri uri) {
+ h.post(new Runnable() {
+ public void run() {
+ log("MediaScanner notified: " +
+ path + " -> " + uri);
+ if (listener != null)
+ listener.onScanCompleted(path, uri);
+ }
+ });
+ }
+ });
+ }
+
+ private void deleteFile(File badFile) {
+ if (badFile.exists()) {
+ boolean success = badFile.delete();
+ if (success) log("Deleted file " + badFile.toString());
+ else log("Unable to delete file " + badFile.toString());
+ }
+ }
+
private void startRecording() {
log("Starting recording");
logIndent(1);
log("Configuring MediaRecoder");
mCamera.unlock();
+ if (mRecorder != null) {
+ mRecorder.release();
+ }
mRecorder = new MediaRecorder();
mRecorder.setOnErrorListener(mRecordingErrorListener);
mRecorder.setOnInfoListener(mRecordingInfoListener);
@@ -713,6 +740,7 @@
mRecorder.start();
mState = CAMERA_RECORD;
log("Recording active");
+ mRecordingFile = outputFile;
} catch (Exception e) {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
@@ -730,7 +758,7 @@
logE("MediaRecorder reports error: " + what + ", extra "
+ extra);
if (mState == CAMERA_RECORD) {
- stopRecording();
+ stopRecording(true);
}
}
};
@@ -743,14 +771,18 @@
}
};
- private void stopRecording() {
+ private void stopRecording(boolean error) {
log("Stopping recording");
if (mRecorder != null) {
mRecorder.stop();
mCamera.lock();
mState = CAMERA_PREVIEW;
- mRecorder.release();
- mRecorder = null;
+ if (!error) {
+ notifyMediaScannerOfFile(mRecordingFile, null);
+ } else {
+ deleteFile(mRecordingFile);
+ }
+ mRecordingFile = null;
} else {
logE("Recorder is unexpectedly null!");
}
@@ -759,7 +791,7 @@
private int mLogIndentLevel = 0;
private String mLogIndent = "\t";
/** Increment or decrement log indentation level */
- private void logIndent(int delta) {
+ synchronized void logIndent(int delta) {
mLogIndentLevel += delta;
if (mLogIndentLevel < 0) mLogIndentLevel = 0;
char[] mLogIndentArray = new char[mLogIndentLevel + 1];
@@ -771,17 +803,17 @@
SimpleDateFormat mDateFormatter = new SimpleDateFormat("HH:mm:ss.SSS");
/** Log both to log text view and to device logcat */
- private void log(String logLine) {
+ void log(String logLine) {
Log.d(TAG, logLine);
logAndScrollToBottom(logLine, mLogIndent);
}
- private void logE(String logLine) {
+ void logE(String logLine) {
Log.e(TAG, logLine);
logAndScrollToBottom(logLine, mLogIndent + "!!! ");
}
- private void logAndScrollToBottom(String logLine, String logIndent) {
+ synchronized private void logAndScrollToBottom(String logLine, String logIndent) {
StringBuffer logEntry = new StringBuffer(32);
logEntry.append("\n").append(mDateFormatter.format(new Date())).append(logIndent);
logEntry.append(logLine);