summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml11
-rw-r--r--jni/feature_stab/src/dbreg/dbreg.cpp2
-rw-r--r--res/layout/camera_control.xml1
-rw-r--r--res/layout/pano_control.xml32
-rw-r--r--res/layout/pano_views.xml43
-rw-r--r--res/layout/panorama.xml25
-rw-r--r--res/values/strings.xml3
-rw-r--r--src/com/android/camera/Storage.java2
-rw-r--r--src/com/android/camera/panorama/CaptureView.java131
-rw-r--r--src/com/android/camera/panorama/PanoUtil.java86
-rw-r--r--src/com/android/camera/panorama/PanoramaActivity.java268
-rw-r--r--src/com/android/camera/panorama/Preview.java464
12 files changed, 1060 insertions, 8 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 02bcba3..5e13b60 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,6 +20,7 @@
<application android:icon="@mipmap/ic_launcher_camera"
android:label="@string/camera_label"
android:taskAffinity=""
+ android:theme="@style/ThemeCamera"
android:hardwareAccelerated="true">
<receiver android:name="com.android.camera.CameraButtonIntentReceiver">
<intent-filter>
@@ -28,10 +29,8 @@
</receiver>
<activity android:name="com.android.camera.Camera"
android:configChanges="orientation|keyboardHidden"
- android:theme="@style/ThemeCamera"
android:screenOrientation="landscape"
android:clearTaskOnLaunch="true"
- android:taskAffinity="android.task.camera"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -51,10 +50,8 @@
android:label="@string/video_camera_label"
android:configChanges="orientation|keyboardHidden"
android:icon="@mipmap/ic_launcher_video_camera"
- android:theme="@style/ThemeCamera"
android:screenOrientation="landscape"
android:clearTaskOnLaunch="true"
- android:taskAffinity="android.task.camcorder"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
<intent-filter>
<action android:name="android.media.action.VIDEO_CAMERA" />
@@ -65,6 +62,12 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <activity android:name="com.android.camera.panorama.PanoramaActivity"
+ android:configChanges="orientation|keyboardHidden"
+ android:screenOrientation="landscape"
+ android:clearTaskOnLaunch="true"
+ android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+ </activity>
</application>
</manifest>
diff --git a/jni/feature_stab/src/dbreg/dbreg.cpp b/jni/feature_stab/src/dbreg/dbreg.cpp
index b87cbbd..fb42838 100644
--- a/jni/feature_stab/src/dbreg/dbreg.cpp
+++ b/jni/feature_stab/src/dbreg/dbreg.cpp
@@ -763,7 +763,6 @@ void db_FrameToReferenceRegistration::GenerateQuarterResImage(const unsigned cha
if ( (smooth_val < 0) || (smooth_val > 255))
{
return;
- //throw(std::exception());
}
}
@@ -787,7 +786,6 @@ void db_FrameToReferenceRegistration::GenerateQuarterResImage(const unsigned cha
if ( (smooth_val < 0) || (smooth_val > 255))
{
return;
- //throw(std::exception());
}
}
diff --git a/res/layout/camera_control.xml b/res/layout/camera_control.xml
index a7dee7d..f2790fb 100644
--- a/res/layout/camera_control.xml
+++ b/res/layout/camera_control.xml
@@ -15,7 +15,6 @@
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="76dp"
android:paddingTop="13dp"
diff --git a/res/layout/pano_control.xml b/res/layout/pano_control.xml
new file mode 100644
index 0000000..530ffc6
--- /dev/null
+++ b/res/layout/pano_control.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+ 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="76dp"
+ android:paddingTop="13dp">
+
+ <include layout="@layout/review_thumbnail"/>
+
+ <com.android.camera.ShutterButton android:id="@+id/pano_shutter_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/btn_ic_camera_shutter"
+ android:background="@drawable/btn_shutter"/>
+</RelativeLayout>
diff --git a/res/layout/pano_views.xml b/res/layout/pano_views.xml
new file mode 100644
index 0000000..ef7738d
--- /dev/null
+++ b/res/layout/pano_views.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+ 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:camera="http://schemas.android.com/apk/res/com.android.camera"
+ android:id="@+id/pano_views_layout"
+ android:gravity="center"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:layout_weight="1"
+ android:layout_marginLeft="2dp">
+ <FrameLayout android:id="@+id/pano_preview_layout"
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="720dp">
+ <com.android.camera.panorama.Preview
+ android:id="@+id/pano_preview"
+ android:layout_gravity="center"
+ android:layout_width="240dp"
+ android:layout_height="180dp"/>
+ <com.android.camera.panorama.CaptureView android:id="@+id/pano_capture_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </FrameLayout>
+ <ImageView android:id="@+id/pano_reviewarea"
+ android:scaleType="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</FrameLayout>
+
diff --git a/res/layout/panorama.xml b/res/layout/panorama.xml
new file mode 100644
index 0000000..b92d81d
--- /dev/null
+++ b/res/layout/panorama.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+ 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:camera="http://schemas.android.com/apk/res/com.android.camera"
+ android:id="@+id/panorama_root"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <include layout="@layout/pano_views"/>
+ <include layout="@layout/pano_control"/>
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7713e51..8772f3a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -271,6 +271,9 @@
<!-- Video Camera format string for new video files. Passed to java.text.SimpleDateFormat. -->
<string name="video_file_name_format" translatable="false">"'VID'_yyyyMMdd_HHmmss"</string>
+ <!-- Filename prefix for panorama output. -->
+ <string name="pano_file_name_format" translatable="false">"'PANO'_yyyyMMdd_HHmmss"</string>
+
<!-- The messsage shown when video record reaches size limit. -->
<string name="video_reach_size_limit">Size limit reached.</string>
diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java
index 86fb443..e7c1553 100644
--- a/src/com/android/camera/Storage.java
+++ b/src/com/android/camera/Storage.java
@@ -29,7 +29,7 @@ import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
-class Storage {
+public class Storage {
private static final String TAG = "CameraStorage";
public static final String DCIM =
diff --git a/src/com/android/camera/panorama/CaptureView.java b/src/com/android/camera/panorama/CaptureView.java
new file mode 100644
index 0000000..3351527
--- /dev/null
+++ b/src/com/android/camera/panorama/CaptureView.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2011 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.
+ * 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 com.android.camera.panorama;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+class CaptureView extends View {
+ private static final String TAG = "CaptureView";
+
+ private Canvas mCanvas;
+ private Bitmap mCanvasBitmap;
+ private String mStatusText = "";
+ private int mStartAngle = 0;
+ private int mSweepAngle = 0;
+ private int mWidth;
+ private int mHeight;
+ private Bitmap mBitmap = null;
+ private Matrix mM = null;
+ private Matrix mMLast = null;
+ private final Paint mPaint = new Paint();
+ // Origin of the coordinate for appending a new alpha bitmap.
+ // mCanvasBitmap is 2000x2000, but the origin is set to (800, 800).
+ // All the alpha bitmaps grow from this origin.
+ float mAlphaOriginX;
+ float mAlphaOriginY;
+
+
+ public CaptureView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mM = new Matrix();
+ mMLast = new Matrix();
+ mMLast.reset();
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(Color.RED);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextSize(40);
+ mPaint.setTypeface(Typeface.create((Typeface) null, Typeface.BOLD));
+ mPaint.setTextAlign(Align.CENTER);
+ }
+
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (mCanvasBitmap != null) {
+ mCanvasBitmap.recycle();
+ }
+ Log.v(TAG, "onSizeChanged: W = " + w + ", H = " + h);
+ // TODO: 2000x2000 is a temporary setting from SRI's code. Should be fixed once the code is
+ // refactored.
+ mCanvasBitmap = Bitmap.createBitmap(2000, 2000, Bitmap.Config.ARGB_8888);
+ mCanvas = new Canvas();
+ mCanvas.setBitmap(mCanvasBitmap);
+ mAlphaOriginX = mCanvasBitmap.getWidth() * 0.4f;
+ mAlphaOriginY = mCanvasBitmap.getHeight() * 0.4f;
+ }
+
+ public void destroy() {
+ if (mCanvasBitmap != null) {
+ mCanvasBitmap.recycle();
+ }
+ }
+
+ public void setStartAngle(int angle) {
+ mStartAngle = angle;
+ }
+
+ public void setSweepAngle(int angle) {
+ mSweepAngle = angle;
+ }
+
+ public void setStatusText(String text) {
+ mStatusText = text;
+ }
+
+ public void setBitmap(Bitmap bitmap, Matrix m) {
+ mBitmap = bitmap;
+ mM = m;
+ }
+
+ public void setBitmap(Bitmap bitmap) {
+ mBitmap = bitmap;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mWidth = getWidth();
+ mHeight = getHeight();
+
+ // Draw bitmaps according to the calculated panorama transformation.
+ if (mBitmap != null) {
+ mM.postTranslate(mAlphaOriginX, mAlphaOriginY);
+ mCanvas.drawBitmap(mBitmap, mM, mPaint);
+
+ Matrix mInverse = mM;
+ mM.invert(mInverse);
+ mInverse.postTranslate(mWidth / 2 - mBitmap.getWidth() / 2,
+ mHeight / 2 - mBitmap.getHeight() / 2);
+
+ canvas.drawBitmap(mCanvasBitmap, mInverse, mPaint);
+
+ RectF rect = new RectF(mWidth / 2 - 100, 3 * mHeight / 4,
+ mWidth / 2 + 100, 3 * mHeight / 4 + 200);
+ canvas.drawText(mStatusText, mWidth / 2, mHeight / 2, mPaint);
+ canvas.drawArc(rect, -90 + mStartAngle, mSweepAngle, true, mPaint);
+ canvas.drawArc(rect, -90 - mStartAngle, mSweepAngle > 0 ? 2 : 0, true, mPaint);
+ }
+ }
+}
diff --git a/src/com/android/camera/panorama/PanoUtil.java b/src/com/android/camera/panorama/PanoUtil.java
new file mode 100644
index 0000000..ef778a3
--- /dev/null
+++ b/src/com/android/camera/panorama/PanoUtil.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.
+ * 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 com.android.camera.panorama;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class PanoUtil {
+ public static String createName(String format, long dateTaken) {
+ Date date = new Date(dateTaken);
+ SimpleDateFormat dateFormat = new SimpleDateFormat(format);
+ return dateFormat.format(date);
+ }
+
+ // TODO: Add comments about the range of these two arguments.
+ public static double calculateDifferenceBetweenAngles(double firstAngle,
+ double secondAngle) {
+ double difference1 = (secondAngle - firstAngle) % 360;
+ if (difference1 < 0) {
+ difference1 += 360;
+ }
+
+ double difference2 = (firstAngle - secondAngle) % 360;
+ if (difference2 < 0) {
+ difference2 += 360;
+ }
+
+ return Math.min(difference1, difference2);
+ }
+
+ public static void decodeYUV420SPQuarterRes(int[] rgb, byte[] yuv420sp, int width, int height) {
+ final int frameSize = width * height;
+
+ for (int j = 0, ypd = 0; j < height; j += 4) {
+ int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
+ for (int i = 0; i < width; i += 4, ypd++) {
+ int y = (0xff & ((int) yuv420sp[j * width + i])) - 16;
+ if (y < 0) {
+ y = 0;
+ }
+ if ((i & 1) == 0) {
+ v = (0xff & yuv420sp[uvp++]) - 128;
+ u = (0xff & yuv420sp[uvp++]) - 128;
+ uvp += 2; // Skip the UV values for the 4 pixels skipped in between
+ }
+ int y1192 = 1192 * y;
+ int r = (y1192 + 1634 * v);
+ int g = (y1192 - 833 * v - 400 * u);
+ int b = (y1192 + 2066 * u);
+
+ if (r < 0) {
+ r = 0;
+ } else if (r > 262143) {
+ r = 262143;
+ }
+ if (g < 0) {
+ g = 0;
+ } else if (g > 262143) {
+ g = 262143;
+ }
+ if (b < 0) {
+ b = 0;
+ } else if (b > 262143) {
+ b = 262143;
+ }
+
+ rgb[ypd] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) |
+ ((b >> 10) & 0xff);
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/panorama/PanoramaActivity.java b/src/com/android/camera/panorama/PanoramaActivity.java
new file mode 100644
index 0000000..5d18525
--- /dev/null
+++ b/src/com/android/camera/panorama/PanoramaActivity.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2011 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.
+ * 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 com.android.camera.panorama;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import com.android.camera.CameraDisabledException;
+import com.android.camera.CameraHardwareException;
+import com.android.camera.CameraHolder;
+import com.android.camera.R;
+import com.android.camera.ShutterButton;
+import com.android.camera.Util;
+
+import java.util.List;
+
+public class PanoramaActivity extends Activity {
+ public static final int DEFAULT_SWEEP_ANGLE = 60;
+ public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
+ public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
+
+ private static final String TAG = "PanoramaActivity";
+ private static final float NS2S = 1.0f / 1000000000.0f; // TODO: commit for this constant.
+
+ private Preview mPreview;
+ private ImageView mReview;
+ private CaptureView mCaptureView;
+ private ShutterButton mShutterButton;
+ private int mPreviewWidth;
+ private int mPreviewHeight;
+ private android.hardware.Camera mCameraDevice;
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ createContentView();
+
+ mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
+
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ if (mSensor == null) {
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
+ }
+ }
+
+ private void setupCamera() {
+ openCamera();
+ Parameters parameters = mCameraDevice.getParameters();
+ setupCaptureParams(parameters);
+ configureCamera(parameters);
+ }
+
+ private void openCamera() {
+ try {
+ mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
+ } catch (CameraHardwareException e) {
+ Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
+ return;
+ } catch (CameraDisabledException e) {
+ Util.showErrorAndFinish(this, R.string.camera_disabled);
+ return;
+ }
+ }
+
+ private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
+ boolean needSmaller) {
+ int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
+ boolean hasFound = false;
+ for (Size size: supportedSizes) {
+ int h = size.height;
+ int w = size.width;
+ // we only want 4:3 format.
+ int d = DEFAULT_CAPTURE_PIXELS - h * w;
+ if (needSmaller && d < 0) { // no bigger preview than 960x720.
+ continue;
+ }
+ if (need4To3 && (h * 4 != w * 3)) {
+ continue;
+ }
+ d = Math.abs(d);
+ if (d < pixelsDiff) {
+ mPreviewWidth = w;
+ mPreviewHeight = h;
+ pixelsDiff = d;
+ hasFound = true;
+ }
+ }
+ return hasFound;
+ }
+
+ private void setupCaptureParams(Parameters parameters) {
+ List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
+ if (!findBestPreviewSize(supportedSizes, true, true)) {
+ Log.w(TAG, "No 4:3 ratio preview size supported.");
+ if (!findBestPreviewSize(supportedSizes, false, true)) {
+ Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
+ findBestPreviewSize(supportedSizes, false, false);
+ }
+ }
+ Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
+ parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
+
+ List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
+ int last = frameRates.size() - 1;
+ int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
+ int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
+ parameters.setPreviewFpsRange(minFps, maxFps);
+ Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
+ }
+
+ public int getPreviewBufSize() {
+ PixelFormat pixelInfo = new PixelFormat();
+ PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
+ // TODO: remove this extra 32 byte after the driver bug is fixed.
+ return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
+ }
+
+ private void configureCamera(Parameters parameters) {
+ mCameraDevice.setParameters(parameters);
+
+ int bufSize = getPreviewBufSize();
+ Log.v(TAG, "BufSize = " + bufSize);
+ for (int i = 0; i < 10; i++) {
+ try {
+ mCameraDevice.addCallbackBuffer(new byte[bufSize]);
+ } catch (OutOfMemoryError e) {
+ Log.v(TAG, "Buffer allocation failed: buffer " + i);
+ break;
+ }
+ }
+ }
+
+ private void createContentView() {
+ setContentView(R.layout.panorama);
+
+ mPreview = (Preview) findViewById(R.id.pano_preview);
+ mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
+ mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
+ mCaptureView.setVisibility(View.INVISIBLE);
+
+ mReview = (ImageView) findViewById(R.id.pano_reviewarea);
+ mReview.setVisibility(View.INVISIBLE);
+
+ mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button);
+ mShutterButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mPreview.setCaptureStarted(DEFAULT_SWEEP_ANGLE, DEFAULT_BLEND_MODE);
+ }
+ });
+ }
+
+ public void showResultingMosaic(String uri) {
+ Uri parsed = Uri.parse(uri);
+ mReview.setImageURI(parsed);
+ mReview.setVisibility(View.VISIBLE);
+ mPreview.setVisibility(View.INVISIBLE);
+ mCaptureView.setVisibility(View.INVISIBLE);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mPreview.onPause();
+ mSensorManager.unregisterListener(mListener);
+ releaseCamera();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ /*
+ * It is not necessary to get accelerometer events at a very high
+ * rate, by using a slower rate (SENSOR_DELAY_UI), we get an
+ * automatic low-pass filter, which "extracts" the gravity component
+ * of the acceleration. As an added benefit, we use less power and
+ * CPU resources.
+ */
+ mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
+
+ setupCamera();
+ mPreview.setCameraDevice(mCameraDevice);
+ mCameraDevice.startPreview();
+ }
+
+ private void releaseCamera() {
+ if (mCameraDevice != null){
+ CameraHolder.instance().release();
+ mCameraDevice = null;
+ }
+ }
+
+ private final SensorEventListener mListener = new SensorEventListener() {
+ private float mCompassCurrX; // degrees
+ private float mCompassCurrY; // degrees
+ private float mTimestamp;
+
+ public void onSensorChanged(SensorEvent event) {
+
+ if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
+ if (mTimestamp != 0) {
+ final float dT = (event.timestamp - mTimestamp) * NS2S;
+ mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
+ mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
+ }
+ mTimestamp = event.timestamp;
+
+ } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
+ mCompassCurrX = event.values[0];
+ mCompassCurrY = event.values[1];
+ }
+
+ if (mPreview != null) {
+ mPreview.updateCompassValue(mCompassCurrX, mCompassCurrY);
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+ };
+
+ public int getPreviewFrameWidth() {
+ return mPreviewWidth;
+ }
+
+ public int getPreviewFrameHeight() {
+ return mPreviewHeight;
+ }
+
+ public CaptureView getCaptureView() {
+ return mCaptureView;
+ }
+}
diff --git a/src/com/android/camera/panorama/Preview.java b/src/com/android/camera/panorama/Preview.java
new file mode 100644
index 0000000..2d5ddd2
--- /dev/null
+++ b/src/com/android/camera/panorama/Preview.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2011 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.
+ * 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 com.android.camera.panorama;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.hardware.Camera;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+
+import com.android.camera.R;
+import com.android.camera.Storage;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+
+class Preview extends SurfaceView implements SurfaceHolder.Callback,
+ Camera.PreviewCallback {
+ private static final String TAG = "Preview";
+ private static final boolean LOGV = true;
+ private static final int NUM_FRAMES_IN_BUFFER = 2;
+ private static final int MAX_NUMBER_OF_FRAMES = 100;
+ private static final int DOWN_SAMPLE_SIZE = 4;
+
+ private final Object mLockFillIn = new Object(); // Object used for synchronization of mFillIn
+ private final byte[][] mFrames = new byte[NUM_FRAMES_IN_BUFFER][]; // Space for N frames
+ private final long [] mFrameTimestamp = new long[NUM_FRAMES_IN_BUFFER];
+
+ private PanoramaActivity mActivity;
+ private Mosaic mMosaicer;
+ private LowResFrameProcessor mLowResProcessor = null;
+ private SurfaceHolder mHolder;
+
+ private android.hardware.Camera mCameraDevice;
+
+ private Bitmap mLRBitmapAlpha = null;
+ private Matrix mTransformationMatrix = null;
+
+ private int mFillIn = 0;
+ private long mLastProcessedFrameTimestamp = 0;
+ private int mTotalFrameCount = 0;
+ private int[] mColors = null;
+
+ private int mPreviewWidth;
+ private int mPreviewHeight;
+
+ private float mTranslationLastX;
+ private float mTranslationLastY;
+ private float mTranslationRate;
+
+ private ScannerClient mScannerClient;
+
+ private String mCurrentImagePath = null;
+ private long mTimeTaken;
+
+ // Need handler for callbacks to the UI thread
+ private final Handler mHandler = new Handler();
+
+ // Create runnable for posting
+ private final Runnable mUpdateResults = new Runnable() {
+ public void run() {
+ mActivity.showResultingMosaic("file://" + mCurrentImagePath);
+ mScannerClient.scanPath(mCurrentImagePath);
+ }
+ };
+
+ public Preview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mActivity = (PanoramaActivity) getContext();
+
+ mMosaicer = new Mosaic();
+ mScannerClient = new ScannerClient(getContext());
+
+ // Install a SurfaceHolder.Callback so we get notified when the
+ // underlying surface is created and destroyed.
+ mHolder = getHolder();
+ mHolder.addCallback(this);
+ mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ }
+
+ private class LowResFrameProcessor {
+ private CaptureView mCaptureView;
+ int mLastProcessFrameIdx = -1;
+ int mCurrProcessFrameIdx = -1;
+ boolean mRun = true;
+
+ private float mCompassValueX;
+ private float mCompassValueY;
+ private float mCompassValueXStart;
+ private float mCompassValueYStart;
+ private int mCompassThreshold;
+ private int mTraversedAngleX;
+ private int mTraversedAngleY;
+
+
+ public LowResFrameProcessor(int sweepAngle, CaptureView overlayer) {
+ mCompassThreshold = sweepAngle;
+ mCaptureView = overlayer;
+ }
+
+ // Processes the last filled image frame through the mosaicer and
+ // updates the UI to show progress.
+ // When done, processes and displays the final mosaic.
+ public void runEachFrame() {
+ mCurrProcessFrameIdx = getLastFilledIn();
+
+ // Check that we are trying to process a frame different from the
+ // last one processed (useful if this class was running asynchronously)
+ if (mCurrProcessFrameIdx != mLastProcessFrameIdx) {
+ mLastProcessFrameIdx = mCurrProcessFrameIdx;
+
+ if (LOGV) Log.v(TAG, "Processing: [" + mCurrProcessFrameIdx + "]");
+
+ // Access the image data and the timestamp associated with it...
+ byte[] data = mFrames[mCurrProcessFrameIdx];
+ long timestamp = mFrameTimestamp[mCurrProcessFrameIdx];
+
+ // Keep track of what compass bearing we started at...
+ if (mTotalFrameCount == 0) { // First frame
+ mCompassValueXStart = mCompassValueX;
+ mCompassValueYStart = mCompassValueY;
+ }
+
+ // By what angle has the camera moved since start of capture?
+ mTraversedAngleX = (int) PanoUtil.calculateDifferenceBetweenAngles(
+ mCompassValueX, mCompassValueXStart);
+ mTraversedAngleY = (int) PanoUtil.calculateDifferenceBetweenAngles(
+ mCompassValueY, mCompassValueYStart);
+
+ if (mTotalFrameCount <= MAX_NUMBER_OF_FRAMES
+ && mTraversedAngleX < mCompassThreshold
+ && mTraversedAngleY < mCompassThreshold) {
+ // If we are still collecting new frames for the current mosaic,
+ // process the new frame.
+ processFrame(data, timestamp);
+
+ // Publish progress of the ongoing processing
+ publishProgress(0);
+ } else {
+ // Publish progress that we are done with capture
+ publishProgress(1);
+
+ // Background-process the final blending of the mosaic so
+ // that the UI is not blocked.
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ generateAndStoreFinalMosaic(false);
+ }
+ };
+ t.start();
+
+ mRun = false;
+ }
+ }
+ }
+
+ // Sets the screen layout before starting each fresh capture.
+ protected void onPreExecute() {
+ if (mTotalFrameCount == 0) {
+ mCaptureView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ // Updates the GUI with ongoing updates if values[0]==0 and
+ // with the constructed mosaic for values[0]==1.
+ public void publishProgress(Integer... values) {
+ long t1 = System.currentTimeMillis();
+
+ if (values[0] == 0) { // Ongoing
+ // This updates the real-time mosaic display with the current image frame and the
+ // transformation matrix to warp it by.
+ mCaptureView.setBitmap(mLRBitmapAlpha, mTransformationMatrix);
+
+ // Update the sweep-angle sector display and show "SLOW DOWN" message if the user
+ // is moving the camera too fast
+ if (mTranslationRate > 150) {
+ // TODO: remove the text and draw implications according to the UI spec.
+ mCaptureView.setStatusText("S L O W D O W N");
+ mCaptureView.setSweepAngle(
+ Math.max(mTraversedAngleX, mTraversedAngleY) + 1);
+ mCaptureView.invalidate();
+ } else {
+ mCaptureView.setStatusText("");
+ mCaptureView.setSweepAngle(
+ Math.max(mTraversedAngleX, mTraversedAngleY) + 1);
+ mCaptureView.invalidate();
+ }
+ } else { // Done
+ setVisibility(View.INVISIBLE);
+ mCaptureView.setVisibility(View.INVISIBLE);
+ mCaptureView.setBitmap(null);
+ mCaptureView.setStatusText("");
+ mCaptureView.setSweepAngle(0);
+ mCaptureView.invalidate();
+ }
+
+ long t2 = System.currentTimeMillis();
+ }
+
+ public void updateCompassValue(float valueX, float valueY) {
+ mCompassValueX = valueX;
+ mCompassValueY = valueY;
+ }
+ }
+
+ public void updateCompassValue(float valueX, float valueY) {
+ if (mLowResProcessor != null) {
+ mLowResProcessor.updateCompassValue(valueX, valueY);
+ }
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ }
+
+ private void setupMosaicer() {
+ mPreviewWidth = mActivity.getPreviewFrameWidth();
+ mPreviewHeight = mActivity.getPreviewFrameHeight();
+
+ mMosaicer.setSourceImageDimensions(mPreviewWidth, mPreviewHeight);
+
+ mColors = new int[(mActivity.getPreviewFrameWidth() / DOWN_SAMPLE_SIZE)
+ * (mActivity.getPreviewFrameHeight() / DOWN_SAMPLE_SIZE)];
+ mLRBitmapAlpha = Bitmap.createBitmap((mPreviewWidth / DOWN_SAMPLE_SIZE),
+ (mPreviewHeight / DOWN_SAMPLE_SIZE), Config.ARGB_8888);
+ mTransformationMatrix = new Matrix();
+
+ int bufSize = mActivity.getPreviewBufSize();
+ for (int i = 0; i < NUM_FRAMES_IN_BUFFER; i++) {
+ mFrames[i] = new byte[bufSize];
+ }
+ }
+
+ public void setCameraDevice(android.hardware.Camera camera) {
+ setupMosaicer();
+
+ mCameraDevice = camera;
+ // Preview callback used whenever new viewfinder frame is available
+ mCameraDevice.setPreviewCallbackWithBuffer(this);
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ try {
+ mCameraDevice.setPreviewDisplay(holder);
+ } catch (Throwable ex) {
+ throw new RuntimeException("setPreviewDisplay failed", ex);
+ }
+ }
+
+ public void onPreviewFrame(final byte[] data, Camera camera) {
+ long t1 = System.currentTimeMillis();
+ synchronized (mLockFillIn) {
+ mFrameTimestamp[mFillIn] = t1;
+ System.arraycopy(data, 0, mFrames[mFillIn], 0, data.length);
+ }
+ incrementFillIn();
+
+ if (mLowResProcessor != null && mLowResProcessor.mRun) {
+ mLowResProcessor.runEachFrame();
+ }
+
+ // The returned buffer needs be added back to callback buffer again.
+ if (mCameraDevice != null) {
+ mCameraDevice.addCallbackBuffer(data);
+ }
+ }
+
+ public void processFrame(final byte[] data, long now) {
+ float deltaTime = (float) (now - mLastProcessedFrameTimestamp) / 1000.0f;
+ mLastProcessedFrameTimestamp = now;
+
+ long t1 = System.currentTimeMillis();
+
+ float[] frameData = mMosaicer.setSourceImage(data);
+
+ mTotalFrameCount = (int) frameData[9];
+ float translationCurrX = frameData[2];
+ float translationCurrY = frameData[5];
+
+ long t2 = System.currentTimeMillis();
+
+ Log.v(TAG, "[ " + deltaTime + " ] AddFrame: " + (t2 - t1));
+
+ t1 = System.currentTimeMillis();
+ mTransformationMatrix.setValues(frameData);
+
+ int outw = mPreviewWidth / DOWN_SAMPLE_SIZE;
+ int outh = mPreviewHeight / DOWN_SAMPLE_SIZE;
+
+ PanoUtil.decodeYUV420SPQuarterRes(mColors, data, mPreviewWidth, mPreviewHeight);
+
+ mLRBitmapAlpha.setPixels(mColors, 0, outw, 0, 0, outw, outh);
+
+ t2 = System.currentTimeMillis();
+ Log.v(TAG, "GenerateLowResBitmap: " + (t2 - t1));
+
+ mTranslationRate = Math.max(Math.abs(translationCurrX - mTranslationLastX),
+ Math.abs(translationCurrY - mTranslationLastY)) / deltaTime;
+ mTranslationLastX = translationCurrX;
+ mTranslationLastY = translationCurrY;
+ }
+
+ public void generateAndStoreFinalMosaic(boolean highRes) {
+ long t1 = System.currentTimeMillis();
+
+ mMosaicer.createMosaic(highRes);
+
+ mCurrentImagePath = Storage.DIRECTORY + "/" + PanoUtil.createName(
+ mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
+
+ if (highRes) {
+ mCurrentImagePath += "_HR.jpg";
+ } else {
+ mCurrentImagePath += "_LR.jpg";
+ }
+
+ long t2 = System.currentTimeMillis();
+ long dur = (t2 - t1) / 1000;
+
+ try {
+ File mosDirectory = new File(Storage.DIRECTORY);
+ // have the object build the directory structure, if needed.
+ mosDirectory.mkdirs();
+
+ byte[] imageData = mMosaicer.getFinalMosaicNV21();
+ int len = imageData.length - 8;
+
+ int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
+ + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
+ int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
+ + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
+ Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
+
+ YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
+ FileOutputStream out = new FileOutputStream(mCurrentImagePath);
+ yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
+ out.close();
+
+ // Now's a good time to run the GC. Since we won't do any explicit
+ // allocation during the test, the GC should stay dormant and not
+ // influence our results.
+ System.runFinalization();
+ System.gc();
+
+ mHandler.post(mUpdateResults);
+ } catch (Exception e) {
+ Log.e(TAG, "exception in storing final mosaic", e);
+ }
+ }
+
+ public void setCaptureStarted(int sweepAngle, int blendType) {
+ // Reset values so we can do this again.
+ mMosaicer.reset();
+ mTotalFrameCount = 0;
+ mLastProcessedFrameTimestamp = 0;
+
+ mTimeTaken = System.currentTimeMillis();
+ CaptureView captureView = mActivity.getCaptureView();
+ captureView.setVisibility(View.VISIBLE);
+ mLowResProcessor = new LowResFrameProcessor(sweepAngle - 5, captureView);
+
+ mLowResProcessor.onPreExecute();
+ }
+
+ /**
+ * This must be called when the activity pauses (in Activity.onPause).
+ */
+ public void onPause() {
+ mMosaicer.reset();
+ }
+
+ private int getLastFilledIn() {
+ synchronized (mLockFillIn) {
+ if (mFillIn > 0) {
+ return mFillIn - 1;
+ } else {
+ return NUM_FRAMES_IN_BUFFER - 1;
+ }
+ }
+ }
+
+ private void incrementFillIn() {
+ synchronized (mLockFillIn) {
+ mFillIn = ((mFillIn + 1) >= NUM_FRAMES_IN_BUFFER) ? 0 : (mFillIn + 1);
+ }
+ }
+
+ /**
+ * Inner class to tell the gallery app to scan the newly created mosaic images.
+ * TODO: insert the image to media store.
+ */
+ private static final class ScannerClient implements MediaScannerConnectionClient {
+ ArrayList<String> mPaths = new ArrayList<String>();
+ MediaScannerConnection mScannerConnection;
+ boolean mConnected;
+ Object mLock = new Object();
+
+ public ScannerClient(Context context) {
+ mScannerConnection = new MediaScannerConnection(context, this);
+ }
+
+ public void scanPath(String path) {
+ synchronized (mLock) {
+ if (mConnected) {
+ mScannerConnection.scanFile(path, null);
+ } else {
+ mPaths.add(path);
+ mScannerConnection.connect();
+ }
+ }
+ }
+
+ @Override
+ public void onMediaScannerConnected() {
+ synchronized (mLock) {
+ mConnected = true;
+ if (!mPaths.isEmpty()) {
+ for (String path : mPaths) {
+ mScannerConnection.scanFile(path, null);
+ }
+ mPaths.clear();
+ }
+ }
+ }
+
+ @Override
+ public void onScanCompleted(String path, Uri uri) {
+ }
+ }
+}