PanoramaModule.java revision 224cf5df606a3017b2899e22b1b4ed82d97c525c
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 17package com.android.camera; 18 19import android.annotation.TargetApi; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.Intent; 23import android.content.res.Configuration; 24import android.content.res.Resources; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.graphics.Canvas; 28import android.graphics.ImageFormat; 29import android.graphics.PixelFormat; 30import android.graphics.Rect; 31import android.graphics.SurfaceTexture; 32import android.graphics.YuvImage; 33import android.graphics.drawable.BitmapDrawable; 34import android.graphics.drawable.Drawable; 35import android.hardware.Camera.Parameters; 36import android.hardware.Camera.Size; 37import android.media.ExifInterface; 38import android.net.Uri; 39import android.os.AsyncTask; 40import android.os.Handler; 41import android.os.Message; 42import android.os.PowerManager; 43import android.util.Log; 44import android.view.KeyEvent; 45import android.view.LayoutInflater; 46import android.view.MotionEvent; 47import android.view.OrientationEventListener; 48import android.view.View; 49import android.view.View.OnClickListener; 50import android.view.ViewGroup; 51import android.view.WindowManager; 52import android.widget.ImageView; 53import android.widget.LinearLayout; 54import android.widget.TextView; 55 56import com.android.camera.CameraManager.CameraProxy; 57import com.android.camera.ui.LayoutChangeNotifier; 58import com.android.camera.ui.LayoutNotifyView; 59import com.android.camera.ui.PopupManager; 60import com.android.camera.ui.Rotatable; 61import com.android.camera.ui.RotateLayout; 62import com.android.gallery3d.common.ApiHelper; 63import com.android.gallery3d.ui.GLRootView; 64 65import java.io.ByteArrayOutputStream; 66import java.io.File; 67import java.io.IOException; 68import java.text.DateFormat; 69import java.text.SimpleDateFormat; 70import java.util.List; 71import java.util.TimeZone; 72 73/** 74 * Activity to handle panorama capturing. 75 */ 76@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture 77public class PanoramaModule implements CameraModule, 78 SurfaceTexture.OnFrameAvailableListener, 79 ShutterButton.OnShutterButtonListener, 80 LayoutChangeNotifier.Listener { 81 82 public static final int DEFAULT_SWEEP_ANGLE = 160; 83 public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; 84 public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; 85 86 private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1; 87 private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2; 88 private static final int MSG_RESET_TO_PREVIEW = 3; 89 private static final int MSG_CLEAR_SCREEN_DELAY = 4; 90 91 private static final int SCREEN_DELAY = 2 * 60 * 1000; 92 93 private static final String TAG = "CAM PanoModule"; 94 private static final int PREVIEW_STOPPED = 0; 95 private static final int PREVIEW_ACTIVE = 1; 96 private static final int CAPTURE_STATE_VIEWFINDER = 0; 97 private static final int CAPTURE_STATE_MOSAIC = 1; 98 99 private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; 100 private static final String GPS_TIME_FORMAT_STR = "kk/1,mm/1,ss/1"; 101 private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss"; 102 103 // The unit of speed is degrees per frame. 104 private static final float PANNING_SPEED_THRESHOLD = 2.5f; 105 106 private ContentResolver mContentResolver; 107 108 private GLRootView mGLRootView; 109 private ViewGroup mPanoLayout; 110 private LinearLayout mCaptureLayout; 111 private View mReviewLayout; 112 private ImageView mReview; 113 private RotateLayout mCaptureIndicator; 114 private PanoProgressBar mPanoProgressBar; 115 private PanoProgressBar mSavingProgressBar; 116 private LayoutNotifyView mPreviewArea; 117 private View mLeftIndicator; 118 private View mRightIndicator; 119 private MosaicPreviewRenderer mMosaicPreviewRenderer; 120 private TextView mTooFastPrompt; 121 private ShutterButton mShutterButton; 122 private Object mWaitObject = new Object(); 123 124 private DateFormat mGPSDateStampFormat; 125 private DateFormat mGPSTimeStampFormat; 126 private DateFormat mDateTimeStampFormat; 127 128 private String mPreparePreviewString; 129 private String mDialogTitle; 130 private String mDialogOkString; 131 private String mDialogPanoramaFailedString; 132 private String mDialogWaitingPreviousString; 133 134 private int mIndicatorColor; 135 private int mIndicatorColorFast; 136 137 private boolean mUsingFrontCamera; 138 private int mPreviewWidth; 139 private int mPreviewHeight; 140 private int mCameraState; 141 private int mCaptureState; 142 private PowerManager.WakeLock mPartialWakeLock; 143 private MosaicFrameProcessor mMosaicFrameProcessor; 144 private boolean mMosaicFrameProcessorInitialized; 145 private AsyncTask <Void, Void, Void> mWaitProcessorTask; 146 private long mTimeTaken; 147 private Handler mMainHandler; 148 private SurfaceTexture mCameraTexture; 149 private boolean mThreadRunning; 150 private boolean mCancelComputation; 151 private float mHorizontalViewAngle; 152 private float mVerticalViewAngle; 153 154 // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of 155 // getting a better image quality by the former. 156 private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY; 157 158 private PanoOrientationEventListener mOrientationEventListener; 159 // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise 160 // respectively. 161 private int mDeviceOrientation; 162 private int mDeviceOrientationAtCapture; 163 private int mCameraOrientation; 164 private int mOrientationCompensation; 165 166 private RotateDialogController mRotateDialog; 167 168 private SoundClips.Player mSoundPlayer; 169 170 private Runnable mOnFrameAvailableRunnable; 171 172 private CameraActivity mActivity; 173 private View mRootView; 174 private CameraProxy mCameraDevice; 175 private boolean mPaused; 176 177 private class MosaicJpeg { 178 public MosaicJpeg(byte[] data, int width, int height) { 179 this.data = data; 180 this.width = width; 181 this.height = height; 182 this.isValid = true; 183 } 184 185 public MosaicJpeg() { 186 this.data = null; 187 this.width = 0; 188 this.height = 0; 189 this.isValid = false; 190 } 191 192 public final byte[] data; 193 public final int width; 194 public final int height; 195 public final boolean isValid; 196 } 197 198 private class PanoOrientationEventListener extends OrientationEventListener { 199 public PanoOrientationEventListener(Context context) { 200 super(context); 201 } 202 203 @Override 204 public void onOrientationChanged(int orientation) { 205 // We keep the last known orientation. So if the user first orient 206 // the camera then point the camera to floor or sky, we still have 207 // the correct orientation. 208 if (orientation == ORIENTATION_UNKNOWN) return; 209 mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation); 210 // When the screen is unlocked, display rotation may change. Always 211 // calculate the up-to-date orientationCompensation. 212 int orientationCompensation = mDeviceOrientation 213 + Util.getDisplayRotation(mActivity) % 360; 214 if (mOrientationCompensation != orientationCompensation) { 215 mOrientationCompensation = orientationCompensation; 216 mActivity.getGLRoot().requestLayoutContentPane(); 217 } 218 } 219 } 220 221 @Override 222 public void init(CameraActivity activity, View parent, boolean reuseScreenNail) { 223 mActivity = activity; 224 mRootView = (ViewGroup) parent; 225 226 createContentView(); 227 228 mContentResolver = mActivity.getContentResolver(); 229 if (reuseScreenNail) { 230 mActivity.reuseCameraScreenNail(true); 231 } else { 232 mActivity.createCameraScreenNail(true); 233 } 234 235 // This runs in UI thread. 236 mOnFrameAvailableRunnable = new Runnable() { 237 @Override 238 public void run() { 239 // Frames might still be available after the activity is paused. 240 // If we call onFrameAvailable after pausing, the GL thread will crash. 241 if (mPaused) return; 242 243 if (mGLRootView.getVisibility() != View.VISIBLE) { 244 mMosaicPreviewRenderer.showPreviewFrameSync(); 245 mGLRootView.setVisibility(View.VISIBLE); 246 } else { 247 if (mCaptureState == CAPTURE_STATE_VIEWFINDER) { 248 mMosaicPreviewRenderer.showPreviewFrame(); 249 } else { 250 mMosaicPreviewRenderer.alignFrameSync(); 251 mMosaicFrameProcessor.processFrame(); 252 } 253 } 254 } 255 }; 256 257 mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR); 258 mGPSTimeStampFormat = new SimpleDateFormat(GPS_TIME_FORMAT_STR); 259 mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR); 260 TimeZone tzUTC = TimeZone.getTimeZone("UTC"); 261 mGPSDateStampFormat.setTimeZone(tzUTC); 262 mGPSTimeStampFormat.setTimeZone(tzUTC); 263 264 PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); 265 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama"); 266 267 mOrientationEventListener = new PanoOrientationEventListener(mActivity); 268 269 mMosaicFrameProcessor = MosaicFrameProcessor.getInstance(); 270 271 Resources appRes = mActivity.getResources(); 272 mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview); 273 mDialogTitle = appRes.getString(R.string.pano_dialog_title); 274 mDialogOkString = appRes.getString(R.string.dialog_ok); 275 mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed); 276 mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous); 277 278 mGLRootView = (GLRootView) mActivity.getGLRoot(); 279 280 mMainHandler = new Handler() { 281 @Override 282 public void handleMessage(Message msg) { 283 switch (msg.what) { 284 case MSG_LOW_RES_FINAL_MOSAIC_READY: 285 onBackgroundThreadFinished(); 286 showFinalMosaic((Bitmap) msg.obj); 287 saveHighResMosaic(); 288 break; 289 case MSG_GENERATE_FINAL_MOSAIC_ERROR: 290 onBackgroundThreadFinished(); 291 if (mPaused) { 292 resetToPreview(); 293 } else { 294 mRotateDialog.showAlertDialog( 295 mDialogTitle, mDialogPanoramaFailedString, 296 mDialogOkString, new Runnable() { 297 @Override 298 public void run() { 299 resetToPreview(); 300 }}, 301 null, null); 302 } 303 clearMosaicFrameProcessorIfNeeded(); 304 break; 305 case MSG_RESET_TO_PREVIEW: 306 onBackgroundThreadFinished(); 307 resetToPreview(); 308 clearMosaicFrameProcessorIfNeeded(); 309 break; 310 case MSG_CLEAR_SCREEN_DELAY: 311 mActivity.getWindow().clearFlags(WindowManager.LayoutParams. 312 FLAG_KEEP_SCREEN_ON); 313 break; 314 } 315 } 316 }; 317 } 318 319 @Override 320 public boolean dispatchTouchEvent(MotionEvent m) { 321 return mActivity.superDispatchTouchEvent(m); 322 } 323 324 private void setupCamera() throws CameraHardwareException, CameraDisabledException { 325 openCamera(); 326 Parameters parameters = mCameraDevice.getParameters(); 327 setupCaptureParams(parameters); 328 configureCamera(parameters); 329 } 330 331 private void releaseCamera() { 332 if (mCameraDevice != null) { 333 mCameraDevice.setPreviewCallbackWithBuffer(null); 334 CameraHolder.instance().release(); 335 mCameraDevice = null; 336 mCameraState = PREVIEW_STOPPED; 337 } 338 } 339 340 private void openCamera() throws CameraHardwareException, CameraDisabledException { 341 int cameraId = CameraHolder.instance().getBackCameraId(); 342 // If there is no back camera, use the first camera. Camera id starts 343 // from 0. Currently if a camera is not back facing, it is front facing. 344 // This is also forward compatible if we have a new facing other than 345 // back or front in the future. 346 if (cameraId == -1) cameraId = 0; 347 mCameraDevice = Util.openCamera(mActivity, cameraId); 348 mCameraOrientation = Util.getCameraOrientation(cameraId); 349 if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true; 350 } 351 352 private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, 353 boolean needSmaller) { 354 int pixelsDiff = DEFAULT_CAPTURE_PIXELS; 355 boolean hasFound = false; 356 for (Size size : supportedSizes) { 357 int h = size.height; 358 int w = size.width; 359 // we only want 4:3 format. 360 int d = DEFAULT_CAPTURE_PIXELS - h * w; 361 if (needSmaller && d < 0) { // no bigger preview than 960x720. 362 continue; 363 } 364 if (need4To3 && (h * 4 != w * 3)) { 365 continue; 366 } 367 d = Math.abs(d); 368 if (d < pixelsDiff) { 369 mPreviewWidth = w; 370 mPreviewHeight = h; 371 pixelsDiff = d; 372 hasFound = true; 373 } 374 } 375 return hasFound; 376 } 377 378 private void setupCaptureParams(Parameters parameters) { 379 List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); 380 if (!findBestPreviewSize(supportedSizes, true, true)) { 381 Log.w(TAG, "No 4:3 ratio preview size supported."); 382 if (!findBestPreviewSize(supportedSizes, false, true)) { 383 Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); 384 findBestPreviewSize(supportedSizes, false, false); 385 } 386 } 387 Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); 388 parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); 389 390 List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); 391 int last = frameRates.size() - 1; 392 int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; 393 int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; 394 parameters.setPreviewFpsRange(minFps, maxFps); 395 Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); 396 397 List<String> supportedFocusModes = parameters.getSupportedFocusModes(); 398 if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) { 399 parameters.setFocusMode(mTargetFocusMode); 400 } else { 401 // Use the default focus mode and log a message 402 Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode + 403 " becuase the mode is not supported."); 404 } 405 406 parameters.set(Util.RECORDING_HINT, Util.FALSE); 407 408 mHorizontalViewAngle = parameters.getHorizontalViewAngle(); 409 mVerticalViewAngle = parameters.getVerticalViewAngle(); 410 } 411 412 public int getPreviewBufSize() { 413 PixelFormat pixelInfo = new PixelFormat(); 414 PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); 415 // TODO: remove this extra 32 byte after the driver bug is fixed. 416 return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; 417 } 418 419 private void configureCamera(Parameters parameters) { 420 mCameraDevice.setParameters(parameters); 421 } 422 423 private void configMosaicPreview(int w, int h) { 424 stopCameraPreview(); 425 CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; 426 screenNail.setSize(w, h); 427 if (screenNail.getSurfaceTexture() == null) { 428 screenNail.acquireSurfaceTexture(); 429 } else { 430 screenNail.releaseSurfaceTexture(); 431 screenNail.acquireSurfaceTexture(); 432 mActivity.notifyScreenNailChanged(); 433 } 434 boolean isLandscape = (mActivity.getResources().getConfiguration().orientation 435 == Configuration.ORIENTATION_LANDSCAPE); 436 if (mMosaicPreviewRenderer != null) mMosaicPreviewRenderer.release(); 437 mMosaicPreviewRenderer = new MosaicPreviewRenderer( 438 screenNail.getSurfaceTexture(), w, h, isLandscape); 439 440 mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture(); 441 if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) { 442 resetToPreview(); 443 } 444 } 445 446 // Receives the layout change event from the preview area. So we can set 447 // the camera preview screennail to the same size and initialize the mosaic 448 // preview renderer. 449 @Override 450 public void onLayoutChange(View v, int l, int t, int r, int b) { 451 Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t)); 452 mActivity.onLayoutChange(v, l, t, r, b); 453 configMosaicPreview(r - l, b - t); 454 } 455 456 @Override 457 public void onFrameAvailable(SurfaceTexture surface) { 458 /* This function may be called by some random thread, 459 * so let's be safe and jump back to ui thread. 460 * No OpenGL calls can be done here. */ 461 mActivity.runOnUiThread(mOnFrameAvailableRunnable); 462 } 463 464 private void hideDirectionIndicators() { 465 mLeftIndicator.setVisibility(View.GONE); 466 mRightIndicator.setVisibility(View.GONE); 467 } 468 469 private void showDirectionIndicators(int direction) { 470 switch (direction) { 471 case PanoProgressBar.DIRECTION_NONE: 472 mLeftIndicator.setVisibility(View.VISIBLE); 473 mRightIndicator.setVisibility(View.VISIBLE); 474 break; 475 case PanoProgressBar.DIRECTION_LEFT: 476 mLeftIndicator.setVisibility(View.VISIBLE); 477 mRightIndicator.setVisibility(View.GONE); 478 break; 479 case PanoProgressBar.DIRECTION_RIGHT: 480 mLeftIndicator.setVisibility(View.GONE); 481 mRightIndicator.setVisibility(View.VISIBLE); 482 break; 483 } 484 } 485 486 public void startCapture() { 487 // Reset values so we can do this again. 488 mCancelComputation = false; 489 mTimeTaken = System.currentTimeMillis(); 490 mActivity.setSwipingEnabled(false); 491 mActivity.hideSwitcher(); 492 mShutterButton.setImageResource(R.drawable.btn_shutter_recording); 493 mCaptureState = CAPTURE_STATE_MOSAIC; 494 mCaptureIndicator.setVisibility(View.VISIBLE); 495 showDirectionIndicators(PanoProgressBar.DIRECTION_NONE); 496 497 mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() { 498 @Override 499 public void onProgress(boolean isFinished, float panningRateX, float panningRateY, 500 float progressX, float progressY) { 501 float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle; 502 float accumulatedVerticalAngle = progressY * mVerticalViewAngle; 503 if (isFinished 504 || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE) 505 || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) { 506 stopCapture(false); 507 } else { 508 float panningRateXInDegree = panningRateX * mHorizontalViewAngle; 509 float panningRateYInDegree = panningRateY * mVerticalViewAngle; 510 updateProgress(panningRateXInDegree, panningRateYInDegree, 511 accumulatedHorizontalAngle, accumulatedVerticalAngle); 512 } 513 } 514 }); 515 516 mPanoProgressBar.reset(); 517 // TODO: calculate the indicator width according to different devices to reflect the actual 518 // angle of view of the camera device. 519 mPanoProgressBar.setIndicatorWidth(20); 520 mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE); 521 mPanoProgressBar.setVisibility(View.VISIBLE); 522 mDeviceOrientationAtCapture = mDeviceOrientation; 523 keepScreenOn(); 524 mActivity.getOrientationManager().lockOrientation(); 525 } 526 527 private void stopCapture(boolean aborted) { 528 mCaptureState = CAPTURE_STATE_VIEWFINDER; 529 mCaptureIndicator.setVisibility(View.GONE); 530 hideTooFastIndication(); 531 hideDirectionIndicators(); 532 533 mMosaicFrameProcessor.setProgressListener(null); 534 stopCameraPreview(); 535 536 mCameraTexture.setOnFrameAvailableListener(null); 537 538 if (!aborted && !mThreadRunning) { 539 mRotateDialog.showWaitingDialog(mPreparePreviewString); 540 // Hide shutter button, shutter icon, etc when waiting for 541 // panorama to stitch 542 mActivity.hideUI(); 543 runBackgroundThread(new Thread() { 544 @Override 545 public void run() { 546 MosaicJpeg jpeg = generateFinalMosaic(false); 547 548 if (jpeg != null && jpeg.isValid) { 549 Bitmap bitmap = null; 550 bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length); 551 mMainHandler.sendMessage(mMainHandler.obtainMessage( 552 MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap)); 553 } else { 554 mMainHandler.sendMessage(mMainHandler.obtainMessage( 555 MSG_RESET_TO_PREVIEW)); 556 } 557 } 558 }); 559 } 560 keepScreenOnAwhile(); 561 } 562 563 private void showTooFastIndication() { 564 mTooFastPrompt.setVisibility(View.VISIBLE); 565 // The PreviewArea also contains the border for "too fast" indication. 566 mPreviewArea.setVisibility(View.VISIBLE); 567 mPanoProgressBar.setIndicatorColor(mIndicatorColorFast); 568 mLeftIndicator.setEnabled(true); 569 mRightIndicator.setEnabled(true); 570 } 571 572 private void hideTooFastIndication() { 573 mTooFastPrompt.setVisibility(View.GONE); 574 // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout 575 // information so we can know the size and position for mCameraScreenNail. 576 mPreviewArea.setVisibility(View.INVISIBLE); 577 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 578 mLeftIndicator.setEnabled(false); 579 mRightIndicator.setEnabled(false); 580 } 581 582 private void updateProgress(float panningRateXInDegree, float panningRateYInDegree, 583 float progressHorizontalAngle, float progressVerticalAngle) { 584 mGLRootView.requestRender(); 585 586 if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD) 587 || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) { 588 showTooFastIndication(); 589 } else { 590 hideTooFastIndication(); 591 } 592 int angleInMajorDirection = 593 (Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle)) 594 ? (int) progressHorizontalAngle 595 : (int) progressVerticalAngle; 596 mPanoProgressBar.setProgress((angleInMajorDirection)); 597 } 598 599 private void setViews(Resources appRes) { 600 mCaptureState = CAPTURE_STATE_VIEWFINDER; 601 mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar); 602 mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 603 mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done)); 604 mPanoProgressBar.setIndicatorColor(mIndicatorColor); 605 mPanoProgressBar.setOnDirectionChangeListener( 606 new PanoProgressBar.OnDirectionChangeListener () { 607 @Override 608 public void onDirectionChange(int direction) { 609 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 610 showDirectionIndicators(direction); 611 } 612 } 613 }); 614 615 mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator); 616 mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator); 617 mLeftIndicator.setEnabled(false); 618 mRightIndicator.setEnabled(false); 619 mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview); 620 // This mPreviewArea also shows the border for visual "too fast" indication. 621 mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area); 622 mPreviewArea.setOnLayoutChangeListener(this); 623 624 mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar); 625 mSavingProgressBar.setIndicatorWidth(0); 626 mSavingProgressBar.setMaxProgress(100); 627 mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty)); 628 mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication)); 629 630 mCaptureIndicator = (RotateLayout) mRootView.findViewById(R.id.pano_capture_indicator); 631 632 mReviewLayout = mRootView.findViewById(R.id.pano_review_layout); 633 mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea); 634 View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button); 635 cancelButton.setOnClickListener(new OnClickListener() { 636 @Override 637 public void onClick(View arg0) { 638 if (mPaused || mCameraTexture == null) return; 639 cancelHighResComputation(); 640 } 641 }); 642 643 mShutterButton = mActivity.getShutterButton(); 644 mShutterButton.setImageResource(R.drawable.btn_new_shutter); 645 mShutterButton.setOnShutterButtonListener(this); 646 647 if (mActivity.getResources().getConfiguration().orientation 648 == Configuration.ORIENTATION_PORTRAIT) { 649 final int[] ids = { 650 R.id.pano_pan_progress_bar_layout, 651 R.id.pano_saving_progress_bar_layout, 652 R.id.pano_rotate_reviewarea}; 653 for (int i = 0; i < ids.length; i++) { 654 Rotatable view = (Rotatable) mRootView.findViewById(ids[i]); 655 view.setOrientation(270, false); 656 } 657 } 658 } 659 660 private void createContentView() { 661 mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView); 662 Resources appRes = mActivity.getResources(); 663 mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root); 664 mIndicatorColor = appRes.getColor(R.color.pano_progress_indication); 665 mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast); 666 mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout); 667 mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog); 668 setViews(appRes); 669 } 670 671 @Override 672 public void onShutterButtonClick() { 673 // If mCameraTexture == null then GL setup is not finished yet. 674 // No buttons can be pressed. 675 if (mPaused || mThreadRunning || mCameraTexture == null) return; 676 // Since this button will stay on the screen when capturing, we need to check the state 677 // right now. 678 switch (mCaptureState) { 679 case CAPTURE_STATE_VIEWFINDER: 680 if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return; 681 mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING); 682 startCapture(); 683 break; 684 case CAPTURE_STATE_MOSAIC: 685 mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING); 686 stopCapture(false); 687 } 688 } 689 690 @Override 691 public void onShutterButtonFocus(boolean pressed) { 692 } 693 694 public void reportProgress() { 695 mSavingProgressBar.reset(); 696 mSavingProgressBar.setRightIncreasing(true); 697 Thread t = new Thread() { 698 @Override 699 public void run() { 700 while (mThreadRunning) { 701 final int progress = mMosaicFrameProcessor.reportProgress( 702 true, mCancelComputation); 703 704 try { 705 synchronized (mWaitObject) { 706 mWaitObject.wait(50); 707 } 708 } catch (InterruptedException e) { 709 throw new RuntimeException("Panorama reportProgress failed", e); 710 } 711 // Update the progress bar 712 mActivity.runOnUiThread(new Runnable() { 713 @Override 714 public void run() { 715 mSavingProgressBar.setProgress(progress); 716 } 717 }); 718 } 719 } 720 }; 721 t.start(); 722 } 723 724 private int getCaptureOrientation() { 725 // The panorama image returned from the library is oriented based on the 726 // natural orientation of a camera. We need to set an orientation for the image 727 // in its EXIF header, so the image can be displayed correctly. 728 // The orientation is calculated from compensating the 729 // device orientation at capture and the camera orientation respective to 730 // the natural orientation of the device. 731 int orientation; 732 if (mUsingFrontCamera) { 733 // mCameraOrientation is negative with respect to the front facing camera. 734 // See document of android.hardware.Camera.Parameters.setRotation. 735 orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360; 736 } else { 737 orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360; 738 } 739 return orientation; 740 } 741 742 public void saveHighResMosaic() { 743 runBackgroundThread(new Thread() { 744 @Override 745 public void run() { 746 mPartialWakeLock.acquire(); 747 MosaicJpeg jpeg; 748 try { 749 jpeg = generateFinalMosaic(true); 750 } finally { 751 mPartialWakeLock.release(); 752 } 753 754 if (jpeg == null) { // Cancelled by user. 755 mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW); 756 } else if (!jpeg.isValid) { // Error when generating mosaic. 757 mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR); 758 } else { 759 int orientation = getCaptureOrientation(); 760 Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation); 761 if (uri != null) { 762 mActivity.addSecureAlbumItemIfNeeded(false, uri); 763 Util.broadcastNewPicture(mActivity, uri); 764 } 765 mMainHandler.sendMessage( 766 mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW)); 767 } 768 } 769 }); 770 reportProgress(); 771 } 772 773 private void runBackgroundThread(Thread thread) { 774 mThreadRunning = true; 775 thread.start(); 776 } 777 778 private void onBackgroundThreadFinished() { 779 mThreadRunning = false; 780 mRotateDialog.dismissDialog(); 781 } 782 783 private void cancelHighResComputation() { 784 mCancelComputation = true; 785 synchronized (mWaitObject) { 786 mWaitObject.notify(); 787 } 788 } 789 790 // This function will be called upon the first camera frame is available. 791 private void reset() { 792 mCaptureState = CAPTURE_STATE_VIEWFINDER; 793 794 mActivity.getOrientationManager().unlockOrientation(); 795 // We should set mGLRootView visible too. However, since there might be no 796 // frame available yet, setting mGLRootView visible should be done right after 797 // the first camera frame is available and therefore it is done by 798 // mOnFirstFrameAvailableRunnable. 799 mActivity.setSwipingEnabled(true); 800 mShutterButton.setImageResource(R.drawable.btn_new_shutter); 801 mReviewLayout.setVisibility(View.GONE); 802 mPanoProgressBar.setVisibility(View.GONE); 803 // Orientation change will trigger onLayoutChange->configMosaicPreview-> 804 // resetToPreview. Do not show the capture UI in film strip. 805 if (mActivity.mShowCameraAppView) { 806 mCaptureLayout.setVisibility(View.VISIBLE); 807 mActivity.showUI(); 808 } 809 mMosaicFrameProcessor.reset(); 810 } 811 812 private void resetToPreview() { 813 reset(); 814 if (!mPaused) startCameraPreview(); 815 } 816 817 private static class FlipBitmapDrawable extends BitmapDrawable { 818 819 public FlipBitmapDrawable(Resources res, Bitmap bitmap) { 820 super(res, bitmap); 821 } 822 823 @Override 824 public void draw(Canvas canvas) { 825 Rect bounds = getBounds(); 826 int cx = bounds.centerX(); 827 int cy = bounds.centerY(); 828 canvas.save(Canvas.MATRIX_SAVE_FLAG); 829 canvas.rotate(180, cx, cy); 830 super.draw(canvas); 831 canvas.restore(); 832 } 833 } 834 835 private void showFinalMosaic(Bitmap bitmap) { 836 if (bitmap != null) { 837 int orientation = getCaptureOrientation(); 838 if (orientation >= 180) { 839 // We need to flip the drawable to compensate 840 mReview.setImageDrawable(new FlipBitmapDrawable( 841 mActivity.getResources(), bitmap)); 842 } else { 843 mReview.setImageBitmap(bitmap); 844 } 845 } 846 847 mGLRootView.setVisibility(View.GONE); 848 mCaptureLayout.setVisibility(View.GONE); 849 mReviewLayout.setVisibility(View.VISIBLE); 850 } 851 852 private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) { 853 if (jpegData != null) { 854 String filename = PanoUtil.createName( 855 mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); 856 String filepath = Storage.generateFilepath(filename); 857 Storage.writeFile(filepath, jpegData); 858 859 // Add Exif tags. 860 try { 861 ExifInterface exif = new ExifInterface(filepath); 862 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, 863 mGPSDateStampFormat.format(mTimeTaken)); 864 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, 865 mGPSTimeStampFormat.format(mTimeTaken)); 866 exif.setAttribute(ExifInterface.TAG_DATETIME, 867 mDateTimeStampFormat.format(mTimeTaken)); 868 exif.setAttribute(ExifInterface.TAG_ORIENTATION, 869 getExifOrientation(orientation)); 870 exif.saveAttributes(); 871 } catch (IOException e) { 872 Log.e(TAG, "Cannot set EXIF for " + filepath, e); 873 } 874 875 int jpegLength = (int) (new File(filepath).length()); 876 return Storage.addImage(mContentResolver, filename, mTimeTaken, 877 null, orientation, jpegLength, filepath, width, height); 878 } 879 return null; 880 } 881 882 private static String getExifOrientation(int orientation) { 883 switch (orientation) { 884 case 0: 885 return String.valueOf(ExifInterface.ORIENTATION_NORMAL); 886 case 90: 887 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90); 888 case 180: 889 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180); 890 case 270: 891 return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270); 892 default: 893 throw new AssertionError("invalid: " + orientation); 894 } 895 } 896 897 private void clearMosaicFrameProcessorIfNeeded() { 898 if (!mPaused || mThreadRunning) return; 899 // Only clear the processor if it is initialized by this activity 900 // instance. Other activity instances may be using it. 901 if (mMosaicFrameProcessorInitialized) { 902 mMosaicFrameProcessor.clear(); 903 mMosaicFrameProcessorInitialized = false; 904 } 905 } 906 907 private void initMosaicFrameProcessorIfNeeded() { 908 if (mPaused || mThreadRunning) return; 909 mMosaicFrameProcessor.initialize( 910 mPreviewWidth, mPreviewHeight, getPreviewBufSize()); 911 mMosaicFrameProcessorInitialized = true; 912 } 913 914 @Override 915 public void onPauseBeforeSuper() { 916 mPaused = true; 917 } 918 919 @Override 920 public void onPauseAfterSuper() { 921 mOrientationEventListener.disable(); 922 if (mCameraDevice == null) { 923 // Camera open failed. Nothing should be done here. 924 return; 925 } 926 // Stop the capturing first. 927 if (mCaptureState == CAPTURE_STATE_MOSAIC) { 928 stopCapture(true); 929 reset(); 930 } 931 932 releaseCamera(); 933 mCameraTexture = null; 934 935 // The preview renderer might not have a chance to be initialized before 936 // onPause(). 937 if (mMosaicPreviewRenderer != null) { 938 mMosaicPreviewRenderer.release(); 939 mMosaicPreviewRenderer = null; 940 } 941 942 clearMosaicFrameProcessorIfNeeded(); 943 if (mWaitProcessorTask != null) { 944 mWaitProcessorTask.cancel(true); 945 mWaitProcessorTask = null; 946 } 947 resetScreenOn(); 948 if (mSoundPlayer != null) { 949 mSoundPlayer.release(); 950 mSoundPlayer = null; 951 } 952 CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail; 953 if (screenNail.getSurfaceTexture() != null) { 954 screenNail.releaseSurfaceTexture(); 955 } 956 System.gc(); 957 } 958 959 @Override 960 public void onConfigurationChanged(Configuration newConfig) { 961 962 Drawable lowResReview = null; 963 if (mThreadRunning) lowResReview = mReview.getDrawable(); 964 965 // Change layout in response to configuration change 966 mCaptureLayout.setOrientation( 967 newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE 968 ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); 969 mCaptureLayout.removeAllViews(); 970 LayoutInflater inflater = mActivity.getLayoutInflater(); 971 inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout); 972 973 mPanoLayout.removeView(mReviewLayout); 974 inflater.inflate(R.layout.pano_review, mPanoLayout); 975 976 setViews(mActivity.getResources()); 977 if (mThreadRunning) { 978 mReview.setImageDrawable(lowResReview); 979 mCaptureLayout.setVisibility(View.GONE); 980 mReviewLayout.setVisibility(View.VISIBLE); 981 } 982 } 983 984 @Override 985 public void onOrientationChanged(int orientation) { 986 } 987 988 @Override 989 public void onResumeBeforeSuper() { 990 mPaused = false; 991 } 992 993 @Override 994 public void onResumeAfterSuper() { 995 mOrientationEventListener.enable(); 996 997 mCaptureState = CAPTURE_STATE_VIEWFINDER; 998 999 try { 1000 setupCamera(); 1001 } catch (CameraHardwareException e) { 1002 Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera); 1003 return; 1004 } catch (CameraDisabledException e) { 1005 Util.showErrorAndFinish(mActivity, R.string.camera_disabled); 1006 return; 1007 } 1008 1009 // Set up sound playback for shutter button 1010 mSoundPlayer = SoundClips.getPlayer(mActivity); 1011 1012 // Check if another panorama instance is using the mosaic frame processor. 1013 mRotateDialog.dismissDialog(); 1014 if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { 1015 mGLRootView.setVisibility(View.GONE); 1016 mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString); 1017 // If stitching is still going on, make sure switcher and shutter button 1018 // are not showing 1019 mActivity.hideUI(); 1020 mWaitProcessorTask = new WaitProcessorTask().execute(); 1021 } else { 1022 if (!mThreadRunning) mGLRootView.setVisibility(View.VISIBLE); 1023 // Camera must be initialized before MosaicFrameProcessor is 1024 // initialized. The preview size has to be decided by camera device. 1025 initMosaicFrameProcessorIfNeeded(); 1026 int w = mPreviewArea.getWidth(); 1027 int h = mPreviewArea.getHeight(); 1028 if (w != 0 && h != 0) { // The layout has been calculated. 1029 configMosaicPreview(w, h); 1030 } 1031 } 1032 keepScreenOnAwhile(); 1033 1034 // Dismiss open menu if exists. 1035 PopupManager.getInstance(mActivity).notifyShowPopup(null); 1036 mRootView.requestLayout(); 1037 } 1038 1039 /** 1040 * Generate the final mosaic image. 1041 * 1042 * @param highRes flag to indicate whether we want to get a high-res version. 1043 * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation 1044 * process is cancelled; and a MosaicJpeg with its isValid flag set to false if there 1045 * is an error in generating the final mosaic. 1046 */ 1047 public MosaicJpeg generateFinalMosaic(boolean highRes) { 1048 int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes); 1049 if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) { 1050 return null; 1051 } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) { 1052 return new MosaicJpeg(); 1053 } 1054 1055 byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21(); 1056 if (imageData == null) { 1057 Log.e(TAG, "getFinalMosaicNV21() returned null."); 1058 return new MosaicJpeg(); 1059 } 1060 1061 int len = imageData.length - 8; 1062 int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) 1063 + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); 1064 int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) 1065 + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); 1066 Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); 1067 1068 if (width <= 0 || height <= 0) { 1069 // TODO: pop up an error message indicating that the final result is not generated. 1070 Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " + 1071 height); 1072 return new MosaicJpeg(); 1073 } 1074 1075 YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); 1076 ByteArrayOutputStream out = new ByteArrayOutputStream(); 1077 yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); 1078 try { 1079 out.close(); 1080 } catch (Exception e) { 1081 Log.e(TAG, "Exception in storing final mosaic", e); 1082 return new MosaicJpeg(); 1083 } 1084 return new MosaicJpeg(out.toByteArray(), width, height); 1085 } 1086 1087 private void startCameraPreview() { 1088 if (mCameraDevice == null) { 1089 // Camera open failed. Return. 1090 return; 1091 } 1092 1093 // This works around a driver issue. startPreview may fail if 1094 // stopPreview/setPreviewTexture/startPreview are called several times 1095 // in a row. mCameraTexture can be null after pressing home during 1096 // mosaic generation and coming back. Preview will be started later in 1097 // onLayoutChange->configMosaicPreview. This also reduces the latency. 1098 if (mCameraTexture == null) return; 1099 1100 // If we're previewing already, stop the preview first (this will blank 1101 // the screen). 1102 if (mCameraState != PREVIEW_STOPPED) stopCameraPreview(); 1103 1104 // Set the display orientation to 0, so that the underlying mosaic library 1105 // can always get undistorted mPreviewWidth x mPreviewHeight image data 1106 // from SurfaceTexture. 1107 mCameraDevice.setDisplayOrientation(0); 1108 1109 if (mCameraTexture != null) mCameraTexture.setOnFrameAvailableListener(this); 1110 mCameraDevice.setPreviewTextureAsync(mCameraTexture); 1111 1112 mCameraDevice.startPreviewAsync(); 1113 mCameraState = PREVIEW_ACTIVE; 1114 } 1115 1116 private void stopCameraPreview() { 1117 if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) { 1118 Log.v(TAG, "stopPreview"); 1119 mCameraDevice.stopPreview(); 1120 } 1121 mCameraState = PREVIEW_STOPPED; 1122 } 1123 1124 @Override 1125 public void onUserInteraction() { 1126 if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile(); 1127 } 1128 1129 @Override 1130 public boolean onBackPressed() { 1131 // If panorama is generating low res or high res mosaic, ignore back 1132 // key. So the activity will not be destroyed. 1133 if (mThreadRunning) return true; 1134 return false; 1135 } 1136 1137 private void resetScreenOn() { 1138 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1139 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1140 } 1141 1142 private void keepScreenOnAwhile() { 1143 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1144 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1145 mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY); 1146 } 1147 1148 private void keepScreenOn() { 1149 mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY); 1150 mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1151 } 1152 1153 private class WaitProcessorTask extends AsyncTask<Void, Void, Void> { 1154 @Override 1155 protected Void doInBackground(Void... params) { 1156 synchronized (mMosaicFrameProcessor) { 1157 while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) { 1158 try { 1159 mMosaicFrameProcessor.wait(); 1160 } catch (Exception e) { 1161 // ignore 1162 } 1163 } 1164 } 1165 return null; 1166 } 1167 1168 @Override 1169 protected void onPostExecute(Void result) { 1170 mWaitProcessorTask = null; 1171 mRotateDialog.dismissDialog(); 1172 mGLRootView.setVisibility(View.VISIBLE); 1173 initMosaicFrameProcessorIfNeeded(); 1174 int w = mPreviewArea.getWidth(); 1175 int h = mPreviewArea.getHeight(); 1176 if (w != 0 && h != 0) { // The layout has been calculated. 1177 configMosaicPreview(w, h); 1178 } 1179 resetToPreview(); 1180 } 1181 } 1182 1183 @Override 1184 public void onFullScreenChanged(boolean full) { 1185 } 1186 1187 1188 @Override 1189 public void onStop() { 1190 } 1191 1192 @Override 1193 public void installIntentFilter() { 1194 } 1195 1196 @Override 1197 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1198 } 1199 1200 1201 @Override 1202 public boolean onKeyDown(int keyCode, KeyEvent event) { 1203 return false; 1204 } 1205 1206 @Override 1207 public boolean onKeyUp(int keyCode, KeyEvent event) { 1208 return false; 1209 } 1210 1211 @Override 1212 public void onSingleTapUp(View view, int x, int y) { 1213 } 1214 1215 @Override 1216 public void onPreviewTextureCopied() { 1217 } 1218 1219 @Override 1220 public void onCaptureTextureCopied() { 1221 } 1222 1223 @Override 1224 public boolean updateStorageHintOnResume() { 1225 return false; 1226 } 1227 1228 @Override 1229 public void updateCameraAppView() { 1230 } 1231 1232 @Override 1233 public boolean collapseCameraControls() { 1234 return false; 1235 } 1236 1237 @Override 1238 public boolean needsSwitcher() { 1239 return true; 1240 } 1241 1242 @Override 1243 public void onShowSwitcherPopup() { 1244 } 1245} 1246