1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.camera.panorama;
18 
19 import com.android.camera.ActivityBase;
20 import com.android.camera.CameraDisabledException;
21 import com.android.camera.CameraHardwareException;
22 import com.android.camera.CameraHolder;
23 import com.android.camera.Exif;
24 import com.android.camera.MenuHelper;
25 import com.android.camera.ModePicker;
26 import com.android.camera.OnClickAttr;
27 import com.android.camera.R;
28 import com.android.camera.RotateDialogController;
29 import com.android.camera.ShutterButton;
30 import com.android.camera.Storage;
31 import com.android.camera.Thumbnail;
32 import com.android.camera.Util;
33 import com.android.camera.ui.PopupManager;
34 import com.android.camera.ui.Rotatable;
35 import com.android.camera.ui.RotateImageView;
36 import com.android.camera.ui.RotateLayout;
37 import com.android.camera.ui.SharePopup;
38 
39 import android.content.ContentResolver;
40 import android.content.Context;
41 import android.content.res.AssetFileDescriptor;
42 import android.content.pm.ActivityInfo;
43 import android.content.res.Resources;
44 import android.graphics.Bitmap;
45 import android.graphics.BitmapFactory;
46 import android.graphics.ImageFormat;
47 import android.graphics.PixelFormat;
48 import android.graphics.Rect;
49 import android.graphics.SurfaceTexture;
50 import android.graphics.YuvImage;
51 import android.hardware.Camera.Parameters;
52 import android.hardware.Camera.Size;
53 import android.hardware.Sensor;
54 import android.hardware.SensorManager;
55 import android.media.ExifInterface;
56 import android.media.MediaActionSound;
57 import android.net.Uri;
58 import android.os.Bundle;
59 import android.os.Handler;
60 import android.os.Message;
61 import android.os.ParcelFileDescriptor;
62 import android.os.PowerManager;
63 import android.util.Log;
64 import android.view.Gravity;
65 import android.view.Menu;
66 import android.view.MenuItem;
67 import android.view.OrientationEventListener;
68 import android.view.View;
69 import android.view.ViewGroup;
70 import android.view.Window;
71 import android.view.WindowManager;
72 import android.widget.ImageView;
73 import android.widget.TextView;
74 
75 import java.io.ByteArrayOutputStream;
76 import java.io.File;
77 import java.io.IOException;
78 import java.util.List;
79 
80 /**
81  * Activity to handle panorama capturing.
82  */
83 public class PanoramaActivity extends ActivityBase implements
84         ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener,
85         ShutterButton.OnShutterButtonListener,
86         MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
87     public static final int DEFAULT_SWEEP_ANGLE = 160;
88     public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
89     public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
90 
91     private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
92     private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
93     private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
94     private static final int MSG_RESET_TO_PREVIEW = 4;
95     private static final int MSG_CLEAR_SCREEN_DELAY = 5;
96 
97     private static final int SCREEN_DELAY = 2 * 60 * 1000;
98 
99     private static final String TAG = "PanoramaActivity";
100     private static final int PREVIEW_STOPPED = 0;
101     private static final int PREVIEW_ACTIVE = 1;
102     private static final int CAPTURE_STATE_VIEWFINDER = 0;
103     private static final int CAPTURE_STATE_MOSAIC = 1;
104 
105     // Speed is in unit of deg/sec
106     private static final float PANNING_SPEED_THRESHOLD = 20f;
107 
108     // Ratio of nanosecond to second
109     private static final float NS2S = 1.0f / 1000000000.0f;
110 
111     private boolean mPausing;
112 
113     private View mPanoLayout;
114     private View mCaptureLayout;
115     private View mReviewLayout;
116     private ImageView mReview;
117     private RotateLayout mCaptureIndicator;
118     private PanoProgressBar mPanoProgressBar;
119     private PanoProgressBar mSavingProgressBar;
120     private View mFastIndicationBorder;
121     private View mLeftIndicator;
122     private View mRightIndicator;
123     private MosaicRendererSurfaceView mMosaicView;
124     private TextView mTooFastPrompt;
125     private ShutterButton mShutterButton;
126     private Object mWaitObject = new Object();
127 
128     private String mPreparePreviewString;
129     private String mDialogTitle;
130     private String mDialogOkString;
131     private String mDialogPanoramaFailedString;
132 
133     private int mIndicatorColor;
134     private int mIndicatorColorFast;
135 
136     private float mCompassValueX;
137     private float mCompassValueY;
138     private float mCompassValueXStart;
139     private float mCompassValueYStart;
140     private float mCompassValueXStartBuffer;
141     private float mCompassValueYStartBuffer;
142     private int mCompassThreshold;
143     private int mTraversedAngleX;
144     private int mTraversedAngleY;
145     private long mTimestamp;
146 
147     private RotateImageView mThumbnailView;
148     private Thumbnail mThumbnail;
149     private SharePopup mSharePopup;
150 
151     private int mPreviewWidth;
152     private int mPreviewHeight;
153     private int mCameraState;
154     private int mCaptureState;
155     private SensorManager mSensorManager;
156     private Sensor mSensor;
157     private PowerManager.WakeLock mPartialWakeLock;
158     private ModePicker mModePicker;
159     private MosaicFrameProcessor mMosaicFrameProcessor;
160     private long mTimeTaken;
161     private Handler mMainHandler;
162     private SurfaceTexture mSurfaceTexture;
163     private boolean mThreadRunning;
164     private boolean mCancelComputation;
165     private float[] mTransformMatrix;
166     private float mHorizontalViewAngle;
167     private float mVerticalViewAngle;
168 
169     // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
170     // getting a better image quality by the former.
171     private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
172 
173     private PanoOrientationEventListener mOrientationEventListener;
174     // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
175     // respectively.
176     private int mDeviceOrientation;
177     private int mDeviceOrientationAtCapture;
178     private int mCameraOrientation;
179     private int mOrientationCompensation;
180 
181     private RotateDialogController mRotateDialog;
182 
183     private MediaActionSound mCameraSound;
184 
185     private class MosaicJpeg {
MosaicJpeg(byte[] data, int width, int height)186         public MosaicJpeg(byte[] data, int width, int height) {
187             this.data = data;
188             this.width = width;
189             this.height = height;
190             this.isValid = true;
191         }
192 
MosaicJpeg()193         public MosaicJpeg() {
194             this.data = null;
195             this.width = 0;
196             this.height = 0;
197             this.isValid = false;
198         }
199 
200         public final byte[] data;
201         public final int width;
202         public final int height;
203         public final boolean isValid;
204     }
205 
206     private class PanoOrientationEventListener extends OrientationEventListener {
PanoOrientationEventListener(Context context)207         public PanoOrientationEventListener(Context context) {
208             super(context);
209         }
210 
211         @Override
onOrientationChanged(int orientation)212         public void onOrientationChanged(int orientation) {
213             // We keep the last known orientation. So if the user first orient
214             // the camera then point the camera to floor or sky, we still have
215             // the correct orientation.
216             if (orientation == ORIENTATION_UNKNOWN) return;
217             mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation);
218             // When the screen is unlocked, display rotation may change. Always
219             // calculate the up-to-date orientationCompensation.
220             int orientationCompensation = mDeviceOrientation
221                     + Util.getDisplayRotation(PanoramaActivity.this);
222             if (mOrientationCompensation != orientationCompensation) {
223                 mOrientationCompensation = orientationCompensation;
224                 setOrientationIndicator(mOrientationCompensation);
225             }
226         }
227     }
228 
setOrientationIndicator(int degree)229     private void setOrientationIndicator(int degree) {
230         if (mSharePopup != null) mSharePopup.setOrientation(degree);
231     }
232 
233     @Override
onCreateOptionsMenu(Menu menu)234     public boolean onCreateOptionsMenu(Menu menu) {
235         super.onCreateOptionsMenu(menu);
236 
237         addBaseMenuItems(menu);
238         return true;
239     }
240 
241     @Override
onPrepareOptionsMenu(Menu menu)242     public boolean onPrepareOptionsMenu(Menu menu) {
243         super.onPrepareOptionsMenu(menu);
244         // Only show the menu when idle.
245         boolean idle = (mCaptureState == CAPTURE_STATE_VIEWFINDER && !mThreadRunning);
246         for (int i = 0; i < menu.size(); i++) {
247             MenuItem item = menu.getItem(i);
248             item.setVisible(idle);
249             item.setEnabled(idle);
250         }
251 
252         return true;
253     }
254 
addBaseMenuItems(Menu menu)255     private void addBaseMenuItems(Menu menu) {
256         MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() {
257             public void run() {
258                 switchToOtherMode(ModePicker.MODE_CAMERA);
259             }
260         });
261         MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_VIDEO, new Runnable() {
262             public void run() {
263                 switchToOtherMode(ModePicker.MODE_VIDEO);
264             }
265         });
266     }
267 
268     @Override
onCreate(Bundle icicle)269     public void onCreate(Bundle icicle) {
270         super.onCreate(icicle);
271 
272         Window window = getWindow();
273         Util.enterLightsOutMode(window);
274         Util.initializeScreenBrightness(window, getContentResolver());
275 
276         createContentView();
277 
278         mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
279         mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
280         if (mSensor == null) {
281             mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
282         }
283         PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
284         mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
285 
286         mOrientationEventListener = new PanoOrientationEventListener(this);
287 
288         mTransformMatrix = new float[16];
289 
290         mPreparePreviewString =
291                 getResources().getString(R.string.pano_dialog_prepare_preview);
292         mDialogTitle = getResources().getString(R.string.pano_dialog_title);
293         mDialogOkString = getResources().getString(R.string.dialog_ok);
294         mDialogPanoramaFailedString =
295                 getResources().getString(R.string.pano_dialog_panorama_failed);
296         mCameraSound = new MediaActionSound();
297 
298         mMainHandler = new Handler() {
299             @Override
300             public void handleMessage(Message msg) {
301                 switch (msg.what) {
302                     case MSG_LOW_RES_FINAL_MOSAIC_READY:
303                         onBackgroundThreadFinished();
304                         showFinalMosaic((Bitmap) msg.obj);
305                         saveHighResMosaic();
306                         break;
307                     case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL:
308                         onBackgroundThreadFinished();
309                         // If the activity is paused, save the thumbnail to the file here.
310                         // If not, it will be saved in onPause.
311                         if (mPausing) saveThumbnailToFile();
312                         // Set the thumbnail bitmap here because mThumbnailView must be accessed
313                         // from the UI thread.
314                         updateThumbnailButton();
315 
316                         // Share popup may still have the reference to the old thumbnail. Clear it.
317                         mSharePopup = null;
318                         resetToPreview();
319                         break;
320                     case MSG_GENERATE_FINAL_MOSAIC_ERROR:
321                         onBackgroundThreadFinished();
322                         if (mPausing) {
323                             resetToPreview();
324                         } else {
325                             mRotateDialog.showAlertDialog(
326                                     mDialogTitle, mDialogPanoramaFailedString,
327                                     mDialogOkString, new Runnable() {
328                                         @Override
329                                         public void run() {
330                                             resetToPreview();
331                                         }},
332                                     null, null);
333                         }
334                         break;
335                     case MSG_RESET_TO_PREVIEW:
336                         onBackgroundThreadFinished();
337                         resetToPreview();
338                         break;
339                     case MSG_CLEAR_SCREEN_DELAY:
340                         getWindow().clearFlags(WindowManager.LayoutParams.
341                                 FLAG_KEEP_SCREEN_ON);
342                         break;
343                 }
344                 clearMosaicFrameProcessorIfNeeded();
345             }
346         };
347     }
348 
setupCamera()349     private void setupCamera() throws CameraHardwareException, CameraDisabledException {
350         openCamera();
351         Parameters parameters = mCameraDevice.getParameters();
352         setupCaptureParams(parameters);
353         configureCamera(parameters);
354     }
355 
releaseCamera()356     private void releaseCamera() {
357         if (mCameraDevice != null) {
358             mCameraDevice.setPreviewCallbackWithBuffer(null);
359             CameraHolder.instance().release();
360             mCameraDevice = null;
361             mCameraState = PREVIEW_STOPPED;
362         }
363     }
364 
openCamera()365     private void openCamera() throws CameraHardwareException, CameraDisabledException {
366         int backCameraId = CameraHolder.instance().getBackCameraId();
367         mCameraDevice = Util.openCamera(this, backCameraId);
368         mCameraOrientation = Util.getCameraOrientation(backCameraId);
369     }
370 
findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, boolean needSmaller)371     private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
372             boolean needSmaller) {
373         int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
374         boolean hasFound = false;
375         for (Size size : supportedSizes) {
376             int h = size.height;
377             int w = size.width;
378             // we only want 4:3 format.
379             int d = DEFAULT_CAPTURE_PIXELS - h * w;
380             if (needSmaller && d < 0) { // no bigger preview than 960x720.
381                 continue;
382             }
383             if (need4To3 && (h * 4 != w * 3)) {
384                 continue;
385             }
386             d = Math.abs(d);
387             if (d < pixelsDiff) {
388                 mPreviewWidth = w;
389                 mPreviewHeight = h;
390                 pixelsDiff = d;
391                 hasFound = true;
392             }
393         }
394         return hasFound;
395     }
396 
setupCaptureParams(Parameters parameters)397     private void setupCaptureParams(Parameters parameters) {
398         List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
399         if (!findBestPreviewSize(supportedSizes, true, true)) {
400             Log.w(TAG, "No 4:3 ratio preview size supported.");
401             if (!findBestPreviewSize(supportedSizes, false, true)) {
402                 Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
403                 findBestPreviewSize(supportedSizes, false, false);
404             }
405         }
406         Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
407         parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
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.v(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.setRecordingHint(false);
426 
427         mHorizontalViewAngle = parameters.getHorizontalViewAngle();
428         mVerticalViewAngle =  parameters.getVerticalViewAngle();
429     }
430 
getPreviewBufSize()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 (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
436     }
437 
configureCamera(Parameters parameters)438     private void configureCamera(Parameters parameters) {
439         mCameraDevice.setParameters(parameters);
440     }
441 
switchToOtherMode(int mode)442     private boolean switchToOtherMode(int mode) {
443         if (isFinishing()) {
444             return false;
445         }
446         MenuHelper.gotoMode(mode, this);
447         finish();
448         return true;
449     }
450 
onModeChanged(int mode)451     public boolean onModeChanged(int mode) {
452         if (mode != ModePicker.MODE_PANORAMA) {
453             return switchToOtherMode(mode);
454         } else {
455             return true;
456         }
457     }
458 
459     @Override
onMosaicSurfaceChanged()460     public void onMosaicSurfaceChanged() {
461         runOnUiThread(new Runnable() {
462             @Override
463             public void run() {
464                 // If panorama is generating low res or high res mosaic, it
465                 // means users exit and come back to panorama. Do not start the
466                 // preview. Preview will be started after final mosaic is
467                 // generated.
468                 if (!mPausing && !mThreadRunning) {
469                     startCameraPreview();
470                 }
471             }
472         });
473     }
474 
475     @Override
onMosaicSurfaceCreated(final int textureID)476     public void onMosaicSurfaceCreated(final int textureID) {
477         runOnUiThread(new Runnable() {
478             @Override
479             public void run() {
480                 if (mSurfaceTexture != null) {
481                     mSurfaceTexture.release();
482                 }
483                 mSurfaceTexture = new SurfaceTexture(textureID);
484                 if (!mPausing) {
485                     mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
486                 }
487             }
488         });
489     }
490 
runViewFinder()491     public void runViewFinder() {
492         mMosaicView.setWarping(false);
493         // Call preprocess to render it to low-res and high-res RGB textures.
494         mMosaicView.preprocess(mTransformMatrix);
495         mMosaicView.setReady();
496         mMosaicView.requestRender();
497     }
498 
runMosaicCapture()499     public void runMosaicCapture() {
500         mMosaicView.setWarping(true);
501         // Call preprocess to render it to low-res and high-res RGB textures.
502         mMosaicView.preprocess(mTransformMatrix);
503         // Lock the conditional variable to ensure the order of transferGPUtoCPU and
504         // mMosaicFrame.processFrame().
505         mMosaicView.lockPreviewReadyFlag();
506         // Now, transfer the textures from GPU to CPU memory for processing
507         mMosaicView.transferGPUtoCPU();
508         // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
509         mMosaicView.waitUntilPreviewReady();
510         mMosaicFrameProcessor.processFrame();
511     }
512 
onFrameAvailable(SurfaceTexture surface)513     public synchronized void onFrameAvailable(SurfaceTexture surface) {
514         /* This function may be called by some random thread,
515          * so let's be safe and use synchronize. No OpenGL calls can be done here.
516          */
517         // Frames might still be available after the activity is paused. If we call onFrameAvailable
518         // after pausing, the GL thread will crash.
519         if (mPausing) return;
520 
521         // Updating the texture should be done in the GL thread which mMosaicView is attached.
522         mMosaicView.queueEvent(new Runnable() {
523             @Override
524             public void run() {
525                 // Check if the activity is paused here can speed up the onPause() process.
526                 if (mPausing) return;
527                 mSurfaceTexture.updateTexImage();
528                 mSurfaceTexture.getTransformMatrix(mTransformMatrix);
529             }
530         });
531         // Update the transformation matrix for mosaic pre-process.
532         if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
533             runViewFinder();
534         } else {
535             runMosaicCapture();
536         }
537     }
538 
hideDirectionIndicators()539     private void hideDirectionIndicators() {
540         mLeftIndicator.setVisibility(View.GONE);
541         mRightIndicator.setVisibility(View.GONE);
542     }
543 
showDirectionIndicators(int direction)544     private void showDirectionIndicators(int direction) {
545         switch (direction) {
546             case PanoProgressBar.DIRECTION_NONE:
547                 mLeftIndicator.setVisibility(View.VISIBLE);
548                 mRightIndicator.setVisibility(View.VISIBLE);
549                 break;
550             case PanoProgressBar.DIRECTION_LEFT:
551                 mLeftIndicator.setVisibility(View.VISIBLE);
552                 mRightIndicator.setVisibility(View.GONE);
553                 break;
554             case PanoProgressBar.DIRECTION_RIGHT:
555                 mLeftIndicator.setVisibility(View.GONE);
556                 mRightIndicator.setVisibility(View.VISIBLE);
557                 break;
558         }
559     }
560 
startCapture()561     public void startCapture() {
562         // Reset values so we can do this again.
563         mCancelComputation = false;
564         mTimeTaken = System.currentTimeMillis();
565         mCaptureState = CAPTURE_STATE_MOSAIC;
566         mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording);
567         mCaptureIndicator.setVisibility(View.VISIBLE);
568         showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
569         mThumbnailView.setEnabled(false);
570 
571         mCompassValueXStart = mCompassValueXStartBuffer;
572         mCompassValueYStart = mCompassValueYStartBuffer;
573         mTimestamp = 0;
574 
575         mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
576             @Override
577             public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
578                     float progressX, float progressY) {
579                 float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
580                 float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
581                 if (isFinished
582                         || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
583                         || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
584                     stopCapture(false);
585                 } else {
586                     float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
587                     float panningRateYInDegree = panningRateY * mVerticalViewAngle;
588                     updateProgress(panningRateXInDegree, panningRateYInDegree,
589                             accumulatedHorizontalAngle, accumulatedVerticalAngle);
590                 }
591             }
592         });
593 
594         if (mModePicker != null) mModePicker.setEnabled(false);
595 
596         mPanoProgressBar.reset();
597         // TODO: calculate the indicator width according to different devices to reflect the actual
598         // angle of view of the camera device.
599         mPanoProgressBar.setIndicatorWidth(20);
600         mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
601         mPanoProgressBar.setVisibility(View.VISIBLE);
602         mDeviceOrientationAtCapture = mDeviceOrientation;
603         keepScreenOn();
604     }
605 
stopCapture(boolean aborted)606     private void stopCapture(boolean aborted) {
607         mCaptureState = CAPTURE_STATE_VIEWFINDER;
608         mCaptureIndicator.setVisibility(View.GONE);
609         hideTooFastIndication();
610         hideDirectionIndicators();
611         mThumbnailView.setEnabled(true);
612 
613         mMosaicFrameProcessor.setProgressListener(null);
614         stopCameraPreview();
615 
616         mSurfaceTexture.setOnFrameAvailableListener(null);
617 
618         if (!aborted && !mThreadRunning) {
619             mRotateDialog.showWaitingDialog(mPreparePreviewString);
620             runBackgroundThread(new Thread() {
621                 @Override
622                 public void run() {
623                     MosaicJpeg jpeg = generateFinalMosaic(false);
624 
625                     if (jpeg != null && jpeg.isValid) {
626                         Bitmap bitmap = null;
627                         bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
628                         mMainHandler.sendMessage(mMainHandler.obtainMessage(
629                                 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
630                     } else {
631                         mMainHandler.sendMessage(mMainHandler.obtainMessage(
632                                 MSG_RESET_TO_PREVIEW));
633                     }
634                 }
635             });
636         }
637         // do we have to wait for the thread to complete before enabling this?
638         if (mModePicker != null) mModePicker.setEnabled(true);
639         keepScreenOnAwhile();
640     }
641 
showTooFastIndication()642     private void showTooFastIndication() {
643         mTooFastPrompt.setVisibility(View.VISIBLE);
644         mFastIndicationBorder.setVisibility(View.VISIBLE);
645         mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
646         mLeftIndicator.setEnabled(true);
647         mRightIndicator.setEnabled(true);
648     }
649 
hideTooFastIndication()650     private void hideTooFastIndication() {
651         mTooFastPrompt.setVisibility(View.GONE);
652         mFastIndicationBorder.setVisibility(View.GONE);
653         mPanoProgressBar.setIndicatorColor(mIndicatorColor);
654         mLeftIndicator.setEnabled(false);
655         mRightIndicator.setEnabled(false);
656     }
657 
updateProgress(float panningRateXInDegree, float panningRateYInDegree, float progressHorizontalAngle, float progressVerticalAngle)658     private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
659             float progressHorizontalAngle, float progressVerticalAngle) {
660         mMosaicView.setReady();
661         mMosaicView.requestRender();
662 
663         // TODO: Now we just display warning message by the panning speed.
664         // Since we only support horizontal panning, we should display a warning message
665         // in UI when there're significant vertical movements.
666         if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
667             || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
668             showTooFastIndication();
669         } else {
670             hideTooFastIndication();
671         }
672         int angleInMajorDirection =
673                 (Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle))
674                 ? (int) progressHorizontalAngle
675                 : (int) progressVerticalAngle;
676         mPanoProgressBar.setProgress((angleInMajorDirection));
677     }
678 
createContentView()679     private void createContentView() {
680         setContentView(R.layout.panorama);
681 
682         mCaptureState = CAPTURE_STATE_VIEWFINDER;
683 
684         Resources appRes = getResources();
685 
686         mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
687         mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar);
688         mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
689         mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
690         mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
691         mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
692         mPanoProgressBar.setIndicatorColor(mIndicatorColor);
693         mPanoProgressBar.setOnDirectionChangeListener(
694                 new PanoProgressBar.OnDirectionChangeListener () {
695                     @Override
696                     public void onDirectionChange(int direction) {
697                         if (mCaptureState == CAPTURE_STATE_MOSAIC) {
698                             showDirectionIndicators(direction);
699                         }
700                     }
701                 });
702 
703         mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator);
704         mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator);
705         mLeftIndicator.setEnabled(false);
706         mRightIndicator.setEnabled(false);
707         mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
708         mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border);
709 
710         mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar);
711         mSavingProgressBar.setIndicatorWidth(0);
712         mSavingProgressBar.setMaxProgress(100);
713         mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
714         mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
715 
716         mCaptureIndicator = (RotateLayout) findViewById(R.id.pano_capture_indicator);
717 
718         mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
719         mThumbnailView.enableFilter(false);
720 
721         mReviewLayout = (View) findViewById(R.id.pano_review_layout);
722         mReview = (ImageView) findViewById(R.id.pano_reviewarea);
723         mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
724         mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
725 
726         mModePicker = (ModePicker) findViewById(R.id.mode_picker);
727         mModePicker.setVisibility(View.VISIBLE);
728         mModePicker.setOnModeChangeListener(this);
729         mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
730 
731         mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
732         mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
733         mShutterButton.setOnShutterButtonListener(this);
734 
735         mPanoLayout = findViewById(R.id.pano_layout);
736 
737         mRotateDialog = new RotateDialogController(this, R.layout.rotate_dialog);
738 
739         if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
740             Rotatable[] rotateLayout = {
741                     (Rotatable) findViewById(R.id.pano_pan_progress_bar_layout),
742                     (Rotatable) findViewById(R.id.pano_capture_too_fast_textview_layout),
743                     (Rotatable) findViewById(R.id.pano_review_saving_indication_layout),
744                     (Rotatable) findViewById(R.id.pano_saving_progress_bar_layout),
745                     (Rotatable) findViewById(R.id.pano_review_cancel_button_layout),
746                     (Rotatable) findViewById(R.id.pano_rotate_reviewarea),
747                     (Rotatable) mRotateDialog,
748                     (Rotatable) mCaptureIndicator,
749                     (Rotatable) mModePicker,
750                     (Rotatable) mThumbnailView};
751             for (Rotatable r : rotateLayout) {
752                 r.setOrientation(270);
753             }
754         }
755     }
756 
757     @Override
onShutterButtonClick()758     public void onShutterButtonClick() {
759         // If mSurfaceTexture == null then GL setup is not finished yet.
760         // No buttons can be pressed.
761         if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
762         // Since this button will stay on the screen when capturing, we need to check the state
763         // right now.
764         switch (mCaptureState) {
765             case CAPTURE_STATE_VIEWFINDER:
766                 mCameraSound.play(MediaActionSound.START_VIDEO_RECORDING);
767                 startCapture();
768                 break;
769             case CAPTURE_STATE_MOSAIC:
770                 mCameraSound.play(MediaActionSound.STOP_VIDEO_RECORDING);
771                 stopCapture(false);
772         }
773     }
774 
775     @Override
onShutterButtonFocus(boolean pressed)776     public void onShutterButtonFocus(boolean pressed) {
777     }
778 
reportProgress()779     public void reportProgress() {
780         mSavingProgressBar.reset();
781         mSavingProgressBar.setRightIncreasing(true);
782         Thread t = new Thread() {
783             @Override
784             public void run() {
785                 while (mThreadRunning) {
786                     final int progress = mMosaicFrameProcessor.reportProgress(
787                             true, mCancelComputation);
788 
789                     try {
790                         synchronized (mWaitObject) {
791                             mWaitObject.wait(50);
792                         }
793                     } catch (InterruptedException e) {
794                         throw new RuntimeException("Panorama reportProgress failed", e);
795                     }
796                     // Update the progress bar
797                     runOnUiThread(new Runnable() {
798                         public void run() {
799                             mSavingProgressBar.setProgress(progress);
800                         }
801                     });
802                 }
803             }
804         };
805         t.start();
806     }
807 
initThumbnailButton()808     private void initThumbnailButton() {
809         // Load the thumbnail from the disk.
810         if (mThumbnail == null) {
811             mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
812         }
813         updateThumbnailButton();
814     }
815 
updateThumbnailButton()816     private void updateThumbnailButton() {
817         // Update last image if URI is invalid and the storage is ready.
818         ContentResolver contentResolver = getContentResolver();
819         if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) {
820             mThumbnail = Thumbnail.getLastThumbnail(contentResolver);
821         }
822         if (mThumbnail != null) {
823             mThumbnailView.setBitmap(mThumbnail.getBitmap());
824         } else {
825             mThumbnailView.setBitmap(null);
826         }
827     }
828 
saveThumbnailToFile()829     private void saveThumbnailToFile() {
830         if (mThumbnail != null && !mThumbnail.fromFile()) {
831             mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
832         }
833     }
834 
saveHighResMosaic()835     public void saveHighResMosaic() {
836         runBackgroundThread(new Thread() {
837             @Override
838             public void run() {
839                 mPartialWakeLock.acquire();
840                 MosaicJpeg jpeg;
841                 try {
842                     jpeg = generateFinalMosaic(true);
843                 } finally {
844                     mPartialWakeLock.release();
845                 }
846 
847                 if (jpeg == null) {  // Cancelled by user.
848                     mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
849                 } else if (!jpeg.isValid) {  // Error when generating mosaic.
850                     mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
851                 } else {
852                     // The panorama image returned from the library is orientated based on the
853                     // natural orientation of a camera. We need to set an orientation for the image
854                     // in its EXIF header, so the image can be displayed correctly.
855                     // The orientation is calculated from compensating the
856                     // device orientation at capture and the camera orientation respective to
857                     // the natural orientation of the device.
858                     int orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
859                     Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
860                     if (uri != null) {
861                         // Create a thumbnail whose width or height is equal or bigger
862                         // than the screen's width or height.
863                         int widthRatio = (int) Math.ceil((double) jpeg.width
864                                 / mPanoLayout.getWidth());
865                         int heightRatio = (int) Math.ceil((double) jpeg.height
866                                 / mPanoLayout.getHeight());
867                         int inSampleSize = Integer.highestOneBit(
868                                 Math.max(widthRatio, heightRatio));
869                         mThumbnail = Thumbnail.createThumbnail(
870                                 jpeg.data, orientation, inSampleSize, uri);
871                         Util.broadcastNewPicture(PanoramaActivity.this, uri);
872                     }
873                     mMainHandler.sendMessage(
874                             mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
875                 }
876             }
877         });
878         reportProgress();
879     }
880 
runBackgroundThread(Thread thread)881     private void runBackgroundThread(Thread thread) {
882         mThreadRunning = true;
883         thread.start();
884     }
885 
onBackgroundThreadFinished()886     private void onBackgroundThreadFinished() {
887         mThreadRunning = false;
888         mRotateDialog.dismissDialog();
889     }
890 
cancelHighResComputation()891     private void cancelHighResComputation() {
892         mCancelComputation = true;
893         synchronized (mWaitObject) {
894             mWaitObject.notify();
895         }
896     }
897 
898     @OnClickAttr
onCancelButtonClicked(View v)899     public void onCancelButtonClicked(View v) {
900         if (mPausing || mSurfaceTexture == null) return;
901         cancelHighResComputation();
902     }
903 
904     @OnClickAttr
onThumbnailClicked(View v)905     public void onThumbnailClicked(View v) {
906         if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
907         showSharePopup();
908     }
909 
showSharePopup()910     private void showSharePopup() {
911         if (mThumbnail == null) return;
912         Uri uri = mThumbnail.getUri();
913         if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
914             // The orientation compensation is set to 0 here because we only support landscape.
915             mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
916                     mOrientationCompensation,
917                     findViewById(R.id.frame_layout));
918         }
919         mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
920     }
921 
reset()922     private void reset() {
923         mCaptureState = CAPTURE_STATE_VIEWFINDER;
924 
925         mReviewLayout.setVisibility(View.GONE);
926         mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
927         mPanoProgressBar.setVisibility(View.GONE);
928         mCaptureLayout.setVisibility(View.VISIBLE);
929         mMosaicFrameProcessor.reset();
930 
931         mSurfaceTexture.setOnFrameAvailableListener(this);
932     }
933 
resetToPreview()934     private void resetToPreview() {
935         reset();
936         if (!mPausing) startCameraPreview();
937     }
938 
showFinalMosaic(Bitmap bitmap)939     private void showFinalMosaic(Bitmap bitmap) {
940         if (bitmap != null) {
941             mReview.setImageBitmap(bitmap);
942         }
943         mCaptureLayout.setVisibility(View.GONE);
944         mReviewLayout.setVisibility(View.VISIBLE);
945     }
946 
savePanorama(byte[] jpegData, int width, int height, int orientation)947     private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
948         if (jpegData != null) {
949             String filename = PanoUtil.createName(
950                     getResources().getString(R.string.pano_file_name_format), mTimeTaken);
951             Uri uri = Storage.addImage(getContentResolver(), filename, mTimeTaken, null,
952                     orientation, jpegData, width, height);
953             if (uri != null && orientation != 0) {
954                 String filepath = Storage.generateFilepath(filename);
955                 try {
956                     // Save the orientation in EXIF.
957                     ExifInterface exif = new ExifInterface(filepath);
958                     exif.setAttribute(ExifInterface.TAG_ORIENTATION,
959                             getExifOrientation(orientation));
960                     exif.saveAttributes();
961                 } catch (IOException e) {
962                     Log.e(TAG, "cannot set exif data: " + filepath);
963                 }
964             }
965             return uri;
966         }
967         return null;
968     }
969 
getExifOrientation(int orientation)970     private static String getExifOrientation(int orientation) {
971         switch (orientation) {
972             case 0:
973                 return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
974             case 90:
975                 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
976             case 180:
977                 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
978             case 270:
979                 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
980             default:
981                 throw new AssertionError("invalid: " + orientation);
982         }
983     }
984 
clearMosaicFrameProcessorIfNeeded()985     private void clearMosaicFrameProcessorIfNeeded() {
986         if (!mPausing || mThreadRunning) return;
987         mMosaicFrameProcessor.clear();
988     }
989 
initMosaicFrameProcessorIfNeeded()990     private void initMosaicFrameProcessorIfNeeded() {
991         if (mPausing || mThreadRunning) return;
992         if (mMosaicFrameProcessor == null) {
993             // Start the activity for the first time.
994             mMosaicFrameProcessor = new MosaicFrameProcessor(
995                     mPreviewWidth, mPreviewHeight, getPreviewBufSize());
996         }
997         mMosaicFrameProcessor.initialize();
998     }
999 
1000     @Override
onPause()1001     protected void onPause() {
1002         super.onPause();
1003 
1004         mPausing = true;
1005         // Stop the capturing first.
1006         if (mCaptureState == CAPTURE_STATE_MOSAIC) {
1007             stopCapture(true);
1008             reset();
1009         }
1010         if (mSharePopup != null) mSharePopup.dismiss();
1011 
1012         saveThumbnailToFile();
1013 
1014         releaseCamera();
1015         mMosaicView.onPause();
1016         clearMosaicFrameProcessorIfNeeded();
1017         mOrientationEventListener.disable();
1018         resetScreenOn();
1019         mCameraSound.release();
1020         System.gc();
1021     }
1022 
1023     @Override
doOnResume()1024     protected void doOnResume() {
1025         mPausing = false;
1026         mOrientationEventListener.enable();
1027 
1028         mCaptureState = CAPTURE_STATE_VIEWFINDER;
1029         try {
1030             setupCamera();
1031 
1032             // Camera must be initialized before MosaicFrameProcessor is initialized.
1033             // The preview size has to be decided by camera device.
1034             initMosaicFrameProcessorIfNeeded();
1035             mMosaicView.onResume();
1036 
1037             initThumbnailButton();
1038             keepScreenOnAwhile();
1039         } catch (CameraHardwareException e) {
1040             Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
1041             return;
1042         } catch (CameraDisabledException e) {
1043             Util.showErrorAndFinish(this, R.string.camera_disabled);
1044             return;
1045         }
1046         // Dismiss open menu if exists.
1047         PopupManager.getInstance(this).notifyShowPopup(null);
1048     }
1049 
1050     /**
1051      * Generate the final mosaic image.
1052      *
1053      * @param highRes flag to indicate whether we want to get a high-res version.
1054      * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
1055      *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
1056      *         is an error in generating the final mosaic.
1057      */
generateFinalMosaic(boolean highRes)1058     public MosaicJpeg generateFinalMosaic(boolean highRes) {
1059         int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
1060         if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
1061             return null;
1062         } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
1063             return new MosaicJpeg();
1064         }
1065 
1066         byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
1067         if (imageData == null) {
1068             Log.e(TAG, "getFinalMosaicNV21() returned null.");
1069             return new MosaicJpeg();
1070         }
1071 
1072         int len = imageData.length - 8;
1073         int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
1074                 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
1075         int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
1076                 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
1077         Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
1078 
1079         if (width <= 0 || height <= 0) {
1080             // TODO: pop up a error meesage indicating that the final result is not generated.
1081             Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
1082                     height);
1083             return new MosaicJpeg();
1084         }
1085 
1086         YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
1087         ByteArrayOutputStream out = new ByteArrayOutputStream();
1088         yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
1089         try {
1090             out.close();
1091         } catch (Exception e) {
1092             Log.e(TAG, "Exception in storing final mosaic", e);
1093             return new MosaicJpeg();
1094         }
1095         return new MosaicJpeg(out.toByteArray(), width, height);
1096     }
1097 
setPreviewTexture(SurfaceTexture surface)1098     private void setPreviewTexture(SurfaceTexture surface) {
1099         try {
1100             mCameraDevice.setPreviewTexture(surface);
1101         } catch (Throwable ex) {
1102             releaseCamera();
1103             throw new RuntimeException("setPreviewTexture failed", ex);
1104         }
1105     }
1106 
startCameraPreview()1107     private void startCameraPreview() {
1108         // If we're previewing already, stop the preview first (this will blank
1109         // the screen).
1110         if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1111 
1112         // Set the display orientation to 0, so that the underlying mosaic library
1113         // can always get undistorted mPreviewWidth x mPreviewHeight image data
1114         // from SurfaceTexture.
1115         mCameraDevice.setDisplayOrientation(0);
1116 
1117         setPreviewTexture(mSurfaceTexture);
1118 
1119         try {
1120             Log.v(TAG, "startPreview");
1121             mCameraDevice.startPreview();
1122         } catch (Throwable ex) {
1123             releaseCamera();
1124             throw new RuntimeException("startPreview failed", ex);
1125         }
1126         mCameraState = PREVIEW_ACTIVE;
1127     }
1128 
stopCameraPreview()1129     private void stopCameraPreview() {
1130         if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1131             Log.v(TAG, "stopPreview");
1132             mCameraDevice.stopPreview();
1133         }
1134         mCameraState = PREVIEW_STOPPED;
1135     }
1136 
1137     @Override
onUserInteraction()1138     public void onUserInteraction() {
1139         super.onUserInteraction();
1140         if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1141     }
1142 
1143     @Override
onBackPressed()1144     public void onBackPressed() {
1145         // If panorama is generating low res or high res mosaic, ignore back
1146         // key. So the activity will not be destroyed.
1147         if (mThreadRunning) return;
1148         super.onBackPressed();
1149     }
1150 
resetScreenOn()1151     private void resetScreenOn() {
1152         mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1153         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1154     }
1155 
keepScreenOnAwhile()1156     private void keepScreenOnAwhile() {
1157         mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1158         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1159         mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1160     }
1161 
keepScreenOn()1162     private void keepScreenOn() {
1163         mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1164         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1165     }
1166 }
1167