blob: bc0a58213011db88489a942f6626d91de593108e [file] [log] [blame]
Angus Konged15d1a2013-08-19 15:06:12 -07001/*
2 * Copyright (C) 2013 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;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.ImageFormat;
27import android.graphics.PixelFormat;
28import android.graphics.Point;
29import android.graphics.Rect;
30import android.graphics.SurfaceTexture;
31import android.graphics.YuvImage;
32import android.hardware.Camera.Parameters;
33import android.hardware.Camera.Size;
34import android.location.Location;
35import android.net.Uri;
36import android.os.AsyncTask;
37import android.os.Handler;
38import android.os.Message;
39import android.os.PowerManager;
40import android.util.Log;
41import android.view.KeyEvent;
42import android.view.OrientationEventListener;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.WindowManager;
46
Angus Kong20fad242013-11-11 18:23:46 -080047import com.android.camera.app.CameraManager.CameraProxy;
48import com.android.camera.app.AppController;
Angus Kongfd4fc0e2013-11-07 15:38:09 -080049import com.android.camera.app.MediaSaver;
Ruben Brunk7cfcafd2013-10-17 15:41:44 -070050import com.android.camera.data.LocalData;
Angus Konged15d1a2013-08-19 15:06:12 -070051import com.android.camera.exif.ExifInterface;
Angus Kong20fad242013-11-11 18:23:46 -080052import com.android.camera.module.ModuleController;
Angus Konged15d1a2013-08-19 15:06:12 -070053import com.android.camera.util.CameraUtil;
54import com.android.camera.util.UsageStatistics;
55import com.android.camera2.R;
56
57import java.io.ByteArrayOutputStream;
58import java.io.File;
59import java.io.IOException;
60import java.util.List;
61import java.util.TimeZone;
62
63/**
64 * Activity to handle panorama capturing.
65 */
66public class WideAnglePanoramaModule
Angus Kong20fad242013-11-11 18:23:46 -080067 implements CameraModule, ModuleController, WideAnglePanoramaController,
Angus Konged15d1a2013-08-19 15:06:12 -070068 SurfaceTexture.OnFrameAvailableListener {
69
70 public static final int DEFAULT_SWEEP_ANGLE = 160;
71 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
72 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
73
74 private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
75 private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
76 private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3;
77 private static final int MSG_CLEAR_SCREEN_DELAY = 4;
78 private static final int MSG_RESET_TO_PREVIEW = 5;
79
80 private static final int SCREEN_DELAY = 2 * 60 * 1000;
81
82 @SuppressWarnings("unused")
83 private static final String TAG = "CAM_WidePanoModule";
84 private static final int PREVIEW_STOPPED = 0;
85 private static final int PREVIEW_ACTIVE = 1;
86 public static final int CAPTURE_STATE_VIEWFINDER = 0;
87 public static final int CAPTURE_STATE_MOSAIC = 1;
88
89 // The unit of speed is degrees per frame.
90 private static final float PANNING_SPEED_THRESHOLD = 2.5f;
91 private static final boolean DEBUG = false;
92
93 private ContentResolver mContentResolver;
94 private WideAnglePanoramaUI mUI;
95
96 private MosaicPreviewRenderer mMosaicPreviewRenderer;
97 private Object mRendererLock = new Object();
98 private Object mWaitObject = new Object();
99
100 private String mPreparePreviewString;
101 private String mDialogTitle;
102 private String mDialogOkString;
103 private String mDialogPanoramaFailedString;
104 private String mDialogWaitingPreviousString;
105
106 private int mPreviewUIWidth;
107 private int mPreviewUIHeight;
108 private boolean mUsingFrontCamera;
109 private int mCameraPreviewWidth;
110 private int mCameraPreviewHeight;
111 private int mCameraState;
112 private int mCaptureState;
113 private PowerManager.WakeLock mPartialWakeLock;
114 private MosaicFrameProcessor mMosaicFrameProcessor;
115 private boolean mMosaicFrameProcessorInitialized;
116 private AsyncTask <Void, Void, Void> mWaitProcessorTask;
117 private long mTimeTaken;
118 private Handler mMainHandler;
119 private SurfaceTexture mCameraTexture;
120 private boolean mThreadRunning;
121 private boolean mCancelComputation;
122 private float mHorizontalViewAngle;
123 private float mVerticalViewAngle;
124
125 // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
126 // getting a better image quality by the former.
127 private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
128
129 private PanoOrientationEventListener mOrientationEventListener;
130 // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
131 // respectively.
132 private int mDeviceOrientation;
133 private int mDeviceOrientationAtCapture;
134 private int mCameraOrientation;
135 private int mOrientationCompensation;
136
137 private SoundClips.Player mSoundPlayer;
138
139 private Runnable mOnFrameAvailableRunnable;
140
141 private CameraActivity mActivity;
142 private View mRootView;
143 private CameraProxy mCameraDevice;
144 private boolean mPaused;
145
146 private LocationManager mLocationManager;
Angus Konged15d1a2013-08-19 15:06:12 -0700147 private ComboPreferences mPreferences;
148 private boolean mMosaicPreviewConfigured;
ztenghui68c1c1e2013-10-21 10:33:40 -0700149 private boolean mPreviewFocused = true;
Angus Konged15d1a2013-08-19 15:06:12 -0700150
151 @Override
152 public void onPreviewUIReady() {
153 configMosaicPreview();
154 }
155
156 @Override
157 public void onPreviewUIDestroyed() {
158
159 }
160
161 private class MosaicJpeg {
162 public MosaicJpeg(byte[] data, int width, int height) {
163 this.data = data;
164 this.width = width;
165 this.height = height;
166 this.isValid = true;
167 }
168
169 public MosaicJpeg() {
170 this.data = null;
171 this.width = 0;
172 this.height = 0;
173 this.isValid = false;
174 }
175
176 public final byte[] data;
177 public final int width;
178 public final int height;
179 public final boolean isValid;
180 }
181
182 private class PanoOrientationEventListener extends OrientationEventListener {
183 public PanoOrientationEventListener(Context context) {
184 super(context);
185 }
186
187 @Override
188 public void onOrientationChanged(int orientation) {
189 // We keep the last known orientation. So if the user first orient
190 // the camera then point the camera to floor or sky, we still have
191 // the correct orientation.
192 if (orientation == ORIENTATION_UNKNOWN) return;
193 mDeviceOrientation = CameraUtil.roundOrientation(orientation, mDeviceOrientation);
194 // When the screen is unlocked, display rotation may change. Always
195 // calculate the up-to-date orientationCompensation.
196 int orientationCompensation = mDeviceOrientation
197 + CameraUtil.getDisplayRotation(mActivity) % 360;
198 if (mOrientationCompensation != orientationCompensation) {
199 mOrientationCompensation = orientationCompensation;
200 }
201 }
202 }
203
204 @Override
205 public void init(CameraActivity activity, View parent) {
206 mActivity = activity;
207 mRootView = parent;
208
Angus Konged15d1a2013-08-19 15:06:12 -0700209 mCaptureState = CAPTURE_STATE_VIEWFINDER;
210 mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView);
211 mUI.setCaptureProgressOnDirectionChangeListener(
212 new PanoProgressBar.OnDirectionChangeListener() {
213 @Override
214 public void onDirectionChange(int direction) {
215 if (mCaptureState == CAPTURE_STATE_MOSAIC) {
216 mUI.showDirectionIndicators(direction);
217 }
218 }
219 });
220
221 mContentResolver = mActivity.getContentResolver();
222 // This runs in UI thread.
223 mOnFrameAvailableRunnable = new Runnable() {
224 @Override
225 public void run() {
226 // Frames might still be available after the activity is paused.
227 // If we call onFrameAvailable after pausing, the GL thread will crash.
228 if (mPaused) return;
229
230 MosaicPreviewRenderer renderer = null;
231 synchronized (mRendererLock) {
232 if (mMosaicPreviewRenderer == null) {
233 return;
234 }
235 renderer = mMosaicPreviewRenderer;
236 }
237 if (mRootView.getVisibility() != View.VISIBLE) {
238 renderer.showPreviewFrameSync();
239 mRootView.setVisibility(View.VISIBLE);
240 } else {
241 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
242 renderer.showPreviewFrame();
243 } else {
244 renderer.alignFrameSync();
245 mMosaicFrameProcessor.processFrame();
246 }
247 }
248 }
249 };
250
251 PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
252 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
253
254 mOrientationEventListener = new PanoOrientationEventListener(mActivity);
255
256 mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
257
258 Resources appRes = mActivity.getResources();
259 mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
260 mDialogTitle = appRes.getString(R.string.pano_dialog_title);
261 mDialogOkString = appRes.getString(R.string.dialog_ok);
262 mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
263 mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
264
265 mPreferences = new ComboPreferences(mActivity);
266 CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
267 mLocationManager = new LocationManager(mActivity, null);
268
269 mMainHandler = new Handler() {
270 @Override
271 public void handleMessage(Message msg) {
272 switch (msg.what) {
273 case MSG_LOW_RES_FINAL_MOSAIC_READY:
274 onBackgroundThreadFinished();
275 showFinalMosaic((Bitmap) msg.obj);
276 saveHighResMosaic();
277 break;
278 case MSG_GENERATE_FINAL_MOSAIC_ERROR:
279 onBackgroundThreadFinished();
280 if (mPaused) {
281 resetToPreviewIfPossible();
282 } else {
283 mUI.showAlertDialog(
284 mDialogTitle, mDialogPanoramaFailedString,
285 mDialogOkString, new Runnable() {
286 @Override
287 public void run() {
288 resetToPreviewIfPossible();
289 }
290 });
291 }
292 clearMosaicFrameProcessorIfNeeded();
293 break;
294 case MSG_END_DIALOG_RESET_TO_PREVIEW:
295 onBackgroundThreadFinished();
296 resetToPreviewIfPossible();
297 clearMosaicFrameProcessorIfNeeded();
298 break;
299 case MSG_CLEAR_SCREEN_DELAY:
300 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
301 FLAG_KEEP_SCREEN_ON);
302 break;
303 case MSG_RESET_TO_PREVIEW:
304 resetToPreviewIfPossible();
305 break;
306 }
307 }
308 };
309 }
310
311 @Override
ztenghui7b265a62013-09-09 14:58:44 -0700312 public void onPreviewFocusChanged(boolean previewFocused) {
Erin Dahlgren3044d8c2013-10-10 18:23:45 -0700313 mPreviewFocused = previewFocused;
314 mUI.onPreviewFocusChanged(previewFocused);
315 }
316
317 @Override
318 public boolean arePreviewControlsVisible() {
319 return mUI.arePreviewControlsVisible();
Angus Konged15d1a2013-08-19 15:06:12 -0700320 }
321
Angus Kong4f795b82013-09-16 14:25:35 -0700322 /**
323 * Opens camera and sets the parameters.
324 *
325 * @return Whether the camera was opened successfully.
326 */
327 private boolean setupCamera() {
328 if (!openCamera()) {
329 return false;
330 }
Angus Konged15d1a2013-08-19 15:06:12 -0700331 Parameters parameters = mCameraDevice.getParameters();
332 setupCaptureParams(parameters);
333 configureCamera(parameters);
Angus Kong4f795b82013-09-16 14:25:35 -0700334 return true;
Angus Konged15d1a2013-08-19 15:06:12 -0700335 }
336
337 private void releaseCamera() {
338 if (mCameraDevice != null) {
339 CameraHolder.instance().release();
340 mCameraDevice = null;
341 mCameraState = PREVIEW_STOPPED;
342 }
343 }
344
Angus Kong4f795b82013-09-16 14:25:35 -0700345 /**
346 * Opens the camera device. The back camera has priority over the front
347 * one.
348 *
349 * @return Whether the camera was opened successfully.
350 */
351 private boolean openCamera() {
Angus Konged15d1a2013-08-19 15:06:12 -0700352 int cameraId = CameraHolder.instance().getBackCameraId();
353 // If there is no back camera, use the first camera. Camera id starts
354 // from 0. Currently if a camera is not back facing, it is front facing.
355 // This is also forward compatible if we have a new facing other than
356 // back or front in the future.
357 if (cameraId == -1) cameraId = 0;
Angus Kong4f795b82013-09-16 14:25:35 -0700358 mCameraDevice = CameraUtil.openCamera(mActivity, cameraId,
359 mMainHandler, mActivity.getCameraOpenErrorCallback());
360 if (mCameraDevice == null) {
361 return false;
362 }
Angus Konged15d1a2013-08-19 15:06:12 -0700363 mCameraOrientation = CameraUtil.getCameraOrientation(cameraId);
364 if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
Angus Kong4f795b82013-09-16 14:25:35 -0700365 return true;
Angus Konged15d1a2013-08-19 15:06:12 -0700366 }
367
368 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
369 boolean needSmaller) {
370 int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
371 boolean hasFound = false;
372 for (Size size : supportedSizes) {
373 int h = size.height;
374 int w = size.width;
375 // we only want 4:3 format.
376 int d = DEFAULT_CAPTURE_PIXELS - h * w;
377 if (needSmaller && d < 0) { // no bigger preview than 960x720.
378 continue;
379 }
380 if (need4To3 && (h * 4 != w * 3)) {
381 continue;
382 }
383 d = Math.abs(d);
384 if (d < pixelsDiff) {
385 mCameraPreviewWidth = w;
386 mCameraPreviewHeight = h;
387 pixelsDiff = d;
388 hasFound = true;
389 }
390 }
391 return hasFound;
392 }
393
394 private void setupCaptureParams(Parameters parameters) {
395 List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
396 if (!findBestPreviewSize(supportedSizes, true, true)) {
397 Log.w(TAG, "No 4:3 ratio preview size supported.");
398 if (!findBestPreviewSize(supportedSizes, false, true)) {
399 Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
400 findBestPreviewSize(supportedSizes, false, false);
401 }
402 }
403 Log.d(TAG, "camera preview h = "
404 + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth);
405 parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight);
406
407 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
408 int last = frameRates.size() - 1;
409 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
410 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
411 parameters.setPreviewFpsRange(minFps, maxFps);
412 Log.d(TAG, "preview fps: " + minFps + ", " + maxFps);
413
414 List<String> supportedFocusModes = parameters.getSupportedFocusModes();
415 if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
416 parameters.setFocusMode(mTargetFocusMode);
417 } else {
418 // Use the default focus mode and log a message
419 Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
420 " becuase the mode is not supported.");
421 }
422
423 parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE);
424
425 mHorizontalViewAngle = parameters.getHorizontalViewAngle();
426 mVerticalViewAngle = parameters.getVerticalViewAngle();
427 }
428
429 public int getPreviewBufSize() {
430 PixelFormat pixelInfo = new PixelFormat();
431 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
432 // TODO: remove this extra 32 byte after the driver bug is fixed.
433 return (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
434 }
435
436 private void configureCamera(Parameters parameters) {
437 mCameraDevice.setParameters(parameters);
438 }
439
440 /**
441 * Configures the preview renderer according to the dimension defined by
442 * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}.
443 * Will stop the camera preview first.
444 */
445 private void configMosaicPreview() {
446 if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0
447 || mUI.getSurfaceTexture() == null) {
448 return;
449 }
450
451 stopCameraPreview();
452 synchronized (mRendererLock) {
453 if (mMosaicPreviewRenderer != null) {
454 mMosaicPreviewRenderer.release();
455 }
456 mMosaicPreviewRenderer = null;
457 }
458 final boolean isLandscape =
459 (mActivity.getResources().getConfiguration().orientation ==
460 Configuration.ORIENTATION_LANDSCAPE);
Doris Liu352b0142013-10-14 12:06:20 -0700461 mUI.flipPreviewIfNeeded();
Angus Konged15d1a2013-08-19 15:06:12 -0700462 MosaicPreviewRenderer renderer = new MosaicPreviewRenderer(
463 mUI.getSurfaceTexture(),
464 mPreviewUIWidth, mPreviewUIHeight, isLandscape);
465 synchronized (mRendererLock) {
466 mMosaicPreviewRenderer = renderer;
467 mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
468
469 if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
470 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
471 }
472 mRendererLock.notifyAll();
473 }
474 mMosaicPreviewConfigured = true;
475 resetToPreviewIfPossible();
476 }
477
478 /**
479 * Receives the layout change event from the preview area. So we can
480 * initialize the mosaic preview renderer.
481 */
482 @Override
483 public void onPreviewUILayoutChange(int l, int t, int r, int b) {
484 Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t));
485 mPreviewUIWidth = r - l;
486 mPreviewUIHeight = b - t;
487 configMosaicPreview();
488 }
489
490 @Override
491 public void onFrameAvailable(SurfaceTexture surface) {
492 /* This function may be called by some random thread,
493 * so let's be safe and jump back to ui thread.
494 * No OpenGL calls can be done here. */
495 mActivity.runOnUiThread(mOnFrameAvailableRunnable);
496 }
497
498 public void startCapture() {
499 // Reset values so we can do this again.
500 mCancelComputation = false;
501 mTimeTaken = System.currentTimeMillis();
502 mActivity.setSwipingEnabled(false);
503 mCaptureState = CAPTURE_STATE_MOSAIC;
504 mUI.onStartCapture();
505
506 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
507 @Override
508 public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
509 float progressX, float progressY) {
510 float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
511 float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
512 if (isFinished
513 || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
514 || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
515 stopCapture(false);
516 } else {
517 float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
518 float panningRateYInDegree = panningRateY * mVerticalViewAngle;
519 mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree,
520 accumulatedHorizontalAngle, accumulatedVerticalAngle,
521 PANNING_SPEED_THRESHOLD);
522 }
523 }
524 });
525
526 mUI.resetCaptureProgress();
527 // TODO: calculate the indicator width according to different devices to reflect the actual
528 // angle of view of the camera device.
529 mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE);
530 mUI.showCaptureProgress();
531 mDeviceOrientationAtCapture = mDeviceOrientation;
532 keepScreenOn();
533 // TODO: mActivity.getOrientationManager().lockOrientation();
Angus Kong9f1db522013-11-09 16:25:59 -0800534 mActivity.lockOrientation();
Angus Konged15d1a2013-08-19 15:06:12 -0700535 int degrees = CameraUtil.getDisplayRotation(mActivity);
536 int cameraId = CameraHolder.instance().getBackCameraId();
537 int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId);
538 mUI.setProgressOrientation(orientation);
539 }
540
541 private void stopCapture(boolean aborted) {
542 mCaptureState = CAPTURE_STATE_VIEWFINDER;
543 mUI.onStopCapture();
544
545 mMosaicFrameProcessor.setProgressListener(null);
546 stopCameraPreview();
547
548 mCameraTexture.setOnFrameAvailableListener(null);
549
550 if (!aborted && !mThreadRunning) {
551 mUI.showWaitingDialog(mPreparePreviewString);
552 // Hide shutter button, shutter icon, etc when waiting for
553 // panorama to stitch
554 mUI.hideUI();
555 runBackgroundThread(new Thread() {
556 @Override
557 public void run() {
558 MosaicJpeg jpeg = generateFinalMosaic(false);
559
560 if (jpeg != null && jpeg.isValid) {
561 Bitmap bitmap = null;
562 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
563 mMainHandler.sendMessage(mMainHandler.obtainMessage(
564 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
565 } else {
566 mMainHandler.sendMessage(mMainHandler.obtainMessage(
567 MSG_END_DIALOG_RESET_TO_PREVIEW));
568 }
569 }
570 });
571 }
572 keepScreenOnAwhile();
573 }
574
575 @Override
576 public void onShutterButtonClick() {
577 // If mCameraTexture == null then GL setup is not finished yet.
578 // No buttons can be pressed.
Angus Kong2dcc0a92013-09-25 13:00:08 -0700579 if (mPaused || mThreadRunning || mCameraTexture == null) {
580 return;
581 }
Angus Konged15d1a2013-08-19 15:06:12 -0700582 // Since this button will stay on the screen when capturing, we need to check the state
583 // right now.
584 switch (mCaptureState) {
585 case CAPTURE_STATE_VIEWFINDER:
Angus Kong2dcc0a92013-09-25 13:00:08 -0700586 final long storageSpaceBytes = mActivity.getStorageSpaceBytes();
587 if(storageSpaceBytes <= Storage.LOW_STORAGE_THRESHOLD_BYTES) {
588 Log.w(TAG, "Low storage warning: " + storageSpaceBytes);
589 return;
590 }
Angus Konged15d1a2013-08-19 15:06:12 -0700591 mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
592 startCapture();
593 break;
594 case CAPTURE_STATE_MOSAIC:
595 mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
596 stopCapture(false);
Angus Kong2dcc0a92013-09-25 13:00:08 -0700597 break;
598 default:
599 Log.w(TAG, "Unknown capture state: " + mCaptureState);
600 break;
Angus Konged15d1a2013-08-19 15:06:12 -0700601 }
602 }
603
604 public void reportProgress() {
605 mUI.resetSavingProgress();
606 Thread t = new Thread() {
607 @Override
608 public void run() {
609 while (mThreadRunning) {
610 final int progress = mMosaicFrameProcessor.reportProgress(
611 true, mCancelComputation);
612
613 try {
614 synchronized (mWaitObject) {
615 mWaitObject.wait(50);
616 }
617 } catch (InterruptedException e) {
618 throw new RuntimeException("Panorama reportProgress failed", e);
619 }
620 // Update the progress bar
621 mActivity.runOnUiThread(new Runnable() {
622 @Override
623 public void run() {
624 mUI.updateSavingProgress(progress);
625 }
626 });
627 }
628 }
629 };
630 t.start();
631 }
632
633 private int getCaptureOrientation() {
634 // The panorama image returned from the library is oriented based on the
635 // natural orientation of a camera. We need to set an orientation for the image
636 // in its EXIF header, so the image can be displayed correctly.
637 // The orientation is calculated from compensating the
638 // device orientation at capture and the camera orientation respective to
639 // the natural orientation of the device.
640 int orientation;
641 if (mUsingFrontCamera) {
642 // mCameraOrientation is negative with respect to the front facing camera.
643 // See document of android.hardware.Camera.Parameters.setRotation.
644 orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
645 } else {
646 orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
647 }
648 return orientation;
649 }
650
Doris Liu209a1652013-10-18 17:15:30 -0700651 /** The orientation of the camera image. The value is the angle that the camera
652 * image needs to be rotated clockwise so it shows correctly on the display
653 * in its natural orientation. It should be 0, 90, 180, or 270.*/
654 public int getCameraOrientation() {
655 return mCameraOrientation;
656 }
657
Angus Konged15d1a2013-08-19 15:06:12 -0700658 public void saveHighResMosaic() {
659 runBackgroundThread(new Thread() {
660 @Override
661 public void run() {
662 mPartialWakeLock.acquire();
663 MosaicJpeg jpeg;
664 try {
665 jpeg = generateFinalMosaic(true);
666 } finally {
667 mPartialWakeLock.release();
668 }
669
670 if (jpeg == null) { // Cancelled by user.
671 mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW);
672 } else if (!jpeg.isValid) { // Error when generating mosaic.
673 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
674 } else {
675 int orientation = getCaptureOrientation();
676 final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
677 if (uri != null) {
678 mActivity.runOnUiThread(new Runnable() {
679 @Override
680 public void run() {
681 mActivity.notifyNewMedia(uri);
682 }
683 });
684 }
685 mMainHandler.sendMessage(
686 mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW));
687 }
688 }
689 });
690 reportProgress();
691 }
692
693 private void runBackgroundThread(Thread thread) {
694 mThreadRunning = true;
695 thread.start();
696 }
697
698 private void onBackgroundThreadFinished() {
699 mThreadRunning = false;
700 mUI.dismissAllDialogs();
701 }
702
703 private void cancelHighResComputation() {
704 mCancelComputation = true;
705 synchronized (mWaitObject) {
706 mWaitObject.notify();
707 }
708 }
709
710 // This function will be called upon the first camera frame is available.
711 private void reset() {
712 mCaptureState = CAPTURE_STATE_VIEWFINDER;
713
Angus Kong9f1db522013-11-09 16:25:59 -0800714 mActivity.unlockOrientation();
Angus Konged15d1a2013-08-19 15:06:12 -0700715 mUI.reset();
716 mActivity.setSwipingEnabled(true);
717 // Orientation change will trigger onLayoutChange->configMosaicPreview->
718 // resetToPreview. Do not show the capture UI in film strip.
Erin Dahlgren3044d8c2013-10-10 18:23:45 -0700719 if (mPreviewFocused) {
720 mUI.showPreviewUI();
721 }
Angus Konged15d1a2013-08-19 15:06:12 -0700722 mMosaicFrameProcessor.reset();
723 }
724
725 private void resetToPreviewIfPossible() {
Doris Liu5737b932013-10-25 13:38:54 -0700726 reset();
Angus Konged15d1a2013-08-19 15:06:12 -0700727 if (!mMosaicFrameProcessorInitialized
728 || mUI.getSurfaceTexture() == null
729 || !mMosaicPreviewConfigured) {
730 return;
731 }
Angus Kong2dcc0a92013-09-25 13:00:08 -0700732 if (!mPaused) {
733 startCameraPreview();
734 }
Angus Konged15d1a2013-08-19 15:06:12 -0700735 }
736
737 private void showFinalMosaic(Bitmap bitmap) {
738 mUI.showFinalMosaic(bitmap, getCaptureOrientation());
739 }
740
741 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
742 if (jpegData != null) {
743 String filename = PanoUtil.createName(
744 mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
745 String filepath = Storage.generateFilepath(filename);
746
Seth Raphaelcbd82672013-11-05 10:12:36 -0800747 UsageStatistics.onEvent(UsageStatistics.COMPONENT_PANORAMA,
748 UsageStatistics.ACTION_CAPTURE_DONE, null, 0,
749 UsageStatistics.hashFileName(filename + ".jpg"));
750
Angus Konged15d1a2013-08-19 15:06:12 -0700751 Location loc = mLocationManager.getCurrentLocation();
752 ExifInterface exif = new ExifInterface();
753 try {
754 exif.readExif(jpegData);
755 exif.addGpsDateTimeStampTag(mTimeTaken);
756 exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken,
757 TimeZone.getDefault());
758 exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
759 ExifInterface.getOrientationValueForRotation(orientation)));
760 writeLocation(loc, exif);
761 exif.writeExif(jpegData, filepath);
762 } catch (IOException e) {
763 Log.e(TAG, "Cannot set exif for " + filepath, e);
764 Storage.writeFile(filepath, jpegData);
765 }
766 int jpegLength = (int) (new File(filepath).length());
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700767 return Storage.addImage(mContentResolver, filename, mTimeTaken, loc, orientation,
768 jpegLength, filepath, width, height, LocalData.MIME_TYPE_JPEG);
Angus Konged15d1a2013-08-19 15:06:12 -0700769 }
770 return null;
771 }
772
773 private static void writeLocation(Location location, ExifInterface exif) {
774 if (location == null) {
775 return;
776 }
777 exif.addGpsTags(location.getLatitude(), location.getLongitude());
778 exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()));
779 }
780
781 private void clearMosaicFrameProcessorIfNeeded() {
782 if (!mPaused || mThreadRunning) return;
783 // Only clear the processor if it is initialized by this activity
784 // instance. Other activity instances may be using it.
785 if (mMosaicFrameProcessorInitialized) {
786 mMosaicFrameProcessor.clear();
787 mMosaicFrameProcessorInitialized = false;
788 }
789 }
790
791 private void initMosaicFrameProcessorIfNeeded() {
792 if (mPaused || mThreadRunning) {
793 return;
794 }
795
796 mMosaicFrameProcessor.initialize(
797 mCameraPreviewWidth, mCameraPreviewHeight, getPreviewBufSize());
798 mMosaicFrameProcessorInitialized = true;
799 }
800
801 @Override
802 public void onPauseBeforeSuper() {
803 mPaused = true;
804 if (mLocationManager != null) mLocationManager.recordLocation(false);
805 }
806
807 @Override
808 public void onPauseAfterSuper() {
809 mOrientationEventListener.disable();
810 if (mCameraDevice == null) {
811 // Camera open failed. Nothing should be done here.
812 return;
813 }
814 // Stop the capturing first.
815 if (mCaptureState == CAPTURE_STATE_MOSAIC) {
816 stopCapture(true);
817 reset();
818 }
Doris Liu3a45c332013-10-15 19:10:28 -0700819 mUI.showPreviewCover();
Angus Konged15d1a2013-08-19 15:06:12 -0700820 releaseCamera();
821 synchronized (mRendererLock) {
822 mCameraTexture = null;
823
824 // The preview renderer might not have a chance to be initialized
825 // before onPause().
826 if (mMosaicPreviewRenderer != null) {
827 mMosaicPreviewRenderer.release();
828 mMosaicPreviewRenderer = null;
829 }
830 }
831
832 clearMosaicFrameProcessorIfNeeded();
833 if (mWaitProcessorTask != null) {
834 mWaitProcessorTask.cancel(true);
835 mWaitProcessorTask = null;
836 }
837 resetScreenOn();
Doris Liu352b0142013-10-14 12:06:20 -0700838 mUI.removeDisplayChangeListener();
Angus Konged15d1a2013-08-19 15:06:12 -0700839 if (mSoundPlayer != null) {
840 mSoundPlayer.release();
841 mSoundPlayer = null;
842 }
843 System.gc();
844 }
845
846 @Override
Angus Kong20fad242013-11-11 18:23:46 -0800847 public void init(AppController app, boolean isSecureCamera, boolean isCaptureIntent) {
848 // TODO: implement this.
849 init((CameraActivity) app.getAndroidContext(), app.getModuleLayoutRoot());
850 }
851
852 @Override
853 public void resume() {
854 // TODO: implement this.
855 onResumeBeforeSuper();
856 onResumeAfterSuper();
857 }
858
859 @Override
860 public void pause() {
861 // TODO: implement this.
862 onPauseBeforeSuper();
863 onPauseAfterSuper();
864 }
865
866 @Override
867 public void destroy() {
868 // TODO: implement this.
869 }
870
871 @Override
872 public void onPreviewSizeChanged(int width, int height) {
873 // TODO: implement this.
874 }
875
876 @Override
Angus Konged15d1a2013-08-19 15:06:12 -0700877 public void onConfigurationChanged(Configuration newConfig) {
878 mUI.onConfigurationChanged(newConfig, mThreadRunning);
879 }
880
881 @Override
882 public void onOrientationChanged(int orientation) {
883 }
884
885 @Override
Angus Kong20fad242013-11-11 18:23:46 -0800886 public void onCameraAvailable(CameraProxy cameraProxy) {
887 // TODO: implement this.
888 }
889
890 @Override
Angus Konged15d1a2013-08-19 15:06:12 -0700891 public void onResumeBeforeSuper() {
892 mPaused = false;
893 }
894
895 @Override
896 public void onResumeAfterSuper() {
897 mOrientationEventListener.enable();
898
899 mCaptureState = CAPTURE_STATE_VIEWFINDER;
900
Angus Kong4f795b82013-09-16 14:25:35 -0700901 if (!setupCamera()) {
902 Log.e(TAG, "Failed to open camera, aborting");
Angus Konged15d1a2013-08-19 15:06:12 -0700903 return;
904 }
905
906 // Set up sound playback for shutter button
907 mSoundPlayer = SoundClips.getPlayer(mActivity);
908
909 // Check if another panorama instance is using the mosaic frame processor.
910 mUI.dismissAllDialogs();
911 if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
912 mUI.showWaitingDialog(mDialogWaitingPreviousString);
913 // If stitching is still going on, make sure switcher and shutter button
914 // are not showing
915 mUI.hideUI();
916 mWaitProcessorTask = new WaitProcessorTask().execute();
917 } else {
918 // Camera must be initialized before MosaicFrameProcessor is
919 // initialized. The preview size has to be decided by camera device.
920 initMosaicFrameProcessorIfNeeded();
921 Point size = mUI.getPreviewAreaSize();
922 mPreviewUIWidth = size.x;
923 mPreviewUIHeight = size.y;
924 configMosaicPreview();
Angus Kong2dcc0a92013-09-25 13:00:08 -0700925 mActivity.updateStorageSpaceAndHint();
Angus Konged15d1a2013-08-19 15:06:12 -0700926 }
927 keepScreenOnAwhile();
928
Angus Konged15d1a2013-08-19 15:06:12 -0700929 // Initialize location service.
930 boolean recordLocation = RecordLocationPreference.get(mPreferences,
931 mContentResolver);
932 mLocationManager.recordLocation(recordLocation);
Doris Liu352b0142013-10-14 12:06:20 -0700933 mUI.initDisplayChangeListener();
Angus Konged15d1a2013-08-19 15:06:12 -0700934 UsageStatistics.onContentViewChanged(
935 UsageStatistics.COMPONENT_CAMERA, "PanoramaModule");
936 }
937
938 /**
939 * Generate the final mosaic image.
940 *
941 * @param highRes flag to indicate whether we want to get a high-res version.
942 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
943 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
944 * is an error in generating the final mosaic.
945 */
946 public MosaicJpeg generateFinalMosaic(boolean highRes) {
947 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
948 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
949 return null;
950 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
951 return new MosaicJpeg();
952 }
953
954 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
955 if (imageData == null) {
956 Log.e(TAG, "getFinalMosaicNV21() returned null.");
957 return new MosaicJpeg();
958 }
959
960 int len = imageData.length - 8;
961 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
962 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
963 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
964 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
965 Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
966
967 if (width <= 0 || height <= 0) {
968 // TODO: pop up an error message indicating that the final result is not generated.
969 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
970 height);
971 return new MosaicJpeg();
972 }
973
974 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
975 ByteArrayOutputStream out = new ByteArrayOutputStream();
976 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
977 try {
978 out.close();
979 } catch (Exception e) {
980 Log.e(TAG, "Exception in storing final mosaic", e);
981 return new MosaicJpeg();
982 }
983 return new MosaicJpeg(out.toByteArray(), width, height);
984 }
985
986 private void startCameraPreview() {
987 if (mCameraDevice == null) {
988 // Camera open failed. Return.
989 return;
990 }
991
992 if (mUI.getSurfaceTexture() == null) {
993 // UI is not ready.
994 return;
995 }
996
997 // This works around a driver issue. startPreview may fail if
998 // stopPreview/setPreviewTexture/startPreview are called several times
999 // in a row. mCameraTexture can be null after pressing home during
1000 // mosaic generation and coming back. Preview will be started later in
1001 // onLayoutChange->configMosaicPreview. This also reduces the latency.
1002 synchronized (mRendererLock) {
1003 if (mCameraTexture == null) return;
1004
1005 // If we're previewing already, stop the preview first (this will
1006 // blank the screen).
1007 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1008
1009 // Set the display orientation to 0, so that the underlying mosaic
1010 // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight
1011 // image data from SurfaceTexture.
1012 mCameraDevice.setDisplayOrientation(0);
1013
1014 mCameraTexture.setOnFrameAvailableListener(this);
1015 mCameraDevice.setPreviewTexture(mCameraTexture);
1016 }
1017 mCameraDevice.startPreview();
1018 mCameraState = PREVIEW_ACTIVE;
1019 }
1020
1021 private void stopCameraPreview() {
1022 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1023 mCameraDevice.stopPreview();
1024 }
1025 mCameraState = PREVIEW_STOPPED;
1026 }
1027
1028 @Override
1029 public void onUserInteraction() {
1030 if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1031 }
1032
1033 @Override
1034 public boolean onBackPressed() {
1035 // If panorama is generating low res or high res mosaic, ignore back
1036 // key. So the activity will not be destroyed.
1037 if (mThreadRunning) return true;
1038 return false;
1039 }
1040
1041 private void resetScreenOn() {
1042 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1043 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1044 }
1045
1046 private void keepScreenOnAwhile() {
1047 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1048 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1049 mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1050 }
1051
1052 private void keepScreenOn() {
1053 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1054 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1055 }
1056
1057 private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
1058 @Override
1059 protected Void doInBackground(Void... params) {
1060 synchronized (mMosaicFrameProcessor) {
1061 while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
1062 try {
1063 mMosaicFrameProcessor.wait();
1064 } catch (Exception e) {
1065 // ignore
1066 }
1067 }
1068 }
Angus Kong2dcc0a92013-09-25 13:00:08 -07001069 mActivity.updateStorageSpace();
Angus Konged15d1a2013-08-19 15:06:12 -07001070 return null;
1071 }
1072
1073 @Override
1074 protected void onPostExecute(Void result) {
1075 mWaitProcessorTask = null;
1076 mUI.dismissAllDialogs();
1077 // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE);
1078 initMosaicFrameProcessorIfNeeded();
1079 Point size = mUI.getPreviewAreaSize();
1080 mPreviewUIWidth = size.x;
1081 mPreviewUIHeight = size.y;
1082 configMosaicPreview();
1083 resetToPreviewIfPossible();
Angus Kong2dcc0a92013-09-25 13:00:08 -07001084 mActivity.updateStorageHint(mActivity.getStorageSpaceBytes());
Angus Konged15d1a2013-08-19 15:06:12 -07001085 }
1086 }
1087
1088 @Override
1089 public void cancelHighResStitching() {
1090 if (mPaused || mCameraTexture == null) return;
1091 cancelHighResComputation();
1092 }
1093
1094 @Override
1095 public void onStop() {
1096 }
1097
1098 @Override
1099 public void installIntentFilter() {
1100 }
1101
1102 @Override
1103 public void onActivityResult(int requestCode, int resultCode, Intent data) {
1104 }
1105
1106
1107 @Override
1108 public boolean onKeyDown(int keyCode, KeyEvent event) {
1109 return false;
1110 }
1111
1112 @Override
1113 public boolean onKeyUp(int keyCode, KeyEvent event) {
1114 return false;
1115 }
1116
1117 @Override
1118 public void onSingleTapUp(View view, int x, int y) {
1119 }
1120
1121 @Override
1122 public void onPreviewTextureCopied() {
1123 }
1124
1125 @Override
1126 public void onCaptureTextureCopied() {
1127 }
1128
1129 @Override
1130 public boolean updateStorageHintOnResume() {
1131 return false;
1132 }
1133
1134 @Override
Angus Konged15d1a2013-08-19 15:06:12 -07001135 public void onShowSwitcherPopup() {
1136 }
1137
1138 @Override
Angus Kongfd4fc0e2013-11-07 15:38:09 -08001139 public void onMediaSaverAvailable(MediaSaver s) {
Angus Konged15d1a2013-08-19 15:06:12 -07001140 // do nothing.
1141 }
1142}