• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ShutterButton;
29 import com.android.camera.SoundPlayer;
30 import com.android.camera.Storage;
31 import com.android.camera.Thumbnail;
32 import com.android.camera.Util;
33 import com.android.camera.ui.RotateImageView;
34 import com.android.camera.ui.SharePopup;
35 
36 import android.app.AlertDialog;
37 import android.app.ProgressDialog;
38 import android.content.ContentResolver;
39 import android.content.Context;
40 import android.content.DialogInterface;
41 import android.content.res.AssetFileDescriptor;
42 import android.content.res.Resources;
43 import android.graphics.Bitmap;
44 import android.graphics.BitmapFactory;
45 import android.graphics.ImageFormat;
46 import android.graphics.PixelFormat;
47 import android.graphics.Rect;
48 import android.graphics.SurfaceTexture;
49 import android.graphics.YuvImage;
50 import android.hardware.Camera;
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.net.Uri;
56 import android.os.Bundle;
57 import android.os.Handler;
58 import android.os.Message;
59 import android.os.ParcelFileDescriptor;
60 import android.os.SystemProperties;
61 import android.util.Log;
62 import android.view.Gravity;
63 import android.view.Menu;
64 import android.view.OrientationEventListener;
65 import android.view.View;
66 import android.view.Window;
67 import android.view.WindowManager;
68 import android.widget.ImageView;
69 import android.widget.TextView;
70 
71 import java.io.ByteArrayOutputStream;
72 import java.io.FileNotFoundException;
73 import java.io.File;
74 import java.util.List;
75 
76 /**
77  * Activity to handle panorama capturing.
78  */
79 public class PanoramaActivity extends ActivityBase implements
80         ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener,
81         ShutterButton.OnShutterButtonListener,
82         MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
83     public static final int DEFAULT_SWEEP_ANGLE = 160;
84     public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
85     public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
86 
87     private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
88     private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
89     private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
90     private static final int MSG_RESET_TO_PREVIEW = 4;
91 
92     private static final String TAG = "PanoramaActivity";
93     private static final int PREVIEW_STOPPED = 0;
94     private static final int PREVIEW_ACTIVE = 1;
95     private static final int CAPTURE_STATE_VIEWFINDER = 0;
96     private static final int CAPTURE_STATE_MOSAIC = 1;
97 
98     // Speed is in unit of deg/sec
99     private static final float PANNING_SPEED_THRESHOLD = 20f;
100 
101     // Ratio of nanosecond to second
102     private static final float NS2S = 1.0f / 1000000000.0f;
103 
104     private static final String VIDEO_RECORD_SOUND = "/system/media/audio/ui/VideoRecord.ogg";
105 
106     private boolean mPausing;
107 
108     private View mPanoLayout;
109     private View mCaptureLayout;
110     private View mReviewLayout;
111     private ImageView mReview;
112     private TextView mCaptureIndicator;
113     private PanoProgressBar mPanoProgressBar;
114     private PanoProgressBar mSavingProgressBar;
115     private View mFastIndicationBorder;
116     private View mLeftIndicator;
117     private View mRightIndicator;
118     private MosaicRendererSurfaceView mMosaicView;
119     private TextView mTooFastPrompt;
120     private ShutterButton mShutterButton;
121     private Object mWaitObject = new Object();
122 
123     private String mPreparePreviewString;
124     private AlertDialog mAlertDialog;
125     private ProgressDialog mProgressDialog;
126     private String mDialogTitle;
127     private String mDialogOk;
128 
129     private int mIndicatorColor;
130     private int mIndicatorColorFast;
131 
132     private float mCompassValueX;
133     private float mCompassValueY;
134     private float mCompassValueXStart;
135     private float mCompassValueYStart;
136     private float mCompassValueXStartBuffer;
137     private float mCompassValueYStartBuffer;
138     private int mCompassThreshold;
139     private int mTraversedAngleX;
140     private int mTraversedAngleY;
141     private long mTimestamp;
142     // Control variables for the terminate condition.
143     private int mMinAngleX;
144     private int mMaxAngleX;
145     private int mMinAngleY;
146     private int mMaxAngleY;
147 
148     private RotateImageView mThumbnailView;
149     private Thumbnail mThumbnail;
150     private SharePopup mSharePopup;
151 
152     private int mPreviewWidth;
153     private int mPreviewHeight;
154     private int mCameraState;
155     private int mCaptureState;
156     private SensorManager mSensorManager;
157     private Sensor mSensor;
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 
168     private SoundPlayer mRecordSound;
169 
170     // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
171     // getting a better image quality by the former.
172     private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
173 
174     private PanoOrientationEventListener mOrientationEventListener;
175     // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
176     // respectively.
177     private int mDeviceOrientation;
178     private int mOrientationCompensation;
179 
180     private class MosaicJpeg {
MosaicJpeg(byte[] data, int width, int height)181         public MosaicJpeg(byte[] data, int width, int height) {
182             this.data = data;
183             this.width = width;
184             this.height = height;
185             this.isValid = true;
186         }
187 
MosaicJpeg()188         public MosaicJpeg() {
189             this.data = null;
190             this.width = 0;
191             this.height = 0;
192             this.isValid = false;
193         }
194 
195         public final byte[] data;
196         public final int width;
197         public final int height;
198         public final boolean isValid;
199     }
200 
201     private class PanoOrientationEventListener extends OrientationEventListener {
PanoOrientationEventListener(Context context)202         public PanoOrientationEventListener(Context context) {
203             super(context);
204         }
205 
206         @Override
onOrientationChanged(int orientation)207         public void onOrientationChanged(int orientation) {
208             // We keep the last known orientation. So if the user first orient
209             // the camera then point the camera to floor or sky, we still have
210             // the correct orientation.
211             if (orientation == ORIENTATION_UNKNOWN) return;
212             mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation);
213             // When the screen is unlocked, display rotation may change. Always
214             // calculate the up-to-date orientationCompensation.
215             int orientationCompensation = mDeviceOrientation
216                     + Util.getDisplayRotation(PanoramaActivity.this);
217             if (mOrientationCompensation != orientationCompensation) {
218                 mOrientationCompensation = orientationCompensation;
219                 setOrientationIndicator(mOrientationCompensation);
220             }
221         }
222     }
223 
setOrientationIndicator(int degree)224     private void setOrientationIndicator(int degree) {
225         if (mSharePopup != null) mSharePopup.setOrientation(degree);
226     }
227 
228     @Override
onCreateOptionsMenu(Menu menu)229     public boolean onCreateOptionsMenu(Menu menu) {
230         super.onCreateOptionsMenu(menu);
231 
232         addBaseMenuItems(menu);
233         return true;
234     }
235 
addBaseMenuItems(Menu menu)236     private void addBaseMenuItems(Menu menu) {
237         MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() {
238             public void run() {
239                 switchToOtherMode(ModePicker.MODE_CAMERA);
240             }
241         });
242         MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_VIDEO, new Runnable() {
243             public void run() {
244                 switchToOtherMode(ModePicker.MODE_VIDEO);
245             }
246         });
247     }
248 
249     @Override
onCreate(Bundle icicle)250     public void onCreate(Bundle icicle) {
251         super.onCreate(icicle);
252 
253         Window window = getWindow();
254         window.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
255                 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
256         Util.enterLightsOutMode(window);
257         Util.initializeScreenBrightness(window, getContentResolver());
258 
259         createContentView();
260 
261         mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
262         mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
263         if (mSensor == null) {
264             mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
265         }
266 
267         mOrientationEventListener = new PanoOrientationEventListener(this);
268 
269         mTransformMatrix = new float[16];
270 
271         mPreparePreviewString =
272                 getResources().getString(R.string.pano_dialog_prepare_preview);
273         mDialogTitle = getResources().getString(R.string.pano_dialog_title);
274         mDialogOk = getResources().getString(R.string.dialog_ok);
275 
276         mMainHandler = new Handler() {
277             @Override
278             public void handleMessage(Message msg) {
279                 switch (msg.what) {
280                     case MSG_LOW_RES_FINAL_MOSAIC_READY:
281                         onBackgroundThreadFinished();
282                         showFinalMosaic((Bitmap) msg.obj);
283                         saveHighResMosaic();
284                         break;
285                     case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL:
286                         onBackgroundThreadFinished();
287                         // Set the thumbnail bitmap here because mThumbnailView must be accessed
288                         // from the UI thread.
289                         updateThumbnailButton();
290 
291                         // Share popup may still have the reference to the old thumbnail. Clear it.
292                         mSharePopup = null;
293                         resetToPreview();
294                         break;
295                     case MSG_GENERATE_FINAL_MOSAIC_ERROR:
296                         onBackgroundThreadFinished();
297                         if (mPausing) {
298                             resetToPreview();
299                         } else {
300                             mAlertDialog.show();
301                         }
302                         break;
303                     case MSG_RESET_TO_PREVIEW:
304                         onBackgroundThreadFinished();
305                         resetToPreview();
306                 }
307                 clearMosaicFrameProcessorIfNeeded();
308             }
309         };
310 
311         mAlertDialog = (new AlertDialog.Builder(this))
312                 .setTitle(mDialogTitle)
313                 .setMessage(R.string.pano_dialog_panorama_failed)
314                 .create();
315         mAlertDialog.setCancelable(false);
316         mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, mDialogOk,
317                 new DialogInterface.OnClickListener() {
318                     @Override
319                     public void onClick(DialogInterface dialog, int which) {
320                         dialog.dismiss();
321                         resetToPreview();
322                     }
323                 });
324     }
325 
326     @Override
onStart()327     public void onStart() {
328         super.onStart();
329         updateThumbnailButton();
330     }
331 
setupCamera()332     private void setupCamera() {
333         openCamera();
334         Parameters parameters = mCameraDevice.getParameters();
335         setupCaptureParams(parameters);
336         configureCamera(parameters);
337     }
338 
releaseCamera()339     private void releaseCamera() {
340         if (mCameraDevice != null) {
341             mCameraDevice.setPreviewCallbackWithBuffer(null);
342             CameraHolder.instance().release();
343             mCameraDevice = null;
344             mCameraState = PREVIEW_STOPPED;
345         }
346     }
347 
openCamera()348     private void openCamera() {
349         try {
350             mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
351         } catch (CameraHardwareException e) {
352             Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
353             return;
354         } catch (CameraDisabledException e) {
355             Util.showErrorAndFinish(this, R.string.camera_disabled);
356             return;
357         }
358     }
359 
findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, boolean needSmaller)360     private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
361             boolean needSmaller) {
362         int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
363         boolean hasFound = false;
364         for (Size size : supportedSizes) {
365             int h = size.height;
366             int w = size.width;
367             // we only want 4:3 format.
368             int d = DEFAULT_CAPTURE_PIXELS - h * w;
369             if (needSmaller && d < 0) { // no bigger preview than 960x720.
370                 continue;
371             }
372             if (need4To3 && (h * 4 != w * 3)) {
373                 continue;
374             }
375             d = Math.abs(d);
376             if (d < pixelsDiff) {
377                 mPreviewWidth = w;
378                 mPreviewHeight = h;
379                 pixelsDiff = d;
380                 hasFound = true;
381             }
382         }
383         return hasFound;
384     }
385 
setupCaptureParams(Parameters parameters)386     private void setupCaptureParams(Parameters parameters) {
387         List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
388         if (!findBestPreviewSize(supportedSizes, true, true)) {
389             Log.w(TAG, "No 4:3 ratio preview size supported.");
390             if (!findBestPreviewSize(supportedSizes, false, true)) {
391                 Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
392                 findBestPreviewSize(supportedSizes, false, false);
393             }
394         }
395         Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
396         parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
397 
398         List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
399         int last = frameRates.size() - 1;
400         int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
401         int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
402         parameters.setPreviewFpsRange(minFps, maxFps);
403         Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
404 
405         List<String> supportedFocusModes = parameters.getSupportedFocusModes();
406         if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
407             parameters.setFocusMode(mTargetFocusMode);
408         } else {
409             // Use the default focus mode and log a message
410             Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
411                   " becuase the mode is not supported.");
412         }
413 
414         parameters.setRecordingHint(false);
415 
416         mHorizontalViewAngle = (((mDeviceOrientation / 90) % 2) == 0) ?
417                 parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle();
418     }
419 
getPreviewBufSize()420     public int getPreviewBufSize() {
421         PixelFormat pixelInfo = new PixelFormat();
422         PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
423         // TODO: remove this extra 32 byte after the driver bug is fixed.
424         return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
425     }
426 
configureCamera(Parameters parameters)427     private void configureCamera(Parameters parameters) {
428         mCameraDevice.setParameters(parameters);
429     }
430 
switchToOtherMode(int mode)431     private boolean switchToOtherMode(int mode) {
432         if (isFinishing()) {
433             return false;
434         }
435         MenuHelper.gotoMode(mode, this);
436         finish();
437         return true;
438     }
439 
onModeChanged(int mode)440     public boolean onModeChanged(int mode) {
441         if (mode != ModePicker.MODE_PANORAMA) {
442             return switchToOtherMode(mode);
443         } else {
444             return true;
445         }
446     }
447 
448     @Override
onMosaicSurfaceChanged()449     public void onMosaicSurfaceChanged() {
450         runOnUiThread(new Runnable() {
451             @Override
452             public void run() {
453                 if (!mPausing) {
454                     startCameraPreview();
455                 }
456             }
457         });
458     }
459 
460     @Override
onMosaicSurfaceCreated(final int textureID)461     public void onMosaicSurfaceCreated(final int textureID) {
462         runOnUiThread(new Runnable() {
463             @Override
464             public void run() {
465                 if (mSurfaceTexture != null) {
466                     mSurfaceTexture.release();
467                 }
468                 mSurfaceTexture = new SurfaceTexture(textureID);
469                 if (!mPausing) {
470                     mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
471                 }
472             }
473         });
474     }
475 
runViewFinder()476     public void runViewFinder() {
477         mMosaicView.setWarping(false);
478         // Call preprocess to render it to low-res and high-res RGB textures.
479         mMosaicView.preprocess(mTransformMatrix);
480         mMosaicView.setReady();
481         mMosaicView.requestRender();
482     }
483 
runMosaicCapture()484     public void runMosaicCapture() {
485         mMosaicView.setWarping(true);
486         // Call preprocess to render it to low-res and high-res RGB textures.
487         mMosaicView.preprocess(mTransformMatrix);
488         // Lock the conditional variable to ensure the order of transferGPUtoCPU and
489         // mMosaicFrame.processFrame().
490         mMosaicView.lockPreviewReadyFlag();
491         // Now, transfer the textures from GPU to CPU memory for processing
492         mMosaicView.transferGPUtoCPU();
493         // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
494         mMosaicView.waitUntilPreviewReady();
495         mMosaicFrameProcessor.processFrame();
496     }
497 
onFrameAvailable(SurfaceTexture surface)498     public synchronized void onFrameAvailable(SurfaceTexture surface) {
499         /* This function may be called by some random thread,
500          * so let's be safe and use synchronize. No OpenGL calls can be done here.
501          */
502         // Updating the texture should be done in the GL thread which mMosaicView is attached.
503         mMosaicView.queueEvent(new Runnable() {
504             @Override
505             public void run() {
506                 mSurfaceTexture.updateTexImage();
507                 mSurfaceTexture.getTransformMatrix(mTransformMatrix);
508             }
509         });
510         // Update the transformation matrix for mosaic pre-process.
511         if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
512             runViewFinder();
513         } else {
514             runMosaicCapture();
515         }
516     }
517 
hideDirectionIndicators()518     private void hideDirectionIndicators() {
519         mLeftIndicator.setVisibility(View.GONE);
520         mRightIndicator.setVisibility(View.GONE);
521     }
522 
showDirectionIndicators(int direction)523     private void showDirectionIndicators(int direction) {
524         switch (direction) {
525             case PanoProgressBar.DIRECTION_NONE:
526                 mLeftIndicator.setVisibility(View.VISIBLE);
527                 mRightIndicator.setVisibility(View.VISIBLE);
528                 break;
529             case PanoProgressBar.DIRECTION_LEFT:
530                 mLeftIndicator.setVisibility(View.VISIBLE);
531                 mRightIndicator.setVisibility(View.GONE);
532                 break;
533             case PanoProgressBar.DIRECTION_RIGHT:
534                 mLeftIndicator.setVisibility(View.GONE);
535                 mRightIndicator.setVisibility(View.VISIBLE);
536                 break;
537         }
538     }
539 
startCapture()540     public void startCapture() {
541         // Reset values so we can do this again.
542         mCancelComputation = false;
543         mTimeTaken = System.currentTimeMillis();
544         mCaptureState = CAPTURE_STATE_MOSAIC;
545         mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording);
546         mCaptureIndicator.setVisibility(View.VISIBLE);
547         showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
548         mThumbnailView.setEnabled(false);
549 
550         mCompassValueXStart = mCompassValueXStartBuffer;
551         mCompassValueYStart = mCompassValueYStartBuffer;
552         mMinAngleX = 0;
553         mMaxAngleX = 0;
554         mMinAngleY = 0;
555         mMaxAngleY = 0;
556         mTimestamp = 0;
557 
558         mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
559             @Override
560             public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
561                     float progressX, float progressY) {
562                 if (isFinished
563                         || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE)
564                         || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) {
565                     stopCapture(false);
566                 } else {
567                     updateProgress(panningRateX, progressX, progressY);
568                 }
569             }
570         });
571 
572         if (mModePicker != null) mModePicker.setEnabled(false);
573 
574         mPanoProgressBar.reset();
575         // TODO: calculate the indicator width according to different devices to reflect the actual
576         // angle of view of the camera device.
577         mPanoProgressBar.setIndicatorWidth(20);
578         mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
579         mPanoProgressBar.setVisibility(View.VISIBLE);
580     }
581 
stopCapture(boolean aborted)582     private void stopCapture(boolean aborted) {
583         mCaptureState = CAPTURE_STATE_VIEWFINDER;
584         mCaptureIndicator.setVisibility(View.GONE);
585         hideTooFastIndication();
586         hideDirectionIndicators();
587         mThumbnailView.setEnabled(true);
588 
589         mMosaicFrameProcessor.setProgressListener(null);
590         stopCameraPreview();
591 
592         mSurfaceTexture.setOnFrameAvailableListener(null);
593 
594         if (!aborted && !mThreadRunning) {
595             showDialog(mPreparePreviewString);
596             runBackgroundThread(new Thread() {
597                 @Override
598                 public void run() {
599                     MosaicJpeg jpeg = generateFinalMosaic(false);
600 
601                     if (jpeg != null && jpeg.isValid) {
602                         Bitmap bitmap = null;
603                         bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
604                         mMainHandler.sendMessage(mMainHandler.obtainMessage(
605                                 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
606                     } else {
607                         mMainHandler.sendMessage(mMainHandler.obtainMessage(
608                                 MSG_RESET_TO_PREVIEW));
609                     }
610                 }
611             });
612         }
613         // do we have to wait for the thread to complete before enabling this?
614         if (mModePicker != null) mModePicker.setEnabled(true);
615     }
616 
showTooFastIndication()617     private void showTooFastIndication() {
618         mTooFastPrompt.setVisibility(View.VISIBLE);
619         mFastIndicationBorder.setVisibility(View.VISIBLE);
620         mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
621         mLeftIndicator.setEnabled(true);
622         mRightIndicator.setEnabled(true);
623     }
624 
hideTooFastIndication()625     private void hideTooFastIndication() {
626         mTooFastPrompt.setVisibility(View.GONE);
627         mFastIndicationBorder.setVisibility(View.GONE);
628         mPanoProgressBar.setIndicatorColor(mIndicatorColor);
629         mLeftIndicator.setEnabled(false);
630         mRightIndicator.setEnabled(false);
631     }
632 
updateProgress(float panningRate, float progressX, float progressY)633     private void updateProgress(float panningRate, float progressX, float progressY) {
634         mMosaicView.setReady();
635         mMosaicView.requestRender();
636 
637         // TODO: Now we just display warning message by the panning speed.
638         // Since we only support horizontal panning, we should display a warning message
639         // in UI when there're significant vertical movements.
640         if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) {
641             showTooFastIndication();
642         } else {
643             hideTooFastIndication();
644         }
645         mPanoProgressBar.setProgress((int) (progressX * mHorizontalViewAngle));
646     }
647 
createContentView()648     private void createContentView() {
649         setContentView(R.layout.panorama);
650 
651         mCaptureState = CAPTURE_STATE_VIEWFINDER;
652 
653         Resources appRes = getResources();
654 
655         mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
656         mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar);
657         mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
658         mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
659         mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
660         mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
661         mPanoProgressBar.setIndicatorColor(mIndicatorColor);
662         mPanoProgressBar.setOnDirectionChangeListener(
663                 new PanoProgressBar.OnDirectionChangeListener () {
664                     @Override
665                     public void onDirectionChange(int direction) {
666                         if (mCaptureState == CAPTURE_STATE_MOSAIC) {
667                             showDirectionIndicators(direction);
668                         }
669                     }
670                 });
671 
672         mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator);
673         mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator);
674         mLeftIndicator.setEnabled(false);
675         mRightIndicator.setEnabled(false);
676         mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
677         mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border);
678 
679         mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar);
680         mSavingProgressBar.setIndicatorWidth(0);
681         mSavingProgressBar.setMaxProgress(100);
682         mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
683         mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
684 
685         mCaptureIndicator = (TextView) findViewById(R.id.pano_capture_indicator);
686 
687         mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
688         mThumbnailView.enableFilter(false);
689 
690         mReviewLayout = (View) findViewById(R.id.pano_review_layout);
691         mReview = (ImageView) findViewById(R.id.pano_reviewarea);
692         mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
693         mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
694 
695         mModePicker = (ModePicker) findViewById(R.id.mode_picker);
696         mModePicker.setVisibility(View.VISIBLE);
697         mModePicker.setOnModeChangeListener(this);
698         mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
699 
700         mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
701         mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
702         mShutterButton.setOnShutterButtonListener(this);
703 
704         mPanoLayout = findViewById(R.id.pano_layout);
705     }
706 
707     @Override
onShutterButtonClick()708     public void onShutterButtonClick() {
709         // If mSurfaceTexture == null then GL setup is not finished yet.
710         // No buttons can be pressed.
711         if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
712         // Since this button will stay on the screen when capturing, we need to check the state
713         // right now.
714         switch (mCaptureState) {
715             case CAPTURE_STATE_VIEWFINDER:
716                 if (mRecordSound != null) mRecordSound.play();
717                 startCapture();
718                 break;
719             case CAPTURE_STATE_MOSAIC:
720                 if (mRecordSound != null) mRecordSound.play();
721                 stopCapture(false);
722         }
723     }
724 
725     @Override
onShutterButtonFocus(boolean pressed)726     public void onShutterButtonFocus(boolean pressed) {
727     }
728 
reportProgress()729     public void reportProgress() {
730         mSavingProgressBar.reset();
731         mSavingProgressBar.setRightIncreasing(true);
732         Thread t = new Thread() {
733             @Override
734             public void run() {
735                 while (mThreadRunning) {
736                     final int progress = mMosaicFrameProcessor.reportProgress(
737                             true, mCancelComputation);
738 
739                     try {
740                         synchronized (mWaitObject) {
741                             mWaitObject.wait(50);
742                         }
743                     } catch (InterruptedException e) {
744                         throw new RuntimeException("Panorama reportProgress failed", e);
745                     }
746                     // Update the progress bar
747                     runOnUiThread(new Runnable() {
748                         public void run() {
749                             mSavingProgressBar.setProgress(progress);
750                         }
751                     });
752                 }
753             }
754         };
755         t.start();
756     }
757 
updateThumbnailButton()758     private void updateThumbnailButton() {
759         // Update last image if URI is invalid and the storage is ready.
760         ContentResolver contentResolver = getContentResolver();
761         if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) {
762             mThumbnail = Thumbnail.getLastThumbnail(contentResolver);
763         }
764         if (mThumbnail != null) {
765             mThumbnailView.setBitmap(mThumbnail.getBitmap());
766         } else {
767             mThumbnailView.setBitmap(null);
768         }
769     }
770 
saveHighResMosaic()771     public void saveHighResMosaic() {
772         runBackgroundThread(new Thread() {
773             @Override
774             public void run() {
775                 MosaicJpeg jpeg = generateFinalMosaic(true);
776 
777                 if (jpeg == null) {  // Cancelled by user.
778                     mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
779                 } else if (!jpeg.isValid) {  // Error when generating mosaic.
780                     mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
781                 } else {
782                     int orientation = Exif.getOrientation(jpeg.data);
783                     Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
784                     if (uri != null) {
785                         // Create a thumbnail whose width or height is equal or bigger
786                         // than the screen's width or height.
787                         int widthRatio = (int) Math.ceil((double) jpeg.width
788                                 / mPanoLayout.getWidth());
789                         int heightRatio = (int) Math.ceil((double) jpeg.height
790                                 / mPanoLayout.getHeight());
791                         int inSampleSize = Integer.highestOneBit(
792                                 Math.max(widthRatio, heightRatio));
793                         mThumbnail = Thumbnail.createThumbnail(
794                                 jpeg.data, orientation, inSampleSize, uri);
795                     }
796                     mMainHandler.sendMessage(
797                             mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
798                 }
799             }
800         });
801         reportProgress();
802     }
803 
showDialog(String str)804     private void showDialog(String str) {
805           mProgressDialog = new ProgressDialog(this);
806           mProgressDialog.setMessage(str);
807           mProgressDialog.show();
808     }
809 
runBackgroundThread(Thread thread)810     private void runBackgroundThread(Thread thread) {
811         mThreadRunning = true;
812         thread.start();
813     }
814 
onBackgroundThreadFinished()815     private void onBackgroundThreadFinished() {
816         mThreadRunning = false;
817         if (mProgressDialog != null) {
818             mProgressDialog.dismiss();
819             mProgressDialog = null;
820         }
821     }
822 
cancelHighResComputation()823     private void cancelHighResComputation() {
824         mCancelComputation = true;
825         synchronized (mWaitObject) {
826             mWaitObject.notify();
827         }
828     }
829 
830     @OnClickAttr
onCancelButtonClicked(View v)831     public void onCancelButtonClicked(View v) {
832         if (mPausing || mSurfaceTexture == null) return;
833         cancelHighResComputation();
834     }
835 
836     @OnClickAttr
onThumbnailClicked(View v)837     public void onThumbnailClicked(View v) {
838         if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
839         showSharePopup();
840     }
841 
showSharePopup()842     private void showSharePopup() {
843         if (mThumbnail == null) return;
844         Uri uri = mThumbnail.getUri();
845         if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
846             // The orientation compensation is set to 0 here because we only support landscape.
847             mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
848                     mOrientationCompensation,
849                     findViewById(R.id.frame_layout));
850         }
851         mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
852     }
853 
reset()854     private void reset() {
855         mCaptureState = CAPTURE_STATE_VIEWFINDER;
856 
857         mReviewLayout.setVisibility(View.GONE);
858         mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
859         mPanoProgressBar.setVisibility(View.GONE);
860         mCaptureLayout.setVisibility(View.VISIBLE);
861         mMosaicFrameProcessor.reset();
862 
863         mSurfaceTexture.setOnFrameAvailableListener(this);
864     }
865 
resetToPreview()866     private void resetToPreview() {
867         reset();
868         if (!mPausing) startCameraPreview();
869     }
870 
showFinalMosaic(Bitmap bitmap)871     private void showFinalMosaic(Bitmap bitmap) {
872         if (bitmap != null) {
873             mReview.setImageBitmap(bitmap);
874         }
875         mCaptureLayout.setVisibility(View.GONE);
876         mReviewLayout.setVisibility(View.VISIBLE);
877     }
878 
savePanorama(byte[] jpegData, int width, int height, int orientation)879     private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
880         if (jpegData != null) {
881             String imagePath = PanoUtil.createName(
882                     getResources().getString(R.string.pano_file_name_format), mTimeTaken);
883             return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null,
884                     orientation, jpegData, width, height);
885         }
886         return null;
887     }
888 
clearMosaicFrameProcessorIfNeeded()889     private void clearMosaicFrameProcessorIfNeeded() {
890         if (!mPausing || mThreadRunning) return;
891         mMosaicFrameProcessor.clear();
892     }
893 
initMosaicFrameProcessorIfNeeded()894     private void initMosaicFrameProcessorIfNeeded() {
895         if (mPausing || mThreadRunning) return;
896         if (mMosaicFrameProcessor == null) {
897             // Start the activity for the first time.
898             mMosaicFrameProcessor = new MosaicFrameProcessor(
899                     mPreviewWidth, mPreviewHeight, getPreviewBufSize());
900         }
901         mMosaicFrameProcessor.initialize();
902     }
903 
initSoundRecorder()904     private void initSoundRecorder() {
905         // Construct sound player; use enforced sound output if necessary
906         File recordSoundFile = new File(VIDEO_RECORD_SOUND);
907         try {
908             ParcelFileDescriptor recordSoundParcel =
909                     ParcelFileDescriptor.open(recordSoundFile,
910                             ParcelFileDescriptor.MODE_READ_ONLY);
911             AssetFileDescriptor recordSoundAsset =
912                     new AssetFileDescriptor(recordSoundParcel, 0,
913                                             AssetFileDescriptor.UNKNOWN_LENGTH);
914             if (SystemProperties.get("ro.camera.sound.forced", "0").equals("0")) {
915                 mRecordSound = new SoundPlayer(recordSoundAsset, false);
916             } else {
917                 mRecordSound = new SoundPlayer(recordSoundAsset, true);
918             }
919         } catch (java.io.FileNotFoundException e) {
920             Log.e(TAG, "System video record sound not found");
921             mRecordSound = null;
922         }
923     }
924 
releaseSoundRecorder()925     private void releaseSoundRecorder() {
926         if (mRecordSound != null) {
927             mRecordSound.release();
928             mRecordSound = null;
929         }
930     }
931 
932     @Override
onPause()933     protected void onPause() {
934         super.onPause();
935 
936         mPausing = true;
937         cancelHighResComputation();
938         // Stop the capturing first.
939         if (mCaptureState == CAPTURE_STATE_MOSAIC) {
940             stopCapture(true);
941             reset();
942         }
943         if (mSharePopup != null) mSharePopup.dismiss();
944 
945         if (mThumbnail != null && !mThumbnail.fromFile()) {
946             mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
947         }
948 
949         releaseCamera();
950         releaseSoundRecorder();
951         mMosaicView.onPause();
952         clearMosaicFrameProcessorIfNeeded();
953         mOrientationEventListener.disable();
954         System.gc();
955     }
956 
957     @Override
doOnResume()958     protected void doOnResume() {
959         mPausing = false;
960         mOrientationEventListener.enable();
961 
962         mCaptureState = CAPTURE_STATE_VIEWFINDER;
963         setupCamera();
964 
965         initSoundRecorder();
966 
967         // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
968         // has to be decided by camera device.
969         initMosaicFrameProcessorIfNeeded();
970         mMosaicView.onResume();
971     }
972 
generateFinalMosaic(boolean highRes)973     public MosaicJpeg generateFinalMosaic(boolean highRes) {
974         if (mMosaicFrameProcessor.createMosaic(highRes) == Mosaic.MOSAIC_RET_CANCELLED) {
975             return null;
976         }
977 
978         byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
979         if (imageData == null) {
980             Log.e(TAG, "getFinalMosaicNV21() returned null.");
981             return new MosaicJpeg();
982         }
983 
984         int len = imageData.length - 8;
985         int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
986                 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
987         int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
988                 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
989         Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
990 
991         if (width <= 0 || height <= 0) {
992             // TODO: pop up a error meesage indicating that the final result is not generated.
993             Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
994                     height);
995             return new MosaicJpeg();
996         }
997 
998         YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
999         ByteArrayOutputStream out = new ByteArrayOutputStream();
1000         yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
1001         try {
1002             out.close();
1003         } catch (Exception e) {
1004             Log.e(TAG, "Exception in storing final mosaic", e);
1005             return new MosaicJpeg();
1006         }
1007         return new MosaicJpeg(out.toByteArray(), width, height);
1008     }
1009 
setPreviewTexture(SurfaceTexture surface)1010     private void setPreviewTexture(SurfaceTexture surface) {
1011         try {
1012             mCameraDevice.setPreviewTexture(surface);
1013         } catch (Throwable ex) {
1014             releaseCamera();
1015             throw new RuntimeException("setPreviewTexture failed", ex);
1016         }
1017     }
1018 
startCameraPreview()1019     private void startCameraPreview() {
1020         // If we're previewing already, stop the preview first (this will blank
1021         // the screen).
1022         if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1023 
1024         int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
1025                 CameraHolder.instance().getBackCameraId());
1026         mCameraDevice.setDisplayOrientation(orientation);
1027 
1028         setPreviewTexture(mSurfaceTexture);
1029 
1030         try {
1031             Log.v(TAG, "startPreview");
1032             mCameraDevice.startPreview();
1033         } catch (Throwable ex) {
1034             releaseCamera();
1035             throw new RuntimeException("startPreview failed", ex);
1036         }
1037         mCameraState = PREVIEW_ACTIVE;
1038     }
1039 
stopCameraPreview()1040     private void stopCameraPreview() {
1041         if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1042             Log.v(TAG, "stopPreview");
1043             mCameraDevice.stopPreview();
1044         }
1045         mCameraState = PREVIEW_STOPPED;
1046     }
1047 }
1048