Home | History | Annotate | Download | only in panorama
      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 {
    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 
    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 {
    202         public PanoOrientationEventListener(Context context) {
    203             super(context);
    204         }
    205 
    206         @Override
    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 
    224     private void setOrientationIndicator(int degree) {
    225         if (mSharePopup != null) mSharePopup.setOrientation(degree);
    226     }
    227 
    228     @Override
    229     public boolean onCreateOptionsMenu(Menu menu) {
    230         super.onCreateOptionsMenu(menu);
    231 
    232         addBaseMenuItems(menu);
    233         return true;
    234     }
    235 
    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
    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
    327     public void onStart() {
    328         super.onStart();
    329         updateThumbnailButton();
    330     }
    331 
    332     private void setupCamera() {
    333         openCamera();
    334         Parameters parameters = mCameraDevice.getParameters();
    335         setupCaptureParams(parameters);
    336         configureCamera(parameters);
    337     }
    338 
    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 
    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 
    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 
    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 
    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 
    427     private void configureCamera(Parameters parameters) {
    428         mCameraDevice.setParameters(parameters);
    429     }
    430 
    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 
    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
    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
    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 
    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 
    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 
    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 
    518     private void hideDirectionIndicators() {
    519         mLeftIndicator.setVisibility(View.GONE);
    520         mRightIndicator.setVisibility(View.GONE);
    521     }
    522 
    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 
    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 
    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 
    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 
    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 
    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 
    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
    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
    726     public void onShutterButtonFocus(boolean pressed) {
    727     }
    728 
    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 
    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 
    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 
    804     private void showDialog(String str) {
    805           mProgressDialog = new ProgressDialog(this);
    806           mProgressDialog.setMessage(str);
    807           mProgressDialog.show();
    808     }
    809 
    810     private void runBackgroundThread(Thread thread) {
    811         mThreadRunning = true;
    812         thread.start();
    813     }
    814 
    815     private void onBackgroundThreadFinished() {
    816         mThreadRunning = false;
    817         if (mProgressDialog != null) {
    818             mProgressDialog.dismiss();
    819             mProgressDialog = null;
    820         }
    821     }
    822 
    823     private void cancelHighResComputation() {
    824         mCancelComputation = true;
    825         synchronized (mWaitObject) {
    826             mWaitObject.notify();
    827         }
    828     }
    829 
    830     @OnClickAttr
    831     public void onCancelButtonClicked(View v) {
    832         if (mPausing || mSurfaceTexture == null) return;
    833         cancelHighResComputation();
    834     }
    835 
    836     @OnClickAttr
    837     public void onThumbnailClicked(View v) {
    838         if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
    839         showSharePopup();
    840     }
    841 
    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 
    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 
    866     private void resetToPreview() {
    867         reset();
    868         if (!mPausing) startCameraPreview();
    869     }
    870 
    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 
    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 
    889     private void clearMosaicFrameProcessorIfNeeded() {
    890         if (!mPausing || mThreadRunning) return;
    891         mMosaicFrameProcessor.clear();
    892     }
    893 
    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 
    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 
    925     private void releaseSoundRecorder() {
    926         if (mRecordSound != null) {
    927             mRecordSound.release();
    928             mRecordSound = null;
    929         }
    930     }
    931 
    932     @Override
    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
    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 
    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 
   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 
   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 
   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