PanoramaActivity.java revision 36839102e03107e3fb121197036365a58ec2686e
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.Storage;
26import com.android.camera.Util;
27
28import android.app.Activity;
29import android.content.Context;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.graphics.ImageFormat;
33import android.graphics.PixelFormat;
34import android.graphics.Rect;
35import android.graphics.SurfaceTexture;
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.os.Bundle;
45import android.os.Handler;
46import android.os.Message;
47import android.util.Log;
48import android.view.View;
49import android.view.WindowManager;
50import android.widget.Button;
51import android.widget.ImageView;
52
53import java.io.ByteArrayOutputStream;
54import java.util.List;
55
56/**
57 * Activity to handle panorama capturing.
58 */
59public class PanoramaActivity extends Activity implements
60        ModePicker.OnModeChangeListener,
61        SurfaceTexture.OnFrameAvailableListener,
62        MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
63    public static final int DEFAULT_SWEEP_ANGLE = 160;
64    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
65    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
66
67    private static final int MSG_FINAL_MOSAIC_READY = 1;
68    private static final int MSG_RESET_TO_PREVIEW = 2;
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    private static final int CAPTURE_VIEWFINDER = 0;
74    private static final int CAPTURE_MOSAIC = 1;
75
76    // Ratio of nanosecond to second
77    private static final float NS2S = 1.0f / 1000000000.0f;
78
79    private boolean mPausing;
80
81    private View mPanoControlLayout;
82    private View mCaptureLayout;
83    private Button mStopCaptureButton;
84    private View mReviewLayout;
85    private ImageView mReview;
86    private CaptureView mCaptureView;
87    private MosaicRendererSurfaceView mRealTimeMosaicView;
88
89    private int mPreviewWidth;
90    private int mPreviewHeight;
91    private Camera mCameraDevice;
92    private int mCameraState;
93    private int mCaptureState;
94    private SensorManager mSensorManager;
95    private Sensor mSensor;
96    private ModePicker mModePicker;
97    private MosaicFrameProcessor mMosaicFrameProcessor;
98    private String mCurrentImagePath = null;
99    private long mTimeTaken;
100    private Handler mMainHandler;
101    private SurfaceTexture mSurface;
102    private boolean mThreadRunning;
103
104    @Override
105    public void onCreate(Bundle icicle) {
106        super.onCreate(icicle);
107
108        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
109                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
110
111        createContentView();
112
113        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
114        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
115        if (mSensor == null) {
116            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
117        }
118
119        mMainHandler = new Handler() {
120            @Override
121            public void handleMessage(Message msg) {
122                switch (msg.what) {
123                    case MSG_FINAL_MOSAIC_READY:
124                        mThreadRunning = false;
125                        showFinalMosaic((Bitmap) msg.obj);
126                        break;
127                    case MSG_RESET_TO_PREVIEW:
128                        mThreadRunning = false;
129                        resetToPreview();
130                        break;
131                }
132                clearMosaicFrameProcessorIfNeeded();
133            }
134        };
135    }
136
137    private void setupCamera() {
138        openCamera();
139        Parameters parameters = mCameraDevice.getParameters();
140        setupCaptureParams(parameters);
141        configureCamera(parameters);
142    }
143
144    private void releaseCamera() {
145        if (mCameraDevice != null) {
146            mCameraDevice.setPreviewCallbackWithBuffer(null);
147            CameraHolder.instance().release();
148            mCameraDevice = null;
149            mCameraState = PREVIEW_STOPPED;
150        }
151    }
152
153    private void openCamera() {
154        try {
155            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
156        } catch (CameraHardwareException e) {
157            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
158            return;
159        } catch (CameraDisabledException e) {
160            Util.showErrorAndFinish(this, R.string.camera_disabled);
161            return;
162        }
163    }
164
165    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
166            boolean needSmaller) {
167        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
168        boolean hasFound = false;
169        for (Size size : supportedSizes) {
170            int h = size.height;
171            int w = size.width;
172            // we only want 4:3 format.
173            int d = DEFAULT_CAPTURE_PIXELS - h * w;
174            if (needSmaller && d < 0) { // no bigger preview than 960x720.
175                continue;
176            }
177            if (need4To3 && (h * 4 != w * 3)) {
178                continue;
179            }
180            d = Math.abs(d);
181            if (d < pixelsDiff) {
182                mPreviewWidth = w;
183                mPreviewHeight = h;
184                pixelsDiff = d;
185                hasFound = true;
186            }
187        }
188        return hasFound;
189    }
190
191    private void setupCaptureParams(Parameters parameters) {
192        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
193        if (!findBestPreviewSize(supportedSizes, true, true)) {
194            Log.w(TAG, "No 4:3 ratio preview size supported.");
195            if (!findBestPreviewSize(supportedSizes, false, true)) {
196                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
197                findBestPreviewSize(supportedSizes, false, false);
198            }
199        }
200        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
201        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
202
203        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
204        int last = frameRates.size() - 1;
205        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
206        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
207        parameters.setPreviewFpsRange(minFps, maxFps);
208        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
209
210        parameters.setRecordingHint(false);
211    }
212
213    public int getPreviewBufSize() {
214        PixelFormat pixelInfo = new PixelFormat();
215        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
216        // TODO: remove this extra 32 byte after the driver bug is fixed.
217        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
218    }
219
220    private void configureCamera(Parameters parameters) {
221        mCameraDevice.setParameters(parameters);
222
223        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
224                CameraHolder.instance().getBackCameraId());
225        mCameraDevice.setDisplayOrientation(orientation);
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 onMosaicSurfaceCreated(final SurfaceTexture surface) {
246        runOnUiThread(new Runnable() {
247            @Override
248            public void run() {
249                mSurface = surface;
250                mSurface.setOnFrameAvailableListener(PanoramaActivity.this);
251                startCameraPreview();
252            }
253        });
254    }
255
256    public void runViewFinder() {
257        mRealTimeMosaicView.setWarping(false);
258
259        // First update the surface texture...
260        mRealTimeMosaicView.updateSurfaceTexture();
261        // ...then call preprocess to render it to low-res and high-res RGB textures
262        mRealTimeMosaicView.preprocess();
263
264        mRealTimeMosaicView.setReady();
265        mRealTimeMosaicView.requestRender();
266    }
267
268    public void runMosaicCapture() {
269        mRealTimeMosaicView.setWarping(true);
270
271        // Lock the condition variable
272        mRealTimeMosaicView.lockPreviewReadyFlag();
273        // First update the surface texture...
274        mRealTimeMosaicView.updateSurfaceTexture();
275        // ...then call preprocess to render it to low-res and high-res RGB textures
276        mRealTimeMosaicView.preprocess();
277        // Now, transfer the textures from GPU to CPU memory for processing
278        mRealTimeMosaicView.transferGPUtoCPU();
279        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
280        mRealTimeMosaicView.waitUntilPreviewReady();
281
282        mMosaicFrameProcessor.processFrame();
283    }
284
285    public synchronized void onFrameAvailable(SurfaceTexture surface) {
286        /* For simplicity, SurfaceTexture calls here when it has new
287         * data available.  Call may come in from some random thread,
288         * so let's be safe and use synchronize. No OpenGL calls can be done here.
289         */
290        if (mCaptureState == CAPTURE_VIEWFINDER) {
291            runViewFinder();
292        } else {
293            runMosaicCapture();
294        }
295    }
296
297    public void startCapture() {
298        // Reset values so we can do this again.
299        mTimeTaken = System.currentTimeMillis();
300        mCaptureState = CAPTURE_MOSAIC;
301
302        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
303            @Override
304            public void onProgress(boolean isFinished, float translationRate, int traversedAngleX,
305                    int traversedAngleY) {
306                if (isFinished) {
307                    stopCapture();
308                } else {
309                    updateProgress(translationRate, traversedAngleX, traversedAngleY);
310                }
311            }
312        });
313
314        mStopCaptureButton.setVisibility(View.VISIBLE);
315        mRealTimeMosaicView.setVisibility(View.VISIBLE);
316        mPanoControlLayout.setVisibility(View.GONE);
317
318    }
319
320    private void stopCapture() {
321        mCaptureState = CAPTURE_VIEWFINDER;
322
323        mMosaicFrameProcessor.setProgressListener(null);
324        stopCameraPreview();
325
326        mSurface.setOnFrameAvailableListener(null);
327
328        // TODO: show some dialog for long computation.
329        if (!mThreadRunning) {
330            mThreadRunning = true;
331            Thread t = new Thread() {
332                @Override
333                public void run() {
334                    byte[] jpegData = generateFinalMosaic(false);
335                    Bitmap bitmap = null;
336                    if (jpegData != null) {
337                        bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
338                    }
339                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
340                            MSG_FINAL_MOSAIC_READY, bitmap));
341                }
342            };
343            t.start();
344        }
345    }
346
347    private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY) {
348
349        mRealTimeMosaicView.setReady();
350        mRealTimeMosaicView.requestRender();
351
352        if (translationRate > 150) {
353            // TODO: remove the text and draw implications according to the UI
354            // spec.
355            mCaptureView.setStatusText("S L O W   D O W N");
356            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
357            mCaptureView.invalidate();
358        } else {
359            mCaptureView.setStatusText("");
360            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
361            mCaptureView.invalidate();
362        }
363    }
364
365    private void createContentView() {
366        setContentView(R.layout.panorama);
367
368        mCaptureState = CAPTURE_VIEWFINDER;
369
370        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
371        mStopCaptureButton = (Button) findViewById(R.id.pano_capture_stop_button);
372        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
373
374        mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
375        mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
376        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
377
378        mRealTimeMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
379        mRealTimeMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
380
381        mPanoControlLayout = (View) findViewById(R.id.pano_control_layout);
382
383        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
384        mModePicker.setVisibility(View.VISIBLE);
385        mModePicker.setOnModeChangeListener(this);
386        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
387
388        mRealTimeMosaicView.setVisibility(View.VISIBLE);
389    }
390
391    @OnClickAttr
392    public void onStartButtonClicked(View v) {
393        // If mSurface == null then GL setup is not finished yet. All buttons cannot be pressed.
394        if (mPausing || mThreadRunning || mSurface == null) return;
395        startCapture();
396    }
397
398    @OnClickAttr
399    public void onStopButtonClicked(View v) {
400        if (mPausing || mThreadRunning || mSurface == null) return;
401        stopCapture();
402    }
403
404    @OnClickAttr
405    public void onOkButtonClicked(View v) {
406        if (mPausing || mThreadRunning || mSurface == null) return;
407        mThreadRunning = true;
408        Thread t = new Thread() {
409            @Override
410            public void run() {
411                byte[] jpegData = generateFinalMosaic(true);
412                savePanorama(jpegData);
413                mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW));
414            }
415        };
416        t.start();
417    }
418
419    @OnClickAttr
420    public void onRetakeButtonClicked(View v) {
421        if (mPausing || mThreadRunning) return;
422        resetToPreview();
423    }
424
425    private void resetToPreview() {
426        mCaptureState = CAPTURE_VIEWFINDER;
427
428        mReviewLayout.setVisibility(View.GONE);
429        mStopCaptureButton.setVisibility(View.GONE);
430        mPanoControlLayout.setVisibility(View.VISIBLE);
431        mCaptureLayout.setVisibility(View.VISIBLE);
432        mMosaicFrameProcessor.reset();
433
434        mSurface.setOnFrameAvailableListener(this);
435
436        if (!mPausing) startCameraPreview();
437
438        mRealTimeMosaicView.setVisibility(View.VISIBLE);
439    }
440
441    private void showFinalMosaic(Bitmap bitmap) {
442        if (bitmap != null) {
443            mReview.setImageBitmap(bitmap);
444        }
445        mCaptureLayout.setVisibility(View.GONE);
446        mReviewLayout.setVisibility(View.VISIBLE);
447        mCaptureView.setStatusText("");
448        mCaptureView.setSweepAngle(0);
449    }
450
451    private void savePanorama(byte[] jpegData) {
452        if (jpegData != null) {
453            String imagePath = PanoUtil.createName(
454                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
455            Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 0,
456                    jpegData);
457        }
458    }
459
460    private void clearMosaicFrameProcessorIfNeeded() {
461        if (!mPausing || mThreadRunning) return;
462        mMosaicFrameProcessor.clear();
463    }
464
465    private void initMosaicFrameProcessorIfNeeded() {
466        if (mPausing || mThreadRunning) return;
467        if (mMosaicFrameProcessor == null) {
468            // Start the activity for the first time.
469            mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5,
470                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
471        }
472        mMosaicFrameProcessor.initialize();
473    }
474
475    @Override
476    protected void onPause() {
477        super.onPause();
478        mSurface.setOnFrameAvailableListener(null);
479        releaseCamera();
480        mPausing = true;
481
482        mRealTimeMosaicView.onPause();
483        mCaptureView.onPause();
484        mSensorManager.unregisterListener(mListener);
485        clearMosaicFrameProcessorIfNeeded();
486        System.gc();
487    }
488
489    @Override
490    protected void onResume() {
491        super.onResume();
492
493        mPausing = false;
494        /*
495         * It is not necessary to get accelerometer events at a very high rate,
496         * by using a slower rate (SENSOR_DELAY_UI), we get an automatic
497         * low-pass filter, which "extracts" the gravity component of the
498         * acceleration. As an added benefit, we use less power and CPU
499         * resources.
500         */
501        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
502        mCaptureState = CAPTURE_VIEWFINDER;
503        setupCamera();
504        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
505        // has to be decided by camera device.
506        initMosaicFrameProcessorIfNeeded();
507        mCaptureView.onResume();
508        mRealTimeMosaicView.onResume();
509    }
510
511    private final SensorEventListener mListener = new SensorEventListener() {
512        private float mCompassCurrX; // degrees
513        private float mCompassCurrY; // degrees
514        private float mTimestamp;
515
516        public void onSensorChanged(SensorEvent event) {
517            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
518                if (mTimestamp != 0) {
519                    final float dT = (event.timestamp - mTimestamp) * NS2S;
520                    mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
521                    mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
522                }
523                mTimestamp = event.timestamp;
524
525            } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
526                mCompassCurrX = event.values[0];
527                mCompassCurrY = event.values[1];
528            }
529
530            if (mMosaicFrameProcessor != null) {
531                mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY);
532            }
533        }
534
535        @Override
536        public void onAccuracyChanged(Sensor sensor, int accuracy) {
537        }
538    };
539
540    public byte[] generateFinalMosaic(boolean highRes) {
541        mMosaicFrameProcessor.createMosaic(highRes);
542
543        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
544        if (imageData == null) {
545            Log.e(TAG, "getFinalMosaicNV21() returned null.");
546            return null;
547        }
548
549        int len = imageData.length - 8;
550        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
551                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
552        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
553                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
554        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
555
556        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
557        ByteArrayOutputStream out = new ByteArrayOutputStream();
558        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
559        try {
560            out.close();
561        } catch (Exception e) {
562            Log.e(TAG, "Exception in storing final mosaic", e);
563            return null;
564        }
565        return out.toByteArray();
566    }
567
568    private void setPreviewTexture(SurfaceTexture surface) {
569        try {
570            mCameraDevice.setPreviewTexture(surface);
571        } catch (Throwable ex) {
572            releaseCamera();
573            throw new RuntimeException("setPreviewTexture failed", ex);
574        }
575    }
576
577    private void startCameraPreview() {
578        // If we're previewing already, stop the preview first (this will blank
579        // the screen).
580        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
581
582        setPreviewTexture(mSurface);
583
584        try {
585            Log.v(TAG, "startPreview");
586            mCameraDevice.startPreview();
587        } catch (Throwable ex) {
588            releaseCamera();
589            throw new RuntimeException("startPreview failed", ex);
590        }
591        mCameraState = PREVIEW_ACTIVE;
592    }
593
594    private void stopCameraPreview() {
595        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
596            Log.v(TAG, "stopPreview");
597            mCameraDevice.stopPreview();
598        }
599        mCameraState = PREVIEW_STOPPED;
600    }
601}
602