PanoramaActivity.java revision eeb94d4de94bfd4e01f3a716803f77a530f5b92c
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 com.android.camera.CameraDisabledException;
20import com.android.camera.CameraHardwareException;
21import com.android.camera.CameraHolder;
22import com.android.camera.MenuHelper;
23import com.android.camera.ModePicker;
24import com.android.camera.R;
25import com.android.camera.ShutterButton;
26import com.android.camera.Storage;
27import com.android.camera.Util;
28
29import android.app.Activity;
30import android.content.Context;
31import android.graphics.Bitmap;
32import android.graphics.ImageFormat;
33import android.graphics.Matrix;
34import android.graphics.PixelFormat;
35import android.graphics.Rect;
36import android.graphics.YuvImage;
37import android.hardware.Camera;
38import android.hardware.Camera.Parameters;
39import android.hardware.Camera.Size;
40import android.hardware.Sensor;
41import android.hardware.SensorEvent;
42import android.hardware.SensorEventListener;
43import android.hardware.SensorManager;
44import android.net.Uri;
45import android.os.Bundle;
46import android.os.Handler;
47import android.os.Message;
48import android.util.Log;
49import android.view.SurfaceHolder;
50import android.view.SurfaceView;
51import android.view.View;
52import android.view.WindowManager;
53import android.widget.Button;
54import android.widget.ImageView;
55
56import java.io.ByteArrayOutputStream;
57import java.util.List;
58
59/**
60 * Activity to handle panorama capturing.
61 */
62public class PanoramaActivity extends Activity implements
63        ModePicker.OnModeChangeListener, SurfaceHolder.Callback {
64    public static final int DEFAULT_SWEEP_ANGLE = 60;
65    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
66    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
67
68    private static final int MSG_FINAL_MOSAIC_READY = 1;
69
70    private static final String TAG = "PanoramaActivity";
71    private static final int PREVIEW_STOPPED = 0;
72    private static final int PREVIEW_ACTIVE = 1;
73
74    // Ratio of nanosecond to second
75    private static final float NS2S = 1.0f / 1000000000.0f;
76    private View mPanoControlLayout;
77    private View mCaptureLayout;
78    private View mReviewLayout;
79    private SurfaceView mPreview;
80    private ImageView mReview;
81    private CaptureView mCaptureView;
82    private MosaicRendererSurfaceView mRealTimeMosaicView;
83
84    private ShutterButton mShutterButton;
85    private Button mStopButton;
86    private int mPreviewWidth;
87    private int mPreviewHeight;
88    private Camera mCameraDevice;
89    private int mCameraState;
90    private SensorManager mSensorManager;
91    private Sensor mSensor;
92    private ModePicker mModePicker;
93    private MosaicFrameProcessor mMosaicFrameProcessor;
94    private String mCurrentImagePath = null;
95    private long mTimeTaken;
96    private Handler mMainHandler;
97    private SurfaceHolder mSurfaceHolder;
98
99    @Override
100    public void onCreate(Bundle icicle) {
101        super.onCreate(icicle);
102
103        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
104                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
105
106        createContentView();
107
108        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
109        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
110        if (mSensor == null) {
111            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
112        }
113
114        mMainHandler = new Handler() {
115            @Override
116            public void handleMessage(Message msg) {
117                switch (msg.what) {
118                    case MSG_FINAL_MOSAIC_READY:
119                        Uri uri = (Uri) msg.obj;
120                        showFinalMosaic(uri);
121                }
122            }
123        };
124    }
125
126    private void setupCamera() {
127        openCamera();
128        Parameters parameters = mCameraDevice.getParameters();
129        setupCaptureParams(parameters);
130        configureCamera(parameters);
131    }
132
133    private void releaseCamera() {
134        if (mCameraDevice != null) {
135            CameraHolder.instance().release();
136            mCameraDevice = null;
137            mCameraState = PREVIEW_STOPPED;
138        }
139    }
140
141    private void openCamera() {
142        try {
143            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
144        } catch (CameraHardwareException e) {
145            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
146            return;
147        } catch (CameraDisabledException e) {
148            Util.showErrorAndFinish(this, R.string.camera_disabled);
149            return;
150        }
151    }
152
153    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
154            boolean needSmaller) {
155        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
156        boolean hasFound = false;
157        for (Size size : supportedSizes) {
158            int h = size.height;
159            int w = size.width;
160            // we only want 4:3 format.
161            int d = DEFAULT_CAPTURE_PIXELS - h * w;
162            if (needSmaller && d < 0) { // no bigger preview than 960x720.
163                continue;
164            }
165            if (need4To3 && (h * 4 != w * 3)) {
166                continue;
167            }
168            d = Math.abs(d);
169            if (d < pixelsDiff) {
170                mPreviewWidth = w;
171                mPreviewHeight = h;
172                pixelsDiff = d;
173                hasFound = true;
174            }
175        }
176        return hasFound;
177    }
178
179    private void setupCaptureParams(Parameters parameters) {
180        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
181        if (!findBestPreviewSize(supportedSizes, true, true)) {
182            Log.w(TAG, "No 4:3 ratio preview size supported.");
183            if (!findBestPreviewSize(supportedSizes, false, true)) {
184                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
185                findBestPreviewSize(supportedSizes, false, false);
186            }
187        }
188        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
189        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
190
191        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
192        int last = frameRates.size() - 1;
193        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
194        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
195        parameters.setPreviewFpsRange(minFps, maxFps);
196        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
197
198        // TODO: use camera API after it is published.
199        parameters.set("recording-hint", "false");
200    }
201
202    public int getPreviewBufSize() {
203        PixelFormat pixelInfo = new PixelFormat();
204        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
205        // TODO: remove this extra 32 byte after the driver bug is fixed.
206        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
207    }
208
209    private void configureCamera(Parameters parameters) {
210        mCameraDevice.setParameters(parameters);
211
212        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
213                CameraHolder.instance().getBackCameraId());
214        mCameraDevice.setDisplayOrientation(orientation);
215
216        int bufSize = getPreviewBufSize();
217        Log.v(TAG, "BufSize = " + bufSize);
218        for (int i = 0; i < 10; i++) {
219            try {
220                mCameraDevice.addCallbackBuffer(new byte[bufSize]);
221            } catch (OutOfMemoryError e) {
222                Log.v(TAG, "Buffer allocation failed: buffer " + i);
223                break;
224            }
225        }
226    }
227
228    private boolean switchToOtherMode(int mode) {
229        if (isFinishing()) {
230            return false;
231        }
232        MenuHelper.gotoMode(mode, this);
233        finish();
234        return true;
235    }
236
237    public boolean onModeChanged(int mode) {
238        if (mode != ModePicker.MODE_PANORAMA) {
239            return switchToOtherMode(mode);
240        } else {
241            return true;
242        }
243    }
244
245    public void startCapture() {
246        // Reset values so we can do this again.
247        mTimeTaken = System.currentTimeMillis();
248
249        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
250            @Override
251            public void onProgress(boolean isFinished, float translationRate, int traversedAngleX,
252                    int traversedAngleY, Bitmap lowResBitmapAlpha, Matrix transformaMatrix) {
253                if (isFinished) {
254                    stopCapture();
255                } else {
256                    updateProgress(translationRate, traversedAngleX, traversedAngleY,
257                            lowResBitmapAlpha, transformaMatrix);
258                }
259            }
260        });
261
262        // Preview callback used whenever new viewfinder frame is available
263        mCameraDevice.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
264            @Override
265            public void onPreviewFrame(final byte[] data, Camera camera) {
266                mMosaicFrameProcessor.processFrame(data, mPreviewWidth, mPreviewHeight);
267                // The returned buffer needs be added back to callback buffer
268                // again.
269                camera.addCallbackBuffer(data);
270            }
271        });
272
273        mCaptureLayout.setVisibility(View.VISIBLE);
274        mPanoControlLayout.setVisibility(View.GONE);
275    }
276
277    private void stopCapture() {
278        mMosaicFrameProcessor.setProgressListener(null);
279        mCameraDevice.stopPreview();
280        mCameraDevice.setPreviewCallbackWithBuffer(null);
281        // TODO: show some dialog for long computation.
282        Thread t = new Thread() {
283            @Override
284            public void run() {
285                generateAndStoreFinalMosaic(false);
286            }
287        };
288        t.start();
289    }
290
291    private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY,
292            Bitmap lowResBitmapAlpha, Matrix transformationMatrix) {
293
294        mRealTimeMosaicView.setReady();
295        mRealTimeMosaicView.requestRender();
296
297        if (translationRate > 150) {
298            // TODO: remove the text and draw implications according to the UI
299            // spec.
300            mCaptureView.setStatusText("S L O W   D O W N");
301            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
302            mCaptureView.invalidate();
303        } else {
304            mCaptureView.setStatusText("");
305            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
306            mCaptureView.invalidate();
307        }
308    }
309
310    private void createContentView() {
311        setContentView(R.layout.panorama);
312
313        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
314        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
315
316        mPreview = (SurfaceView) findViewById(R.id.pano_preview_view);
317        mPreview.getHolder().addCallback(this);
318        mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
319        mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
320        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
321
322        mRealTimeMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
323
324        mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button);
325        mShutterButton.setOnClickListener(new View.OnClickListener() {
326            @Override
327            public void onClick(View v) {
328                startCapture();
329            }
330        });
331        mStopButton = (Button) findViewById(R.id.pano_capture_stop_button);
332        mStopButton.setOnClickListener(new View.OnClickListener() {
333            @Override
334            public void onClick(View v) {
335                stopCapture();
336            }
337        });
338
339        mPanoControlLayout = (View) findViewById(R.id.pano_control_layout);
340
341        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
342        mModePicker.setVisibility(View.VISIBLE);
343        mModePicker.setOnModeChangeListener(this);
344        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
345    }
346
347    private void showFinalMosaic(Uri uri) {
348        mReview.setImageURI(uri);
349        mCaptureLayout.setVisibility(View.INVISIBLE);
350        mPreview.setVisibility(View.INVISIBLE);
351        mReviewLayout.setVisibility(View.VISIBLE);
352        mCaptureView.setStatusText("");
353        mCaptureView.setSweepAngle(0);
354    }
355
356    @Override
357    protected void onPause() {
358        super.onPause();
359        releaseCamera();
360        mMosaicFrameProcessor.onPause();
361        mCaptureView.onPause();
362        mRealTimeMosaicView.onPause();
363        mSensorManager.unregisterListener(mListener);
364        System.gc();
365    }
366
367    @Override
368    protected void onResume() {
369        super.onResume();
370
371        /*
372         * It is not necessary to get accelerometer events at a very high rate,
373         * by using a slower rate (SENSOR_DELAY_UI), we get an automatic
374         * low-pass filter, which "extracts" the gravity component of the
375         * acceleration. As an added benefit, we use less power and CPU
376         * resources.
377         */
378        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
379
380        setupCamera();
381        startPreview();
382
383        if (mMosaicFrameProcessor == null) {
384            // Start the activity for the first time.
385            mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5,
386                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
387            mMosaicFrameProcessor.onResume();
388        } else {
389            mMosaicFrameProcessor.onResume();
390        }
391        mCaptureView.onResume();
392        mRealTimeMosaicView.onResume();
393    }
394
395    private final SensorEventListener mListener = new SensorEventListener() {
396        private float mCompassCurrX; // degrees
397        private float mCompassCurrY; // degrees
398        private float mTimestamp;
399
400        public void onSensorChanged(SensorEvent event) {
401            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
402                if (mTimestamp != 0) {
403                    final float dT = (event.timestamp - mTimestamp) * NS2S;
404                    mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
405                    mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
406                }
407                mTimestamp = event.timestamp;
408
409            } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
410                mCompassCurrX = event.values[0];
411                mCompassCurrY = event.values[1];
412            }
413
414            if (mMosaicFrameProcessor != null) {
415                mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY);
416            }
417        }
418
419        @Override
420        public void onAccuracyChanged(Sensor sensor, int accuracy) {
421        }
422    };
423
424    public void generateAndStoreFinalMosaic(boolean highRes) {
425        mMosaicFrameProcessor.createMosaic(highRes);
426
427        mCurrentImagePath = PanoUtil.createName(
428                getResources().getString(R.string.pano_file_name_format), mTimeTaken);
429
430        if (highRes) {
431            mCurrentImagePath += "_HR";
432        } else {
433            mCurrentImagePath += "_LR";
434        }
435
436            byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
437            int len = imageData.length - 8;
438
439            int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
440                    + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
441            int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
442                    + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
443            Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
444
445            YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
446            ByteArrayOutputStream out = new ByteArrayOutputStream();
447            yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
448            try {
449                out.close();
450            } catch (Exception e) {
451                Log.e(TAG, "Exception in storing final mosaic", e);
452                return;
453            }
454            Uri uri = Storage.addImage(
455                    getContentResolver(), mCurrentImagePath, mTimeTaken, null, 0,
456                    out.toByteArray());
457
458            mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_FINAL_MOSAIC_READY, uri));
459            // Now's a good time to run the GC. Since we won't do any explicit
460            // allocation during the test, the GC should stay dormant and not
461            // influence our results.
462            System.runFinalization();
463            System.gc();
464    }
465
466    @Override
467    public void surfaceCreated(SurfaceHolder holder) {
468    }
469
470    @Override
471    public void surfaceDestroyed(SurfaceHolder holder) {
472    }
473
474    @Override
475    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
476        mSurfaceHolder = holder;
477
478        if (mCameraDevice == null) return;
479
480        // Set preview display if the surface is being created. Preview was
481        // already started. Also restart the preview if display rotation has
482        // changed. Sometimes this happens when the device is held in portrait
483        // and camera app is opened. Rotation animation takes some time and
484        // display rotation in onCreate may not be what we want.
485        if (holder.isCreating()) {
486            // Set preview display if the surface is being created and preview
487            // was already started. That means preview display was set to null
488            // and we need to set it now.
489            setPreviewDisplay(holder);
490        } else {
491            // 1. Restart the preview if the size of surface was changed. The
492            // framework may not support changing preview display on the fly.
493            // 2. Start the preview now if surface was destroyed and preview
494            // stopped.
495            startPreview();
496        }
497    }
498
499    private void setPreviewDisplay(SurfaceHolder holder) {
500        try {
501            mCameraDevice.setPreviewDisplay(holder);
502        } catch (Throwable ex) {
503            releaseCamera();
504            throw new RuntimeException("setPreviewDisplay failed", ex);
505        }
506    }
507
508    private void startPreview() {
509        // If we're previewing already, stop the preview first (this will blank
510        // the screen).
511        if (mCameraState != PREVIEW_STOPPED) stopPreview();
512
513        setPreviewDisplay(mSurfaceHolder);
514
515        try {
516            Log.v(TAG, "startPreview");
517            mCameraDevice.startPreview();
518        } catch (Throwable ex) {
519            releaseCamera();
520            throw new RuntimeException("startPreview failed", ex);
521        }
522        mCameraState = PREVIEW_ACTIVE;
523    }
524
525    private void stopPreview() {
526        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
527            Log.v(TAG, "stopPreview");
528            mCameraDevice.stopPreview();
529        }
530        mCameraState = PREVIEW_STOPPED;
531    }
532}
533