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);