PanoramaActivity.java revision 4b602592e0d189499b22d107d997b83e798b5bd9
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera.panorama;
18
19import android.app.Activity;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.ImageFormat;
23import android.graphics.Matrix;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.graphics.YuvImage;
27import android.hardware.Camera;
28import android.hardware.Camera.Parameters;
29import android.hardware.Camera.Size;
30import android.hardware.Sensor;
31import android.hardware.SensorEvent;
32import android.hardware.SensorEventListener;
33import android.hardware.SensorManager;
34import android.media.MediaScannerConnection;
35import android.media.MediaScannerConnection.MediaScannerConnectionClient;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.Message;
40import android.util.Log;
41import android.view.View;
42import android.view.WindowManager;
43import android.widget.ImageView;
44
45import com.android.camera.CameraDisabledException;
46import com.android.camera.CameraHardwareException;
47import com.android.camera.CameraHolder;
48import com.android.camera.MenuHelper;
49import com.android.camera.ModePicker;
50import com.android.camera.R;
51import com.android.camera.ShutterButton;
52import com.android.camera.Storage;
53import com.android.camera.Util;
54
55import java.io.File;
56import java.io.ByteArrayOutputStream;
57import java.util.ArrayList;
58import java.util.List;
59
60public class PanoramaActivity extends Activity implements ModePicker.OnModeChangeListener {
61    public static final int DEFAULT_SWEEP_ANGLE = 60;
62    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
63    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
64
65    private static final int MSG_FINAL_MOSAIC_READY = 1;
66
67    private static final String TAG = "PanoramaActivity";
68    private static final float NS2S = 1.0f / 1000000000.0f; // TODO: commit for
69                                                            // this constant.
70    private Preview mPreview;
71    private ImageView mReview;
72    private CaptureView mCaptureView;
73
74    private ShutterButton mShutterButton;
75    private int mPreviewWidth;
76    private int mPreviewHeight;
77    private Camera mCameraDevice;
78    private SensorManager mSensorManager;
79    private Sensor mSensor;
80    private ModePicker mModePicker;
81    private MosaicFrameProcessor mMosaicFrameProcessor;
82    private String mCurrentImagePath = null;
83    private long mTimeTaken;
84    private Handler mMainHandler;
85
86    @Override
87    public void onCreate(Bundle icicle) {
88        super.onCreate(icicle);
89
90        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
91                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
92
93        createContentView();
94
95        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
96        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
97        if (mSensor == null) {
98            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
99        }
100
101        mMainHandler = new Handler() {
102            @Override
103            public void handleMessage(Message msg) {
104                switch (msg.what) {
105                    case MSG_FINAL_MOSAIC_READY:
106                        Uri uri = (Uri) msg.obj;
107                        showFinalMosaic(uri);
108                }
109            }
110        };
111    }
112
113    private void setupCamera() {
114        openCamera();
115        Parameters parameters = mCameraDevice.getParameters();
116        setupCaptureParams(parameters);
117        configureCamera(parameters);
118    }
119
120    private void releaseCamera() {
121        if (mCameraDevice != null) {
122            mCameraDevice.stopPreview();
123            CameraHolder.instance().release();
124            mCameraDevice = null;
125        }
126    }
127
128    private void openCamera() {
129        try {
130            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
131        } catch (CameraHardwareException e) {
132            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
133            return;
134        } catch (CameraDisabledException e) {
135            Util.showErrorAndFinish(this, R.string.camera_disabled);
136            return;
137        }
138    }
139
140    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
141            boolean needSmaller) {
142        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
143        boolean hasFound = false;
144        for (Size size : supportedSizes) {
145            int h = size.height;
146            int w = size.width;
147            // we only want 4:3 format.
148            int d = DEFAULT_CAPTURE_PIXELS - h * w;
149            if (needSmaller && d < 0) { // no bigger preview than 960x720.
150                continue;
151            }
152            if (need4To3 && (h * 4 != w * 3)) {
153                continue;
154            }
155            d = Math.abs(d);
156            if (d < pixelsDiff) {
157                mPreviewWidth = w;
158                mPreviewHeight = h;
159                pixelsDiff = d;
160                hasFound = true;
161            }
162        }
163        return hasFound;
164    }
165
166    private void setupCaptureParams(Parameters parameters) {
167        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
168        if (!findBestPreviewSize(supportedSizes, true, true)) {
169            Log.w(TAG, "No 4:3 ratio preview size supported.");
170            if (!findBestPreviewSize(supportedSizes, false, true)) {
171                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
172                findBestPreviewSize(supportedSizes, false, false);
173            }
174        }
175        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
176        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
177
178        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
179        int last = frameRates.size() - 1;
180        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
181        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
182        parameters.setPreviewFpsRange(minFps, maxFps);
183        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
184
185        // TODO: use camera API after it is published.
186        parameters.set("recording-hint", "false");
187    }
188
189    public int getPreviewBufSize() {
190        PixelFormat pixelInfo = new PixelFormat();
191        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
192        // TODO: remove this extra 32 byte after the driver bug is fixed.
193        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
194    }
195
196    private void configureCamera(Parameters parameters) {
197        mCameraDevice.setParameters(parameters);
198
199        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
200                CameraHolder.instance().getBackCameraId());
201        mCameraDevice.setDisplayOrientation(orientation);
202
203        int bufSize = getPreviewBufSize();
204        Log.v(TAG, "BufSize = " + bufSize);
205        for (int i = 0; i < 10; i++) {
206            try {
207                mCameraDevice.addCallbackBuffer(new byte[bufSize]);
208            } catch (OutOfMemoryError e) {
209                Log.v(TAG, "Buffer allocation failed: buffer " + i);
210                break;
211            }
212        }
213    }
214
215    private boolean switchToOtherMode(int mode) {
216        if (isFinishing())
217            return false;
218        MenuHelper.gotoMode(mode, this);
219        finish();
220        return true;
221    }
222
223    public boolean onModeChanged(int mode) {
224        if (mode != ModePicker.MODE_PANORAMA) {
225            return switchToOtherMode(mode);
226        } else {
227            return true;
228        }
229    }
230
231    public void setCaptureStarted() {
232        // Reset values so we can do this again.
233        mTimeTaken = System.currentTimeMillis();
234
235        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
236            @Override
237            public void onProgress(boolean isFinished, float translationRate, int traversedAngleX,
238                    int traversedAngleY, Bitmap lowResBitmapAlpha, Matrix transformaMatrix) {
239                if (isFinished) {
240                    onMosaicFinished();
241                } else {
242                    updateProgress(translationRate, traversedAngleX, traversedAngleY,
243                            lowResBitmapAlpha, transformaMatrix);
244                }
245            }
246        });
247
248        // Preview callback used whenever new viewfinder frame is available
249        mCameraDevice.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
250            @Override
251            public void onPreviewFrame(final byte[] data, Camera camera) {
252                mMosaicFrameProcessor.processFrame(data, mPreviewWidth, mPreviewHeight);
253                // The returned buffer needs be added back to callback buffer
254                // again.
255                camera.addCallbackBuffer(data);
256            }
257        });
258
259        mCaptureView.setVisibility(View.VISIBLE);
260    }
261
262    private void onMosaicFinished() {
263        mMosaicFrameProcessor.setProgressListener(null);
264        mCameraDevice.stopPreview();
265        mCameraDevice.setPreviewCallbackWithBuffer(null);
266        // TODO: set mPreview invisible after the camera driver bug is fixed.
267        mCaptureView.setVisibility(View.INVISIBLE);
268        mCaptureView.setBitmap(null);
269        mCaptureView.setStatusText("");
270        mCaptureView.setSweepAngle(0);
271        mCaptureView.invalidate();
272        // TODO: show some dialog for long computation.
273        Thread t = new Thread() {
274            @Override
275            public void run() {
276                generateAndStoreFinalMosaic(false);
277            }
278        };
279        t.start();
280    }
281
282    private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY,
283            Bitmap lowResBitmapAlpha, Matrix transformationMatrix) {
284        mCaptureView.setBitmap(lowResBitmapAlpha, transformationMatrix);
285        if (translationRate > 150) {
286            // TODO: remove the text and draw implications according to the UI
287            // spec.
288            mCaptureView.setStatusText("S L O W   D O W N");
289            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
290            mCaptureView.invalidate();
291        } else {
292            mCaptureView.setStatusText("");
293            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
294            mCaptureView.invalidate();
295        }
296    }
297
298    private void createContentView() {
299        setContentView(R.layout.panorama);
300
301        mPreview = (Preview) findViewById(R.id.pano_preview);
302        mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
303        mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
304        mCaptureView.setVisibility(View.INVISIBLE);
305
306        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
307        mReview.setVisibility(View.INVISIBLE);
308
309        mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button);
310        mShutterButton.setOnClickListener(new View.OnClickListener() {
311            @Override
312            public void onClick(View v) {
313                setCaptureStarted();
314            }
315        });
316        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
317        mModePicker.setVisibility(View.VISIBLE);
318        mModePicker.setOnModeChangeListener(this);
319        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
320    }
321
322    private void showFinalMosaic(Uri uri) {
323        mReview.setImageURI(uri);
324        mReview.setVisibility(View.VISIBLE);
325        mCaptureView.setVisibility(View.INVISIBLE);
326    }
327
328    @Override
329    protected void onPause() {
330        super.onPause();
331        releaseCamera();
332        mMosaicFrameProcessor.onPause();
333        mCaptureView.onPause();
334        mSensorManager.unregisterListener(mListener);
335        System.gc();
336    }
337
338    @Override
339    protected void onResume() {
340        super.onResume();
341
342        /*
343         * It is not necessary to get accelerometer events at a very high rate,
344         * by using a slower rate (SENSOR_DELAY_UI), we get an automatic
345         * low-pass filter, which "extracts" the gravity component of the
346         * acceleration. As an added benefit, we use less power and CPU
347         * resources.
348         */
349        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
350
351        setupCamera();
352        mPreview.setCameraDevice(mCameraDevice);
353        mCameraDevice.startPreview();
354
355        if (mMosaicFrameProcessor == null) {
356            // Start the activity for the first time.
357            mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5,
358                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
359            mMosaicFrameProcessor.onResume();
360        } else {
361            mMosaicFrameProcessor.onResume();
362        }
363        mCaptureView.onResume();
364    }
365
366    private final SensorEventListener mListener = new SensorEventListener() {
367        private float mCompassCurrX; // degrees
368        private float mCompassCurrY; // degrees
369        private float mTimestamp;
370
371        public void onSensorChanged(SensorEvent event) {
372            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
373                if (mTimestamp != 0) {
374                    final float dT = (event.timestamp - mTimestamp) * NS2S;
375                    mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
376                    mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
377                }
378                mTimestamp = event.timestamp;
379
380            } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
381                mCompassCurrX = event.values[0];
382                mCompassCurrY = event.values[1];
383            }
384
385            if (mMosaicFrameProcessor != null) {
386                mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY);
387            }
388        }
389
390        @Override
391        public void onAccuracyChanged(Sensor sensor, int accuracy) {
392        }
393    };
394
395    public void generateAndStoreFinalMosaic(boolean highRes) {
396        mMosaicFrameProcessor.createMosaic(highRes);
397
398        mCurrentImagePath = PanoUtil.createName(
399                getResources().getString(R.string.pano_file_name_format), mTimeTaken);
400
401        if (highRes) {
402            mCurrentImagePath += "_HR";
403        } else {
404            mCurrentImagePath += "_LR";
405        }
406
407            byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
408            int len = imageData.length - 8;
409
410            int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
411                    + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
412            int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
413                    + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
414            Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
415
416            YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
417            ByteArrayOutputStream out = new ByteArrayOutputStream();
418            yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
419            try {
420                out.close();
421            } catch (Exception e) {
422                Log.e(TAG, "Exception in storing final mosaic", e);
423                return;
424            }
425            Uri uri = Storage.addImage(
426                    getContentResolver(), mCurrentImagePath, mTimeTaken, null, 0,
427                    out.toByteArray());
428
429            mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_FINAL_MOSAIC_READY, uri));
430            // Now's a good time to run the GC. Since we won't do any explicit
431            // allocation during the test, the GC should stay dormant and not
432            // influence our results.
433            System.runFinalization();
434            System.gc();
435    }
436
437}
438