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