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