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