blob: 22756d1b67a1f433bc0562b8a5882d94fd791542 [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;
Angus Konged15d1a2013-08-19 15:06:12 -070021import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.ImageFormat;
26import android.graphics.PixelFormat;
27import android.graphics.Point;
28import android.graphics.Rect;
29import android.graphics.SurfaceTexture;
30import android.graphics.YuvImage;
31import android.hardware.Camera.Parameters;
32import android.hardware.Camera.Size;
33import android.location.Location;
34import android.net.Uri;
35import android.os.AsyncTask;
36import android.os.Handler;
37import android.os.Message;
38import android.os.PowerManager;
39import android.util.Log;
40import android.view.KeyEvent;
41import android.view.OrientationEventListener;
42import android.view.View;
43import android.view.ViewGroup;
Angus Konged15d1a2013-08-19 15:06:12 -070044
Angus Kong20fad242013-11-11 18:23:46 -080045import com.android.camera.app.CameraManager.CameraProxy;
46import com.android.camera.app.AppController;
Angus Kongfd4fc0e2013-11-07 15:38:09 -080047import com.android.camera.app.MediaSaver;
Ruben Brunk7cfcafd2013-10-17 15:41:44 -070048import com.android.camera.data.LocalData;
Angus Konged15d1a2013-08-19 15:06:12 -070049import com.android.camera.exif.ExifInterface;
Angus Kong20fad242013-11-11 18:23:46 -080050import com.android.camera.module.ModuleController;
Angus Konged15d1a2013-08-19 15:06:12 -070051import com.android.camera.util.CameraUtil;
52import com.android.camera.util.UsageStatistics;
53import com.android.camera2.R;
54
55import java.io.ByteArrayOutputStream;
56import java.io.File;
57import java.io.IOException;
58import java.util.List;
59import java.util.TimeZone;
60
61/**
62 * Activity to handle panorama capturing.
63 */
64public class WideAnglePanoramaModule
Sascha Haeberling280fd3e2013-11-21 13:52:15 -080065 extends CameraModule
66 implements ModuleController,
67 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;
Angus Kong13e87c42013-11-25 10:02:47 -080077 private static final int MSG_RESET_TO_PREVIEW = 4;
Angus Konged15d1a2013-08-19 15:06:12 -070078
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;
Sascha Haeberling280fd3e2013-11-21 13:52:15 -080096 private final Object mRendererLock = new Object();
97 private final Object mWaitObject = new Object();
Angus Konged15d1a2013-08-19 15:06:12 -070098
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.
Sascha Haeberling280fd3e2013-11-21 13:52:15 -0800126 private final String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
Angus Konged15d1a2013-08-19 15:06:12 -0700127
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;
Angus Konged15d1a2013-08-19 15:06:12 -0700146 private ComboPreferences mPreferences;
147 private boolean mMosaicPreviewConfigured;
ztenghui68c1c1e2013-10-21 10:33:40 -0700148 private boolean mPreviewFocused = true;
Angus Konged15d1a2013-08-19 15:06:12 -0700149
Sascha Haeberling280fd3e2013-11-21 13:52:15 -0800150 /**
151 * Constructs a new Wide-Angle panorama module.
152 */
Angus Kongc4e66562013-11-22 23:03:21 -0800153 public WideAnglePanoramaModule(AppController app) {
154 super(app);
Sascha Haeberling280fd3e2013-11-21 13:52:15 -0800155 }
156
Angus Konged15d1a2013-08-19 15:06:12 -0700157 @Override
158 public void onPreviewUIReady() {
159 configMosaicPreview();
160 }
161
162 @Override
163 public void onPreviewUIDestroyed() {
164
165 }
166
167 private class MosaicJpeg {
168 public MosaicJpeg(byte[] data, int width, int height) {
169 this.data = data;
170 this.width = width;
171 this.height = height;
172 this.isValid = true;
173 }
174
175 public MosaicJpeg() {
176 this.data = null;
177 this.width = 0;
178 this.height = 0;
179 this.isValid = false;
180 }
181
182 public final byte[] data;
183 public final int width;
184 public final int height;
185 public final boolean isValid;
186 }
187
188 private class PanoOrientationEventListener extends OrientationEventListener {
189 public PanoOrientationEventListener(Context context) {
190 super(context);
191 }
192
193 @Override
194 public void onOrientationChanged(int orientation) {
195 // We keep the last known orientation. So if the user first orient
196 // the camera then point the camera to floor or sky, we still have
197 // the correct orientation.
198 if (orientation == ORIENTATION_UNKNOWN) return;
199 mDeviceOrientation = CameraUtil.roundOrientation(orientation, mDeviceOrientation);
200 // When the screen is unlocked, display rotation may change. Always
201 // calculate the up-to-date orientationCompensation.
202 int orientationCompensation = mDeviceOrientation
203 + CameraUtil.getDisplayRotation(mActivity) % 360;
204 if (mOrientationCompensation != orientationCompensation) {
205 mOrientationCompensation = orientationCompensation;
206 }
207 }
208 }
209
210 @Override
Angus Kong13e87c42013-11-25 10:02:47 -0800211 public void init(AppController app, boolean isSecureCamera, boolean isCaptureIntent) {
212 mActivity = (CameraActivity) app.getAndroidContext();
213 mRootView = app.getModuleLayoutRoot();
Angus Konged15d1a2013-08-19 15:06:12 -0700214
Angus Konged15d1a2013-08-19 15:06:12 -0700215 mCaptureState = CAPTURE_STATE_VIEWFINDER;
216 mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView);
217 mUI.setCaptureProgressOnDirectionChangeListener(
218 new PanoProgressBar.OnDirectionChangeListener() {
219 @Override
220 public void onDirectionChange(int direction) {
221 if (mCaptureState == CAPTURE_STATE_MOSAIC) {
222 mUI.showDirectionIndicators(direction);
223 }
224 }
225 });
226
227 mContentResolver = mActivity.getContentResolver();
228 // This runs in UI thread.
229 mOnFrameAvailableRunnable = new Runnable() {
230 @Override
231 public void run() {
232 // Frames might still be available after the activity is paused.
233 // If we call onFrameAvailable after pausing, the GL thread will crash.
234 if (mPaused) return;
235
236 MosaicPreviewRenderer renderer = null;
237 synchronized (mRendererLock) {
238 if (mMosaicPreviewRenderer == null) {
239 return;
240 }
241 renderer = mMosaicPreviewRenderer;
242 }
243 if (mRootView.getVisibility() != View.VISIBLE) {
244 renderer.showPreviewFrameSync();
245 mRootView.setVisibility(View.VISIBLE);
246 } else {
247 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
248 renderer.showPreviewFrame();
249 } else {
250 renderer.alignFrameSync();
251 mMosaicFrameProcessor.processFrame();
252 }
253 }
254 }
255 };
256
257 PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
258 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
259
260 mOrientationEventListener = new PanoOrientationEventListener(mActivity);
261
262 mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
263
264 Resources appRes = mActivity.getResources();
265 mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
266 mDialogTitle = appRes.getString(R.string.pano_dialog_title);
267 mDialogOkString = appRes.getString(R.string.dialog_ok);
268 mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
269 mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
270
271 mPreferences = new ComboPreferences(mActivity);
272 CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
Erin Dahlgren21c21a62013-11-19 16:37:38 -0800273 mLocationManager = mActivity.getLocationManager();
Angus Konged15d1a2013-08-19 15:06:12 -0700274
275 mMainHandler = new Handler() {
276 @Override
277 public void handleMessage(Message msg) {
278 switch (msg.what) {
279 case MSG_LOW_RES_FINAL_MOSAIC_READY:
280 onBackgroundThreadFinished();
281 showFinalMosaic((Bitmap) msg.obj);
282 saveHighResMosaic();
283 break;
284 case MSG_GENERATE_FINAL_MOSAIC_ERROR:
285 onBackgroundThreadFinished();
286 if (mPaused) {
287 resetToPreviewIfPossible();
288 } else {
289 mUI.showAlertDialog(
290 mDialogTitle, mDialogPanoramaFailedString,
291 mDialogOkString, new Runnable() {
292 @Override
293 public void run() {
294 resetToPreviewIfPossible();
295 }
296 });
297 }
298 clearMosaicFrameProcessorIfNeeded();
299 break;
300 case MSG_END_DIALOG_RESET_TO_PREVIEW:
301 onBackgroundThreadFinished();
302 resetToPreviewIfPossible();
303 clearMosaicFrameProcessorIfNeeded();
304 break;
Angus Konged15d1a2013-08-19 15:06:12 -0700305 case MSG_RESET_TO_PREVIEW:
306 resetToPreviewIfPossible();
307 break;
308 }
309 }
310 };
311 }
312
313 @Override
ztenghui7b265a62013-09-09 14:58:44 -0700314 public void onPreviewFocusChanged(boolean previewFocused) {
Erin Dahlgren3044d8c2013-10-10 18:23:45 -0700315 mPreviewFocused = previewFocused;
316 mUI.onPreviewFocusChanged(previewFocused);
317 }
318
319 @Override
320 public boolean arePreviewControlsVisible() {
321 return mUI.arePreviewControlsVisible();
Angus Konged15d1a2013-08-19 15:06:12 -0700322 }
323
Angus Kong4f795b82013-09-16 14:25:35 -0700324 /**
325 * Opens camera and sets the parameters.
326 *
327 * @return Whether the camera was opened successfully.
328 */
329 private boolean setupCamera() {
330 if (!openCamera()) {
331 return false;
332 }
Angus Konged15d1a2013-08-19 15:06:12 -0700333 Parameters parameters = mCameraDevice.getParameters();
334 setupCaptureParams(parameters);
335 configureCamera(parameters);
Angus Kong4f795b82013-09-16 14:25:35 -0700336 return true;
Angus Konged15d1a2013-08-19 15:06:12 -0700337 }
338
339 private void releaseCamera() {
340 if (mCameraDevice != null) {
341 CameraHolder.instance().release();
342 mCameraDevice = null;
343 mCameraState = PREVIEW_STOPPED;
344 }
345 }
346
Angus Kong4f795b82013-09-16 14:25:35 -0700347 /**
348 * Opens the camera device. The back camera has priority over the front
349 * one.
350 *
351 * @return Whether the camera was opened successfully.
352 */
353 private boolean openCamera() {
Angus Konged15d1a2013-08-19 15:06:12 -0700354 int cameraId = CameraHolder.instance().getBackCameraId();
355 // If there is no back camera, use the first camera. Camera id starts
356 // from 0. Currently if a camera is not back facing, it is front facing.
357 // This is also forward compatible if we have a new facing other than
358 // back or front in the future.
359 if (cameraId == -1) cameraId = 0;
Angus Kong4f795b82013-09-16 14:25:35 -0700360 mCameraDevice = CameraUtil.openCamera(mActivity, cameraId,
361 mMainHandler, mActivity.getCameraOpenErrorCallback());
362 if (mCameraDevice == null) {
363 return false;
364 }
Angus Konged15d1a2013-08-19 15:06:12 -0700365 mCameraOrientation = CameraUtil.getCameraOrientation(cameraId);
366 if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
Angus Kong4f795b82013-09-16 14:25:35 -0700367 return true;
Angus Konged15d1a2013-08-19 15:06:12 -0700368 }
369
370 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
371 boolean needSmaller) {
372 int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
373 boolean hasFound = false;
374 for (Size size : supportedSizes) {
375 int h = size.height;
376 int w = size.width;
377 // we only want 4:3 format.
378 int d = DEFAULT_CAPTURE_PIXELS - h * w;
379 if (needSmaller && d < 0) { // no bigger preview than 960x720.
380 continue;
381 }
382 if (need4To3 && (h * 4 != w * 3)) {
383 continue;
384 }
385 d = Math.abs(d);
386 if (d < pixelsDiff) {
387 mCameraPreviewWidth = w;
388 mCameraPreviewHeight = h;
389 pixelsDiff = d;
390 hasFound = true;
391 }
392 }
393 return hasFound;
394 }
395
396 private void setupCaptureParams(Parameters parameters) {
397 List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
398 if (!findBestPreviewSize(supportedSizes, true, true)) {
399 Log.w(TAG, "No 4:3 ratio preview size supported.");
400 if (!findBestPreviewSize(supportedSizes, false, true)) {
401 Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
402 findBestPreviewSize(supportedSizes, false, false);
403 }
404 }
405 Log.d(TAG, "camera preview h = "
406 + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth);
407 parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight);
408
409 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
410 int last = frameRates.size() - 1;
411 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
412 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
413 parameters.setPreviewFpsRange(minFps, maxFps);
414 Log.d(TAG, "preview fps: " + minFps + ", " + maxFps);
415
416 List<String> supportedFocusModes = parameters.getSupportedFocusModes();
417 if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
418 parameters.setFocusMode(mTargetFocusMode);
419 } else {
420 // Use the default focus mode and log a message
421 Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
422 " becuase the mode is not supported.");
423 }
424
425 parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE);
426
427 mHorizontalViewAngle = parameters.getHorizontalViewAngle();
428 mVerticalViewAngle = parameters.getVerticalViewAngle();
429 }
430
431 public int getPreviewBufSize() {
432 PixelFormat pixelInfo = new PixelFormat();
433 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
434 // TODO: remove this extra 32 byte after the driver bug is fixed.
435 return (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
436 }
437
438 private void configureCamera(Parameters parameters) {
439 mCameraDevice.setParameters(parameters);
440 }
441
442 /**
443 * Configures the preview renderer according to the dimension defined by
444 * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}.
445 * Will stop the camera preview first.
446 */
447 private void configMosaicPreview() {
448 if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0
449 || mUI.getSurfaceTexture() == null) {
450 return;
451 }
452
453 stopCameraPreview();
454 synchronized (mRendererLock) {
455 if (mMosaicPreviewRenderer != null) {
456 mMosaicPreviewRenderer.release();
457 }
458 mMosaicPreviewRenderer = null;
459 }
460 final boolean isLandscape =
461 (mActivity.getResources().getConfiguration().orientation ==
462 Configuration.ORIENTATION_LANDSCAPE);
Doris Liu352b0142013-10-14 12:06:20 -0700463 mUI.flipPreviewIfNeeded();
Angus Konged15d1a2013-08-19 15:06:12 -0700464 MosaicPreviewRenderer renderer = new MosaicPreviewRenderer(
465 mUI.getSurfaceTexture(),
466 mPreviewUIWidth, mPreviewUIHeight, isLandscape);
467 synchronized (mRendererLock) {
468 mMosaicPreviewRenderer = renderer;
469 mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
470
471 if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
472 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
473 }
474 mRendererLock.notifyAll();
475 }
476 mMosaicPreviewConfigured = true;
477 resetToPreviewIfPossible();
478 }
479
480 /**
481 * Receives the layout change event from the preview area. So we can
482 * initialize the mosaic preview renderer.
483 */
484 @Override
485 public void onPreviewUILayoutChange(int l, int t, int r, int b) {
486 Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t));
487 mPreviewUIWidth = r - l;
488 mPreviewUIHeight = b - t;
489 configMosaicPreview();
490 }
491
492 @Override
493 public void onFrameAvailable(SurfaceTexture surface) {
494 /* This function may be called by some random thread,
495 * so let's be safe and jump back to ui thread.
496 * No OpenGL calls can be done here. */
497 mActivity.runOnUiThread(mOnFrameAvailableRunnable);
498 }
499
500 public void startCapture() {
501 // Reset values so we can do this again.
502 mCancelComputation = false;
503 mTimeTaken = System.currentTimeMillis();
504 mActivity.setSwipingEnabled(false);
505 mCaptureState = CAPTURE_STATE_MOSAIC;
506 mUI.onStartCapture();
507
508 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
509 @Override
510 public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
511 float progressX, float progressY) {
512 float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
513 float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
514 if (isFinished
515 || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
516 || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
517 stopCapture(false);
518 } else {
519 float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
520 float panningRateYInDegree = panningRateY * mVerticalViewAngle;
521 mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree,
522 accumulatedHorizontalAngle, accumulatedVerticalAngle,
523 PANNING_SPEED_THRESHOLD);
524 }
525 }
526 });
527
528 mUI.resetCaptureProgress();
529 // TODO: calculate the indicator width according to different devices to reflect the actual
530 // angle of view of the camera device.
531 mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE);
532 mUI.showCaptureProgress();
533 mDeviceOrientationAtCapture = mDeviceOrientation;
Angus Kong13e87c42013-11-25 10:02:47 -0800534 mActivity.enableKeepScreenOn(true);
Angus Konged15d1a2013-08-19 15:06:12 -0700535 // TODO: mActivity.getOrientationManager().lockOrientation();
Angus Kong9f1db522013-11-09 16:25:59 -0800536 mActivity.lockOrientation();
Angus Konged15d1a2013-08-19 15:06:12 -0700537 int degrees = CameraUtil.getDisplayRotation(mActivity);
538 int cameraId = CameraHolder.instance().getBackCameraId();
539 int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId);
540 mUI.setProgressOrientation(orientation);
541 }
542
543 private void stopCapture(boolean aborted) {
544 mCaptureState = CAPTURE_STATE_VIEWFINDER;
545 mUI.onStopCapture();
546
547 mMosaicFrameProcessor.setProgressListener(null);
548 stopCameraPreview();
549
550 mCameraTexture.setOnFrameAvailableListener(null);
551
552 if (!aborted && !mThreadRunning) {
553 mUI.showWaitingDialog(mPreparePreviewString);
554 // Hide shutter button, shutter icon, etc when waiting for
555 // panorama to stitch
556 mUI.hideUI();
557 runBackgroundThread(new Thread() {
558 @Override
559 public void run() {
560 MosaicJpeg jpeg = generateFinalMosaic(false);
561
562 if (jpeg != null && jpeg.isValid) {
563 Bitmap bitmap = null;
564 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
565 mMainHandler.sendMessage(mMainHandler.obtainMessage(
566 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
567 } else {
568 mMainHandler.sendMessage(mMainHandler.obtainMessage(
569 MSG_END_DIALOG_RESET_TO_PREVIEW));
570 }
571 }
572 });
573 }
Angus Kong13e87c42013-11-25 10:02:47 -0800574 mActivity.enableKeepScreenOn(false);
Angus Konged15d1a2013-08-19 15:06:12 -0700575 }
576
577 @Override
578 public void onShutterButtonClick() {
579 // If mCameraTexture == null then GL setup is not finished yet.
580 // No buttons can be pressed.
Angus Kong2dcc0a92013-09-25 13:00:08 -0700581 if (mPaused || mThreadRunning || mCameraTexture == null) {
582 return;
583 }
Angus Konged15d1a2013-08-19 15:06:12 -0700584 // Since this button will stay on the screen when capturing, we need to check the state
585 // right now.
586 switch (mCaptureState) {
587 case CAPTURE_STATE_VIEWFINDER:
Angus Kong2dcc0a92013-09-25 13:00:08 -0700588 final long storageSpaceBytes = mActivity.getStorageSpaceBytes();
589 if(storageSpaceBytes <= Storage.LOW_STORAGE_THRESHOLD_BYTES) {
590 Log.w(TAG, "Low storage warning: " + storageSpaceBytes);
591 return;
592 }
Angus Konged15d1a2013-08-19 15:06:12 -0700593 mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
594 startCapture();
595 break;
596 case CAPTURE_STATE_MOSAIC:
597 mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
598 stopCapture(false);
Angus Kong2dcc0a92013-09-25 13:00:08 -0700599 break;
600 default:
601 Log.w(TAG, "Unknown capture state: " + mCaptureState);
602 break;
Angus Konged15d1a2013-08-19 15:06:12 -0700603 }
604 }
605
606 public void reportProgress() {
607 mUI.resetSavingProgress();
608 Thread t = new Thread() {
609 @Override
610 public void run() {
611 while (mThreadRunning) {
612 final int progress = mMosaicFrameProcessor.reportProgress(
613 true, mCancelComputation);
614
615 try {
616 synchronized (mWaitObject) {
617 mWaitObject.wait(50);
618 }
619 } catch (InterruptedException e) {
620 throw new RuntimeException("Panorama reportProgress failed", e);
621 }
622 // Update the progress bar
623 mActivity.runOnUiThread(new Runnable() {
624 @Override
625 public void run() {
626 mUI.updateSavingProgress(progress);
627 }
628 });
629 }
630 }
631 };
632 t.start();
633 }
634
635 private int getCaptureOrientation() {
636 // The panorama image returned from the library is oriented based on the
637 // natural orientation of a camera. We need to set an orientation for the image
638 // in its EXIF header, so the image can be displayed correctly.
639 // The orientation is calculated from compensating the
640 // device orientation at capture and the camera orientation respective to
641 // the natural orientation of the device.
642 int orientation;
643 if (mUsingFrontCamera) {
644 // mCameraOrientation is negative with respect to the front facing camera.
645 // See document of android.hardware.Camera.Parameters.setRotation.
646 orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
647 } else {
648 orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
649 }
650 return orientation;
651 }
652
Doris Liu209a1652013-10-18 17:15:30 -0700653 /** The orientation of the camera image. The value is the angle that the camera
654 * image needs to be rotated clockwise so it shows correctly on the display
655 * in its natural orientation. It should be 0, 90, 180, or 270.*/
Sascha Haeberling280fd3e2013-11-21 13:52:15 -0800656 @Override
Doris Liu209a1652013-10-18 17:15:30 -0700657 public int getCameraOrientation() {
658 return mCameraOrientation;
659 }
660
Angus Konged15d1a2013-08-19 15:06:12 -0700661 public void saveHighResMosaic() {
662 runBackgroundThread(new Thread() {
663 @Override
664 public void run() {
665 mPartialWakeLock.acquire();
666 MosaicJpeg jpeg;
667 try {
668 jpeg = generateFinalMosaic(true);
669 } finally {
670 mPartialWakeLock.release();
671 }
672
673 if (jpeg == null) { // Cancelled by user.
674 mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW);
675 } else if (!jpeg.isValid) { // Error when generating mosaic.
676 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
677 } else {
678 int orientation = getCaptureOrientation();
679 final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
680 if (uri != null) {
681 mActivity.runOnUiThread(new Runnable() {
682 @Override
683 public void run() {
684 mActivity.notifyNewMedia(uri);
685 }
686 });
687 }
688 mMainHandler.sendMessage(
689 mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW));
690 }
691 }
692 });
693 reportProgress();
694 }
695
696 private void runBackgroundThread(Thread thread) {
697 mThreadRunning = true;
698 thread.start();
699 }
700
701 private void onBackgroundThreadFinished() {
702 mThreadRunning = false;
703 mUI.dismissAllDialogs();
704 }
705
706 private void cancelHighResComputation() {
707 mCancelComputation = true;
708 synchronized (mWaitObject) {
709 mWaitObject.notify();
710 }
711 }
712
713 // This function will be called upon the first camera frame is available.
714 private void reset() {
715 mCaptureState = CAPTURE_STATE_VIEWFINDER;
716
Angus Kong9f1db522013-11-09 16:25:59 -0800717 mActivity.unlockOrientation();
Angus Konged15d1a2013-08-19 15:06:12 -0700718 mUI.reset();
719 mActivity.setSwipingEnabled(true);
720 // Orientation change will trigger onLayoutChange->configMosaicPreview->
721 // resetToPreview. Do not show the capture UI in film strip.
Erin Dahlgren3044d8c2013-10-10 18:23:45 -0700722 if (mPreviewFocused) {
723 mUI.showPreviewUI();
724 }
Angus Konged15d1a2013-08-19 15:06:12 -0700725 mMosaicFrameProcessor.reset();
726 }
727
728 private void resetToPreviewIfPossible() {
Doris Liu5737b932013-10-25 13:38:54 -0700729 reset();
Angus Konged15d1a2013-08-19 15:06:12 -0700730 if (!mMosaicFrameProcessorInitialized
731 || mUI.getSurfaceTexture() == null
732 || !mMosaicPreviewConfigured) {
733 return;
734 }
Angus Kong2dcc0a92013-09-25 13:00:08 -0700735 if (!mPaused) {
736 startCameraPreview();
737 }
Angus Konged15d1a2013-08-19 15:06:12 -0700738 }
739
740 private void showFinalMosaic(Bitmap bitmap) {
741 mUI.showFinalMosaic(bitmap, getCaptureOrientation());
742 }
743
744 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
745 if (jpegData != null) {
746 String filename = PanoUtil.createName(
747 mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
748 String filepath = Storage.generateFilepath(filename);
749
Seth Raphaelcbd82672013-11-05 10:12:36 -0800750 UsageStatistics.onEvent(UsageStatistics.COMPONENT_PANORAMA,
751 UsageStatistics.ACTION_CAPTURE_DONE, null, 0,
752 UsageStatistics.hashFileName(filename + ".jpg"));
753
Angus Konged15d1a2013-08-19 15:06:12 -0700754 Location loc = mLocationManager.getCurrentLocation();
755 ExifInterface exif = new ExifInterface();
756 try {
757 exif.readExif(jpegData);
758 exif.addGpsDateTimeStampTag(mTimeTaken);
759 exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken,
760 TimeZone.getDefault());
761 exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
762 ExifInterface.getOrientationValueForRotation(orientation)));
763 writeLocation(loc, exif);
764 exif.writeExif(jpegData, filepath);
765 } catch (IOException e) {
766 Log.e(TAG, "Cannot set exif for " + filepath, e);
767 Storage.writeFile(filepath, jpegData);
768 }
769 int jpegLength = (int) (new File(filepath).length());
Ruben Brunk7cfcafd2013-10-17 15:41:44 -0700770 return Storage.addImage(mContentResolver, filename, mTimeTaken, loc, orientation,
771 jpegLength, filepath, width, height, LocalData.MIME_TYPE_JPEG);
Angus Konged15d1a2013-08-19 15:06:12 -0700772 }
773 return null;
774 }
775
776 private static void writeLocation(Location location, ExifInterface exif) {
777 if (location == null) {
778 return;
779 }
780 exif.addGpsTags(location.getLatitude(), location.getLongitude());
781 exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()));
782 }
783
784 private void clearMosaicFrameProcessorIfNeeded() {
785 if (!mPaused || mThreadRunning) return;
786 // Only clear the processor if it is initialized by this activity
787 // instance. Other activity instances may be using it.
788 if (mMosaicFrameProcessorInitialized) {
789 mMosaicFrameProcessor.clear();
790 mMosaicFrameProcessorInitialized = false;
791 }
792 }
793
794 private void initMosaicFrameProcessorIfNeeded() {
795 if (mPaused || mThreadRunning) {
796 return;
797 }
798
799 mMosaicFrameProcessor.initialize(
800 mCameraPreviewWidth, mCameraPreviewHeight, getPreviewBufSize());
801 mMosaicFrameProcessorInitialized = true;
802 }
803
804 @Override
Angus Kong20fad242013-11-11 18:23:46 -0800805 public void resume() {
Angus Konged15d1a2013-08-19 15:06:12 -0700806 mPaused = false;
Angus Konged15d1a2013-08-19 15:06:12 -0700807 mOrientationEventListener.enable();
808
809 mCaptureState = CAPTURE_STATE_VIEWFINDER;
810
Angus Kong4f795b82013-09-16 14:25:35 -0700811 if (!setupCamera()) {
812 Log.e(TAG, "Failed to open camera, aborting");
Angus Konged15d1a2013-08-19 15:06:12 -0700813 return;
814 }
815
816 // Set up sound playback for shutter button
817 mSoundPlayer = SoundClips.getPlayer(mActivity);
818
819 // Check if another panorama instance is using the mosaic frame processor.
820 mUI.dismissAllDialogs();
821 if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
822 mUI.showWaitingDialog(mDialogWaitingPreviousString);
823 // If stitching is still going on, make sure switcher and shutter button
824 // are not showing
825 mUI.hideUI();
826 mWaitProcessorTask = new WaitProcessorTask().execute();
827 } else {
828 // Camera must be initialized before MosaicFrameProcessor is
829 // initialized. The preview size has to be decided by camera device.
830 initMosaicFrameProcessorIfNeeded();
831 Point size = mUI.getPreviewAreaSize();
832 mPreviewUIWidth = size.x;
833 mPreviewUIHeight = size.y;
834 configMosaicPreview();
Angus Kong2dcc0a92013-09-25 13:00:08 -0700835 mActivity.updateStorageSpaceAndHint();
Angus Konged15d1a2013-08-19 15:06:12 -0700836 }
Angus Konged15d1a2013-08-19 15:06:12 -0700837
Angus Konged15d1a2013-08-19 15:06:12 -0700838 // Initialize location service.
839 boolean recordLocation = RecordLocationPreference.get(mPreferences,
840 mContentResolver);
841 mLocationManager.recordLocation(recordLocation);
Doris Liu352b0142013-10-14 12:06:20 -0700842 mUI.initDisplayChangeListener();
Angus Konged15d1a2013-08-19 15:06:12 -0700843 UsageStatistics.onContentViewChanged(
844 UsageStatistics.COMPONENT_CAMERA, "PanoramaModule");
845 }
846
Angus Kongc4e66562013-11-22 23:03:21 -0800847 @Override
848 public void pause() {
849 mPaused = true;
850 if (mLocationManager != null) mLocationManager.recordLocation(false);
851 mOrientationEventListener.disable();
852 if (mCameraDevice == null) {
853 // Camera open failed. Nothing should be done here.
854 return;
855 }
856 // Stop the capturing first.
857 if (mCaptureState == CAPTURE_STATE_MOSAIC) {
858 stopCapture(true);
859 reset();
860 }
861 mUI.showPreviewCover();
862 releaseCamera();
863 synchronized (mRendererLock) {
864 mCameraTexture = null;
865
866 // The preview renderer might not have a chance to be initialized
867 // before onPause().
868 if (mMosaicPreviewRenderer != null) {
869 mMosaicPreviewRenderer.release();
870 mMosaicPreviewRenderer = null;
871 }
872 }
873
874 clearMosaicFrameProcessorIfNeeded();
875 if (mWaitProcessorTask != null) {
876 mWaitProcessorTask.cancel(true);
877 mWaitProcessorTask = null;
878 }
Angus Kong13e87c42013-11-25 10:02:47 -0800879 mActivity.enableKeepScreenOn(false);
Angus Kongc4e66562013-11-22 23:03:21 -0800880 mUI.removeDisplayChangeListener();
881 if (mSoundPlayer != null) {
882 mSoundPlayer.release();
883 mSoundPlayer = null;
884 }
885 System.gc();
886 }
887
888 @Override
889 public void destroy() {
890 // TODO: implement this.
891 }
892
893 @Override
894 public void onPreviewSizeChanged(int width, int height) {
895 // TODO: implement this.
896 }
897
898 @Override
899 public void onConfigurationChanged(Configuration newConfig) {
900 mUI.onConfigurationChanged(newConfig, mThreadRunning);
901 }
902
903 @Override
904 public void onOrientationChanged(int orientation) {
905 }
906
907 @Override
908 public void onCameraAvailable(CameraProxy cameraProxy) {
909 // TODO: implement this.
910 }
911
Angus Konged15d1a2013-08-19 15:06:12 -0700912 /**
913 * Generate the final mosaic image.
914 *
915 * @param highRes flag to indicate whether we want to get a high-res version.
916 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
917 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
918 * is an error in generating the final mosaic.
919 */
920 public MosaicJpeg generateFinalMosaic(boolean highRes) {
921 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
922 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
923 return null;
924 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
925 return new MosaicJpeg();
926 }
927
928 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
929 if (imageData == null) {
930 Log.e(TAG, "getFinalMosaicNV21() returned null.");
931 return new MosaicJpeg();
932 }
933
934 int len = imageData.length - 8;
935 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
936 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
937 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
938 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
939 Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
940
941 if (width <= 0 || height <= 0) {
942 // TODO: pop up an error message indicating that the final result is not generated.
943 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
944 height);
945 return new MosaicJpeg();
946 }
947
948 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
949 ByteArrayOutputStream out = new ByteArrayOutputStream();
950 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
951 try {
952 out.close();
953 } catch (Exception e) {
954 Log.e(TAG, "Exception in storing final mosaic", e);
955 return new MosaicJpeg();
956 }
957 return new MosaicJpeg(out.toByteArray(), width, height);
958 }
959
960 private void startCameraPreview() {
961 if (mCameraDevice == null) {
962 // Camera open failed. Return.
963 return;
964 }
965
966 if (mUI.getSurfaceTexture() == null) {
967 // UI is not ready.
968 return;
969 }
970
971 // This works around a driver issue. startPreview may fail if
972 // stopPreview/setPreviewTexture/startPreview are called several times
973 // in a row. mCameraTexture can be null after pressing home during
974 // mosaic generation and coming back. Preview will be started later in
975 // onLayoutChange->configMosaicPreview. This also reduces the latency.
976 synchronized (mRendererLock) {
977 if (mCameraTexture == null) return;
978
979 // If we're previewing already, stop the preview first (this will
980 // blank the screen).
981 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
982
983 // Set the display orientation to 0, so that the underlying mosaic
984 // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight
985 // image data from SurfaceTexture.
986 mCameraDevice.setDisplayOrientation(0);
987
988 mCameraTexture.setOnFrameAvailableListener(this);
989 mCameraDevice.setPreviewTexture(mCameraTexture);
990 }
991 mCameraDevice.startPreview();
992 mCameraState = PREVIEW_ACTIVE;
993 }
994
995 private void stopCameraPreview() {
996 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
997 mCameraDevice.stopPreview();
998 }
999 mCameraState = PREVIEW_STOPPED;
1000 }
1001
1002 @Override
Angus Konged15d1a2013-08-19 15:06:12 -07001003 public boolean onBackPressed() {
1004 // If panorama is generating low res or high res mosaic, ignore back
1005 // key. So the activity will not be destroyed.
1006 if (mThreadRunning) return true;
1007 return false;
1008 }
1009
Angus Konged15d1a2013-08-19 15:06:12 -07001010 private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
1011 @Override
1012 protected Void doInBackground(Void... params) {
1013 synchronized (mMosaicFrameProcessor) {
1014 while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
1015 try {
1016 mMosaicFrameProcessor.wait();
1017 } catch (Exception e) {
1018 // ignore
1019 }
1020 }
1021 }
Angus Kong2dcc0a92013-09-25 13:00:08 -07001022 mActivity.updateStorageSpace();
Angus Konged15d1a2013-08-19 15:06:12 -07001023 return null;
1024 }
1025
1026 @Override
1027 protected void onPostExecute(Void result) {
1028 mWaitProcessorTask = null;
1029 mUI.dismissAllDialogs();
1030 // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE);
1031 initMosaicFrameProcessorIfNeeded();
1032 Point size = mUI.getPreviewAreaSize();
1033 mPreviewUIWidth = size.x;
1034 mPreviewUIHeight = size.y;
1035 configMosaicPreview();
1036 resetToPreviewIfPossible();
Angus Kong2dcc0a92013-09-25 13:00:08 -07001037 mActivity.updateStorageHint(mActivity.getStorageSpaceBytes());
Angus Konged15d1a2013-08-19 15:06:12 -07001038 }
1039 }
1040
1041 @Override
1042 public void cancelHighResStitching() {
1043 if (mPaused || mCameraTexture == null) return;
1044 cancelHighResComputation();
1045 }
1046
1047 @Override
Angus Konged15d1a2013-08-19 15:06:12 -07001048 public boolean onKeyDown(int keyCode, KeyEvent event) {
1049 return false;
1050 }
1051
1052 @Override
1053 public boolean onKeyUp(int keyCode, KeyEvent event) {
1054 return false;
1055 }
1056
1057 @Override
1058 public void onSingleTapUp(View view, int x, int y) {
1059 }
1060
1061 @Override
Angus Kongfd4fc0e2013-11-07 15:38:09 -08001062 public void onMediaSaverAvailable(MediaSaver s) {
Angus Konged15d1a2013-08-19 15:06:12 -07001063 // do nothing.
1064 }
1065}