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