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