PanoramaActivity.java revision ef91f8514bf3c21091dde6b292f9616464dc32e8
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 = 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 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 int mPreviewWidth;
91    private int mPreviewHeight;
92    private Camera mCameraDevice;
93    private int mCameraState;
94    private int mCaptureState;
95    private SensorManager mSensorManager;
96    private Sensor mSensor;
97    private ModePicker mModePicker;
98    private MosaicFrameProcessor mMosaicFrameProcessor;
99    private String mCurrentImagePath = null;
100    private long mTimeTaken;
101    private Handler mMainHandler;
102    private SurfaceTexture mSurface;
103    private boolean mUseSurfaceTexture = true;
104
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        if(!mUseSurfaceTexture) {
247            int bufSize = getPreviewBufSize();
248            Log.v(TAG, "BufSize = " + bufSize);
249            for (int i = 0; i < 10; i++) {
250                try {
251                    mCameraDevice.addCallbackBuffer(new byte[bufSize]);
252                } catch (OutOfMemoryError e) {
253                    Log.v(TAG, "Buffer allocation failed: buffer " + i);
254                    break;
255                }
256            }
257        }
258    }
259
260    private boolean switchToOtherMode(int mode) {
261        if (isFinishing()) {
262            return false;
263        }
264        MenuHelper.gotoMode(mode, this);
265        finish();
266        return true;
267    }
268
269    public boolean onModeChanged(int mode) {
270        if (mode != ModePicker.MODE_PANORAMA) {
271            return switchToOtherMode(mode);
272        } else {
273            return true;
274        }
275    }
276
277    public void runViewFinder() {
278        mRealTimeMosaicView.setWarping(false);
279
280        // First update the surface texture...
281        mRealTimeMosaicView.updateSurfaceTexture();
282        // ...then call preprocess to render it to low-res and high-res RGB textures
283        mRealTimeMosaicView.preprocess();
284
285        mRealTimeMosaicView.setReady();
286        mRealTimeMosaicView.requestRender();
287    }
288
289    public void runMosaicCapture() {
290        mRealTimeMosaicView.setWarping(true);
291
292        // Lock the condition variable
293        mRealTimeMosaicView.lockPreviewReadyFlag();
294        // First update the surface texture...
295        mRealTimeMosaicView.updateSurfaceTexture();
296        // ...then call preprocess to render it to low-res and high-res RGB textures
297        mRealTimeMosaicView.preprocess();
298        // Now, transfer the textures from GPU to CPU memory for processing
299        mRealTimeMosaicView.transferGPUtoCPU();
300        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
301        mRealTimeMosaicView.waitUntilPreviewReady();
302
303        mMosaicFrameProcessor.processFrame(null);
304    }
305
306    synchronized public void onFrameAvailable(SurfaceTexture surface) {
307        /* For simplicity, SurfaceTexture calls here when it has new
308         * data available.  Call may come in from some random thread,
309         * so let's be safe and use synchronize. No OpenGL calls can be done here.
310         */
311        if (mCaptureState == CAPTURE_VIEWFINDER) {
312            runViewFinder();
313        } else {
314            runMosaicCapture();
315        }
316    }
317
318    public void startCapture() {
319        // Reset values so we can do this again.
320        mTimeTaken = System.currentTimeMillis();
321        mCaptureState = CAPTURE_MOSAIC;
322
323        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
324            @Override
325            public void onProgress(boolean isFinished, float translationRate, int traversedAngleX,
326                    int traversedAngleY) {
327                if (isFinished) {
328                    stopCapture();
329                } else {
330                    updateProgress(translationRate, traversedAngleX, traversedAngleY);
331                }
332            }
333        });
334
335        if (!mUseSurfaceTexture) {
336            // Preview callback used whenever new viewfinder frame is available
337            mCameraDevice.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
338                @Override
339                public void onPreviewFrame(final byte[] data, Camera camera) {
340                    mMosaicFrameProcessor.processFrame(data);
341                    // The returned buffer needs be added back to callback buffer
342                    // again.
343                    camera.addCallbackBuffer(data);
344                }
345            });
346        }
347
348        mCaptureLayout.setVisibility(View.VISIBLE);
349        mPreview.setVisibility(View.INVISIBLE);  // will be re-used, invisible is better than gone.
350        mRealTimeMosaicView.setVisibility(View.VISIBLE);
351        mPanoControlLayout.setVisibility(View.GONE);
352
353    }
354
355    private void stopCapture() {
356        mCaptureState = CAPTURE_VIEWFINDER;
357
358        mMosaicFrameProcessor.setProgressListener(null);
359        stopPreview();
360
361        if (!mUseSurfaceTexture) {
362            mCameraDevice.setPreviewCallbackWithBuffer(null);
363        }
364
365        mSurface.setOnFrameAvailableListener(null);
366
367        // TODO: show some dialog for long computation.
368        if (!mThreadRunning) {
369            mThreadRunning = true;
370            Thread t = new Thread() {
371                @Override
372                public void run() {
373                    byte[] jpegData = generateFinalMosaic(false);
374                    Bitmap bitmap = null;
375                    if (jpegData != null) {
376                        bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
377                    }
378                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
379                            MSG_FINAL_MOSAIC_READY, bitmap));
380                }
381            };
382            t.start();
383        }
384    }
385
386    private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY) {
387
388        mRealTimeMosaicView.setReady();
389        mRealTimeMosaicView.requestRender();
390
391        if (translationRate > 150) {
392            // TODO: remove the text and draw implications according to the UI
393            // spec.
394            mCaptureView.setStatusText("S L O W   D O W N");
395            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
396            mCaptureView.invalidate();
397        } else {
398            mCaptureView.setStatusText("");
399            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
400            mCaptureView.invalidate();
401        }
402    }
403
404    private void createContentView() {
405        setContentView(R.layout.panorama);
406
407        mCaptureState = CAPTURE_VIEWFINDER;
408
409        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
410        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
411
412        mPreview = (SurfaceView) findViewById(R.id.pano_preview_view);
413
414        mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
415        mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
416        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
417
418        mRealTimeMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
419        mRealTimeMosaicView.setUIObject(this);
420
421        mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button);
422        mShutterButton.setOnClickListener(new View.OnClickListener() {
423            @Override
424            public void onClick(View v) {
425                if (mPausing || mThreadRunning) return;
426                startCapture();
427            }
428        });
429
430        mPanoControlLayout = (View) findViewById(R.id.pano_control_layout);
431
432        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
433        mModePicker.setVisibility(View.VISIBLE);
434        mModePicker.setOnModeChangeListener(this);
435        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
436
437        mRealTimeMosaicView.setVisibility(View.VISIBLE);
438    }
439
440    @OnClickAttr
441    public void onStopButtonClicked(View v) {
442        if (mPausing || mThreadRunning) return;
443        stopCapture();
444    }
445
446    @OnClickAttr
447    public void onOkButtonClicked(View v) {
448        if (mPausing || mThreadRunning) return;
449        mThreadRunning = true;
450        Thread t = new Thread() {
451            @Override
452            public void run() {
453                byte[] jpegData = generateFinalMosaic(true);
454                savePanorama(jpegData);
455                mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW));
456            }
457        };
458        t.start();
459    }
460
461    @OnClickAttr
462    public void onRetakeButtonClicked(View v) {
463        if (mPausing || mThreadRunning) return;
464        resetToPreview();
465    }
466
467    private void resetToPreview() {
468        mCaptureState = CAPTURE_VIEWFINDER;
469
470        mReviewLayout.setVisibility(View.GONE);
471        mPreview.setVisibility(View.VISIBLE);
472        mPanoControlLayout.setVisibility(View.VISIBLE);
473        mCaptureLayout.setVisibility(View.GONE);
474        mMosaicFrameProcessor.reset();
475
476        mSurface.setOnFrameAvailableListener(this);
477
478        if (!mPausing) startPreview();
479
480        mRealTimeMosaicView.setVisibility(View.VISIBLE);
481    }
482
483    private void showFinalMosaic(Bitmap bitmap) {
484        if (bitmap != null) {
485            mReview.setImageBitmap(bitmap);
486        }
487        mCaptureLayout.setVisibility(View.GONE);
488        mPreview.setVisibility(View.INVISIBLE);
489        mReviewLayout.setVisibility(View.VISIBLE);
490        mCaptureView.setStatusText("");
491        mCaptureView.setSweepAngle(0);
492    }
493
494    private void savePanorama(byte[] jpegData) {
495        if (jpegData != null) {
496            String imagePath = PanoUtil.createName(
497                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
498            Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 0,
499                    jpegData);
500        }
501    }
502
503    private void clearMosaicFrameProcessorIfNeeded() {
504        if (!mPausing || mThreadRunning) return;
505        mMosaicFrameProcessor.clear();
506    }
507
508    private void initMosaicFrameProcessorIfNeeded() {
509        if (mPausing || mThreadRunning) return;
510        if (mMosaicFrameProcessor == null) {
511            // Start the activity for the first time.
512            mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5,
513                    mPreviewWidth, mPreviewHeight, getPreviewBufSize(), mUseSurfaceTexture);
514        }
515        mMosaicFrameProcessor.initialize();
516    }
517
518    @Override
519    protected void onPause() {
520        super.onPause();
521        mSurface.setOnFrameAvailableListener(null);
522        releaseCamera();
523        mPausing = true;
524
525        mRealTimeMosaicView.onPause();
526        mCaptureView.onPause();
527        mSensorManager.unregisterListener(mListener);
528        clearMosaicFrameProcessorIfNeeded();
529        System.gc();
530    }
531
532    @Override
533    protected void onResume() {
534        super.onResume();
535
536        mPausing = false;
537        /*
538         * It is not necessary to get accelerometer events at a very high rate,
539         * by using a slower rate (SENSOR_DELAY_UI), we get an automatic
540         * low-pass filter, which "extracts" the gravity component of the
541         * acceleration. As an added benefit, we use less power and CPU
542         * resources.
543         */
544        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
545        mCaptureState = CAPTURE_VIEWFINDER;
546        setupCamera();
547        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
548        // has to be decided by camera device.
549        initMosaicFrameProcessorIfNeeded();
550        mCaptureView.onResume();
551        mRealTimeMosaicView.onResume();
552    }
553
554    private final SensorEventListener mListener = new SensorEventListener() {
555        private float mCompassCurrX; // degrees
556        private float mCompassCurrY; // degrees
557        private float mTimestamp;
558
559        public void onSensorChanged(SensorEvent event) {
560            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
561                if (mTimestamp != 0) {
562                    final float dT = (event.timestamp - mTimestamp) * NS2S;
563                    mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
564                    mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
565                }
566                mTimestamp = event.timestamp;
567
568            } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
569                mCompassCurrX = event.values[0];
570                mCompassCurrY = event.values[1];
571            }
572
573            if (mMosaicFrameProcessor != null) {
574                mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY);
575            }
576        }
577
578        @Override
579        public void onAccuracyChanged(Sensor sensor, int accuracy) {
580        }
581    };
582
583    public byte[] generateFinalMosaic(boolean highRes) {
584        mMosaicFrameProcessor.createMosaic(highRes);
585
586        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
587        if (imageData == null) {
588            Log.e(TAG, "getFinalMosaicNV21() returned null.");
589            return null;
590        }
591
592        int len = imageData.length - 8;
593        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
594                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
595        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
596                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
597        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
598
599        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
600        ByteArrayOutputStream out = new ByteArrayOutputStream();
601        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
602        try {
603            out.close();
604        } catch (Exception e) {
605            Log.e(TAG, "Exception in storing final mosaic", e);
606            return null;
607        }
608        return out.toByteArray();
609    }
610
611    private void setPreviewTexture(SurfaceTexture surface) {
612        try {
613            mCameraDevice.setPreviewTexture(surface);
614        } catch (Throwable ex) {
615            releaseCamera();
616            throw new RuntimeException("setPreviewTexture failed", ex);
617        }
618    }
619
620    private void startPreview() {
621        // If we're previewing already, stop the preview first (this will blank
622        // the screen).
623        if (mCameraState != PREVIEW_STOPPED) stopPreview();
624
625        setPreviewTexture(mSurface);
626
627        try {
628            Log.v(TAG, "startPreview");
629            mCameraDevice.startPreview();
630        } catch (Throwable ex) {
631            releaseCamera();
632            throw new RuntimeException("startPreview failed", ex);
633        }
634        mCameraState = PREVIEW_ACTIVE;
635    }
636
637    private void stopPreview() {
638        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
639            Log.v(TAG, "stopPreview");
640            mCameraDevice.stopPreview();
641        }
642        mCameraState = PREVIEW_STOPPED;
643    }
644}
645