PanoramaActivity.java revision ead4a28d6c99e95707149563d89b563c372b870a
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 android.app.Activity;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.ImageFormat;
23import android.graphics.Matrix;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.graphics.YuvImage;
27import android.hardware.Camera;
28import android.hardware.Camera.Parameters;
29import android.hardware.Camera.Size;
30import android.hardware.Sensor;
31import android.hardware.SensorEvent;
32import android.hardware.SensorEventListener;
33import android.hardware.SensorManager;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.Message;
38import android.util.Log;
39import android.view.SurfaceHolder;
40import android.view.SurfaceView;
41import android.view.View;
42import android.view.WindowManager;
43import android.widget.ImageView;
44
45import com.android.camera.CameraDisabledException;
46import com.android.camera.CameraHardwareException;
47import com.android.camera.CameraHolder;
48import com.android.camera.MenuHelper;
49import com.android.camera.ModePicker;
50import com.android.camera.R;
51import com.android.camera.ShutterButton;
52import com.android.camera.Storage;
53import com.android.camera.Util;
54
55import java.io.ByteArrayOutputStream;
56import java.util.List;
57
58public class PanoramaActivity extends Activity implements
59        ModePicker.OnModeChangeListener, SurfaceHolder.Callback {
60    public static final int DEFAULT_SWEEP_ANGLE = 60;
61    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
62    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
63
64    private static final int MSG_FINAL_MOSAIC_READY = 1;
65
66    private static final String TAG = "PanoramaActivity";
67
68    // Ratio of nanosecond to second
69    private static final float NS2S = 1.0f / 1000000000.0f;
70
71    private static final int PREVIEW_STOPPED = 0;
72    private static final int PREVIEW_ACTIVE = 1;
73
74    private SurfaceView mPreview;
75    private ImageView mReview;
76    private CaptureView mCaptureView;
77
78    private ShutterButton mShutterButton;
79    private int mPreviewWidth;
80    private int mPreviewHeight;
81    private Camera mCameraDevice;
82    private int mCameraState;
83    private SensorManager mSensorManager;
84    private Sensor mSensor;
85    private ModePicker mModePicker;
86    private MosaicFrameProcessor mMosaicFrameProcessor;
87    private String mCurrentImagePath = null;
88    private long mTimeTaken;
89    private Handler mMainHandler;
90    private SurfaceHolder mSurfaceHolder;
91
92    @Override
93    public void onCreate(Bundle icicle) {
94        super.onCreate(icicle);
95
96        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
97                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
98
99        createContentView();
100
101        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
102        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
103        if (mSensor == null) {
104            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
105        }
106
107        mMainHandler = new Handler() {
108            @Override
109            public void handleMessage(Message msg) {
110                switch (msg.what) {
111                    case MSG_FINAL_MOSAIC_READY:
112                        Uri uri = (Uri) msg.obj;
113                        showFinalMosaic(uri);
114                }
115            }
116        };
117    }
118
119    private void setupCamera() {
120        openCamera();
121        Parameters parameters = mCameraDevice.getParameters();
122        setupCaptureParams(parameters);
123        configureCamera(parameters);
124    }
125
126    private void releaseCamera() {
127        if (mCameraDevice != null) {
128            CameraHolder.instance().release();
129            mCameraDevice = null;
130            mCameraState = PREVIEW_STOPPED;
131        }
132    }
133
134    private void openCamera() {
135        try {
136            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
137        } catch (CameraHardwareException e) {
138            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
139            return;
140        } catch (CameraDisabledException e) {
141            Util.showErrorAndFinish(this, R.string.camera_disabled);
142            return;
143        }
144    }
145
146    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
147            boolean needSmaller) {
148        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
149        boolean hasFound = false;
150        for (Size size : supportedSizes) {
151            int h = size.height;
152            int w = size.width;
153            // we only want 4:3 format.
154            int d = DEFAULT_CAPTURE_PIXELS - h * w;
155            if (needSmaller && d < 0) { // no bigger preview than 960x720.
156                continue;
157            }
158            if (need4To3 && (h * 4 != w * 3)) {
159                continue;
160            }
161            d = Math.abs(d);
162            if (d < pixelsDiff) {
163                mPreviewWidth = w;
164                mPreviewHeight = h;
165                pixelsDiff = d;
166                hasFound = true;
167            }
168        }
169        return hasFound;
170    }
171
172    private void setupCaptureParams(Parameters parameters) {
173        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
174        if (!findBestPreviewSize(supportedSizes, true, true)) {
175            Log.w(TAG, "No 4:3 ratio preview size supported.");
176            if (!findBestPreviewSize(supportedSizes, false, true)) {
177                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
178                findBestPreviewSize(supportedSizes, false, false);
179            }
180        }
181        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
182        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
183
184        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
185        int last = frameRates.size() - 1;
186        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
187        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
188        parameters.setPreviewFpsRange(minFps, maxFps);
189        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
190
191        // TODO: use camera API after it is published.
192        parameters.set("recording-hint", "false");
193    }
194
195    public int getPreviewBufSize() {
196        PixelFormat pixelInfo = new PixelFormat();
197        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
198        // TODO: remove this extra 32 byte after the driver bug is fixed.
199        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
200    }
201
202    private void configureCamera(Parameters parameters) {
203        mCameraDevice.setParameters(parameters);
204
205        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
206                CameraHolder.instance().getBackCameraId());
207        mCameraDevice.setDisplayOrientation(orientation);
208
209        int bufSize = getPreviewBufSize();
210        Log.v(TAG, "BufSize = " + bufSize);
211        for (int i = 0; i < 10; i++) {
212            try {
213                mCameraDevice.addCallbackBuffer(new byte[bufSize]);
214            } catch (OutOfMemoryError e) {
215                Log.v(TAG, "Buffer allocation failed: buffer " + i);
216                break;
217            }
218        }
219    }
220
221    private boolean switchToOtherMode(int mode) {
222        if (isFinishing())
223            return false;
224        MenuHelper.gotoMode(mode, this);
225        finish();
226        return true;
227    }
228
229    public boolean onModeChanged(int mode) {
230        if (mode != ModePicker.MODE_PANORAMA) {
231            return switchToOtherMode(mode);
232        } else {
233            return true;
234        }
235    }
236
237    public void setCaptureStarted() {
238        // Reset values so we can do this again.
239        mTimeTaken = System.currentTimeMillis();
240
241        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
242            @Override
243            public void onProgress(boolean isFinished, float translationRate, int traversedAngleX,
244                    int traversedAngleY, Bitmap lowResBitmapAlpha, Matrix transformaMatrix) {
245                if (isFinished) {
246                    onMosaicFinished();
247                } else {
248                    updateProgress(translationRate, traversedAngleX, traversedAngleY,
249                            lowResBitmapAlpha, transformaMatrix);
250                }
251            }
252        });
253
254        // Preview callback used whenever new viewfinder frame is available
255        mCameraDevice.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
256            @Override
257            public void onPreviewFrame(final byte[] data, Camera camera) {
258                mMosaicFrameProcessor.processFrame(data, mPreviewWidth, mPreviewHeight);
259                // The returned buffer needs be added back to callback buffer
260                // again.
261                camera.addCallbackBuffer(data);
262            }
263        });
264
265        mCaptureView.setVisibility(View.VISIBLE);
266    }
267
268    private void onMosaicFinished() {
269        mMosaicFrameProcessor.setProgressListener(null);
270        mCameraDevice.stopPreview();
271        mCameraDevice.setPreviewCallbackWithBuffer(null);
272        // TODO: set mPreview invisible after the camera driver bug is fixed.
273        mCaptureView.setVisibility(View.INVISIBLE);
274        mCaptureView.setBitmap(null);
275        mCaptureView.setStatusText("");
276        mCaptureView.setSweepAngle(0);
277        mCaptureView.invalidate();
278        // TODO: show some dialog for long computation.
279        Thread t = new Thread() {
280            @Override
281            public void run() {
282                generateAndStoreFinalMosaic(false);
283            }
284        };
285        t.start();
286    }
287
288    private void updateProgress(float translationRate, int traversedAngleX, int traversedAngleY,
289            Bitmap lowResBitmapAlpha, Matrix transformationMatrix) {
290        mCaptureView.setBitmap(lowResBitmapAlpha, transformationMatrix);
291        if (translationRate > 150) {
292            // TODO: remove the text and draw implications according to the UI
293            // spec.
294            mCaptureView.setStatusText("S L O W   D O W N");
295            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
296            mCaptureView.invalidate();
297        } else {
298            mCaptureView.setStatusText("");
299            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
300            mCaptureView.invalidate();
301        }
302    }
303
304    private void createContentView() {
305        setContentView(R.layout.panorama);
306
307        mPreview = (SurfaceView) findViewById(R.id.pano_preview);
308        mPreview.getHolder().addCallback(this);
309
310        mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
311        mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
312        mCaptureView.setVisibility(View.INVISIBLE);
313
314        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
315        mReview.setVisibility(View.INVISIBLE);
316
317        mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button);
318        mShutterButton.setOnClickListener(new View.OnClickListener() {
319            @Override
320            public void onClick(View v) {
321                setCaptureStarted();
322            }
323        });
324        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
325        mModePicker.setVisibility(View.VISIBLE);
326        mModePicker.setOnModeChangeListener(this);
327        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
328    }
329
330    private void showFinalMosaic(Uri uri) {
331        mReview.setImageURI(uri);
332        mReview.setVisibility(View.VISIBLE);
333        mCaptureView.setVisibility(View.INVISIBLE);
334    }
335
336    @Override
337    protected void onPause() {
338        super.onPause();
339        releaseCamera();
340        mMosaicFrameProcessor.onPause();
341        mCaptureView.onPause();
342        mSensorManager.unregisterListener(mListener);
343        System.gc();
344    }
345
346    @Override
347    protected void onResume() {
348        super.onResume();
349
350        /*
351         * It is not necessary to get accelerometer events at a very high rate,
352         * by using a slower rate (SENSOR_DELAY_UI), we get an automatic
353         * low-pass filter, which "extracts" the gravity component of the
354         * acceleration. As an added benefit, we use less power and CPU
355         * resources.
356         */
357        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
358
359        setupCamera();
360        startPreview();
361
362        if (mMosaicFrameProcessor == null) {
363            // Start the activity for the first time.
364            mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5,
365                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
366            mMosaicFrameProcessor.onResume();
367        } else {
368            mMosaicFrameProcessor.onResume();
369        }
370        mCaptureView.onResume();
371    }
372
373    private final SensorEventListener mListener = new SensorEventListener() {
374        private float mCompassCurrX; // degrees
375        private float mCompassCurrY; // degrees
376        private float mTimestamp;
377
378        public void onSensorChanged(SensorEvent event) {
379            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
380                if (mTimestamp != 0) {
381                    final float dT = (event.timestamp - mTimestamp) * NS2S;
382                    mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
383                    mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
384                }
385                mTimestamp = event.timestamp;
386
387            } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
388                mCompassCurrX = event.values[0];
389                mCompassCurrY = event.values[1];
390            }
391
392            if (mMosaicFrameProcessor != null) {
393                mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY);
394            }
395        }
396
397        @Override
398        public void onAccuracyChanged(Sensor sensor, int accuracy) {
399        }
400    };
401
402    public void generateAndStoreFinalMosaic(boolean highRes) {
403        mMosaicFrameProcessor.createMosaic(highRes);
404
405        mCurrentImagePath = PanoUtil.createName(
406                getResources().getString(R.string.pano_file_name_format), mTimeTaken);
407
408        if (highRes) {
409            mCurrentImagePath += "_HR";
410        } else {
411            mCurrentImagePath += "_LR";
412        }
413
414            byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
415            int len = imageData.length - 8;
416
417            int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
418                    + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
419            int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
420                    + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
421            Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
422
423            YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
424            ByteArrayOutputStream out = new ByteArrayOutputStream();
425            yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
426            try {
427                out.close();
428            } catch (Exception e) {
429                Log.e(TAG, "Exception in storing final mosaic", e);
430                return;
431            }
432            Uri uri = Storage.addImage(
433                    getContentResolver(), mCurrentImagePath, mTimeTaken, null, 0,
434                    out.toByteArray());
435
436            mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_FINAL_MOSAIC_READY, uri));
437            // Now's a good time to run the GC. Since we won't do any explicit
438            // allocation during the test, the GC should stay dormant and not
439            // influence our results.
440            System.runFinalization();
441            System.gc();
442    }
443
444    @Override
445    public void surfaceCreated(SurfaceHolder holder) {
446    }
447
448    @Override
449    public void surfaceDestroyed(SurfaceHolder holder) {
450    }
451
452    @Override
453    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
454        mSurfaceHolder = holder;
455
456        if (mCameraDevice == null) return;
457
458        // Set preview display if the surface is being created. Preview was
459        // already started. Also restart the preview if display rotation has
460        // changed. Sometimes this happens when the device is held in portrait
461        // and camera app is opened. Rotation animation takes some time and
462        // display rotation in onCreate may not be what we want.
463        if (holder.isCreating()) {
464            // Set preview display if the surface is being created and preview
465            // was already started. That means preview display was set to null
466            // and we need to set it now.
467            setPreviewDisplay(holder);
468        } else {
469            // 1. Restart the preview if the size of surface was changed. The
470            // framework may not support changing preview display on the fly.
471            // 2. Start the preview now if surface was destroyed and preview
472            // stopped.
473            startPreview();
474        }
475    }
476
477    private void setPreviewDisplay(SurfaceHolder holder) {
478        try {
479            mCameraDevice.setPreviewDisplay(holder);
480        } catch (Throwable ex) {
481            releaseCamera();
482            throw new RuntimeException("setPreviewDisplay failed", ex);
483        }
484    }
485
486    private void startPreview() {
487        // If we're previewing already, stop the preview first (this will blank
488        // the screen).
489        if (mCameraState != PREVIEW_STOPPED) stopPreview();
490
491        setPreviewDisplay(mSurfaceHolder);
492
493        try {
494            Log.v(TAG, "startPreview");
495            mCameraDevice.startPreview();
496        } catch (Throwable ex) {
497            releaseCamera();
498            throw new RuntimeException("startPreview failed", ex);
499        }
500        mCameraState = PREVIEW_ACTIVE;
501    }
502
503    private void stopPreview() {
504        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
505            Log.v(TAG, "stopPreview");
506            mCameraDevice.stopPreview();
507        }
508        mCameraState = PREVIEW_STOPPED;
509    }
510}
511