VideoEditorActivity.java revision a693d9a493024c2dc474917ff8b47fb58d87089b
1/* 2 * Copyright (C) 2010 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.videoeditor; 18 19import java.util.ArrayList; 20import java.util.Date; 21import java.util.NoSuchElementException; 22import java.util.Queue; 23import java.util.concurrent.LinkedBlockingQueue; 24import java.text.SimpleDateFormat; 25 26import android.app.ActionBar; 27import android.app.AlertDialog; 28import android.app.Dialog; 29import android.app.ProgressDialog; 30import android.content.ContentValues; 31import android.content.Context; 32import android.content.DialogInterface; 33import android.content.Intent; 34import android.graphics.Bitmap; 35import android.graphics.Color; 36import android.graphics.Rect; 37import android.graphics.Bitmap.Config; 38import android.media.videoeditor.MediaItem; 39import android.media.videoeditor.MediaProperties; 40import android.media.videoeditor.VideoEditor; 41import android.net.Uri; 42import android.os.Bundle; 43import android.os.Environment; 44import android.os.Handler; 45import android.os.Looper; 46import android.os.PowerManager; 47import android.provider.MediaStore; 48import android.text.InputType; 49import android.util.DisplayMetrics; 50import android.util.Log; 51import android.view.Display; 52import android.view.GestureDetector; 53import android.view.Menu; 54import android.view.MenuInflater; 55import android.view.MenuItem; 56import android.view.MotionEvent; 57import android.view.ScaleGestureDetector; 58import android.view.SurfaceHolder; 59import android.view.View; 60import android.view.ViewGroup; 61import android.view.WindowManager; 62import android.widget.FrameLayout; 63import android.widget.ImageButton; 64import android.widget.ImageView; 65import android.widget.TextView; 66import android.widget.Toast; 67 68import com.android.videoeditor.service.ApiService; 69import com.android.videoeditor.service.MovieMediaItem; 70import com.android.videoeditor.service.VideoEditorProject; 71import com.android.videoeditor.util.FileUtils; 72import com.android.videoeditor.util.MediaItemUtils; 73import com.android.videoeditor.util.StringUtils; 74import com.android.videoeditor.widgets.AudioTrackLinearLayout; 75import com.android.videoeditor.widgets.MediaLinearLayout; 76import com.android.videoeditor.widgets.MediaLinearLayoutListener; 77import com.android.videoeditor.widgets.OverlayLinearLayout; 78import com.android.videoeditor.widgets.PlayheadView; 79import com.android.videoeditor.widgets.PreviewSurfaceView; 80import com.android.videoeditor.widgets.ScrollViewListener; 81import com.android.videoeditor.widgets.TimelineHorizontalScrollView; 82import com.android.videoeditor.widgets.TimelineRelativeLayout; 83import com.android.videoeditor.widgets.ZoomControl; 84 85/** 86 * Main activity of the video editor. It handles video editing of 87 * a project. 88 */ 89public class VideoEditorActivity extends VideoEditorBaseActivity 90 implements SurfaceHolder.Callback { 91 private static final String TAG = "VideoEditorActivity"; 92 93 // State keys 94 private static final String STATE_INSERT_AFTER_MEDIA_ITEM_ID = "insert_after_media_item_id"; 95 private static final String STATE_PLAYING = "playing"; 96 private static final String STATE_CAPTURE_URI = "capture_uri"; 97 private static final String STATE_SELECTED_POS_ID = "selected_pos_id"; 98 99 private static final String DCIM = 100 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString(); 101 private static final String DIRECTORY = DCIM + "/Camera"; 102 103 // Dialog ids 104 private static final int DIALOG_DELETE_PROJECT_ID = 1; 105 private static final int DIALOG_EDIT_PROJECT_NAME_ID = 2; 106 private static final int DIALOG_CHOOSE_ASPECT_RATIO_ID = 3; 107 private static final int DIALOG_EXPORT_OPTIONS_ID = 4; 108 109 public static final int DIALOG_REMOVE_MEDIA_ITEM_ID = 10; 110 public static final int DIALOG_REMOVE_TRANSITION_ID = 11; 111 public static final int DIALOG_CHANGE_RENDERING_MODE_ID = 12; 112 public static final int DIALOG_REMOVE_OVERLAY_ID = 13; 113 public static final int DIALOG_REMOVE_EFFECT_ID = 14; 114 public static final int DIALOG_REMOVE_AUDIO_TRACK_ID = 15; 115 116 // Dialog parameters 117 private static final String PARAM_ASPECT_RATIOS_LIST = "aspect_ratios"; 118 private static final String PARAM_CURRENT_ASPECT_RATIO_INDEX = "current_aspect_ratio"; 119 120 // Request codes 121 private static final int REQUEST_CODE_IMPORT_VIDEO = 1; 122 private static final int REQUEST_CODE_IMPORT_IMAGE = 2; 123 private static final int REQUEST_CODE_IMPORT_MUSIC = 3; 124 private static final int REQUEST_CODE_CAPTURE_VIDEO = 4; 125 private static final int REQUEST_CODE_CAPTURE_IMAGE = 5; 126 127 public static final int REQUEST_CODE_EDIT_TRANSITION = 10; 128 public static final int REQUEST_CODE_PICK_TRANSITION = 11; 129 public static final int REQUEST_CODE_PICK_OVERLAY = 12; 130 public static final int REQUEST_CODE_KEN_BURNS = 13; 131 132 // The maximum zoom level 133 private static final int MAX_ZOOM_LEVEL = 120; 134 private static final int ZOOM_STEP = 2; 135 136 // Threshold in width dip for showing title in action bar. 137 private static final int SHOW_TITLE_THRESHOLD_WIDTH_DIP = 1000; 138 139 private final TimelineRelativeLayout.LayoutCallback mLayoutCallback = 140 new TimelineRelativeLayout.LayoutCallback() { 141 142 @Override 143 public void onLayoutComplete() { 144 // Scroll the timeline such that the specified position 145 // is in the center of the screen. 146 mTimelineScroller.appScrollTo(timeToDimension(mProject.getPlayheadPos()), false); 147 } 148 }; 149 150 // Instance variables 151 private PreviewSurfaceView mSurfaceView; 152 private SurfaceHolder mSurfaceHolder; 153 private boolean mHaveSurface; 154 private boolean mResumed; 155 private ImageView mOverlayView; 156 private PreviewThread mPreviewThread; 157 private View mEditorProjectView; 158 private View mEditorEmptyView; 159 private TimelineHorizontalScrollView mTimelineScroller; 160 private TimelineRelativeLayout mTimelineLayout; 161 private OverlayLinearLayout mOverlayLayout; 162 private AudioTrackLinearLayout mAudioTrackLayout; 163 private MediaLinearLayout mMediaLayout; 164 private int mMediaLayoutSelectedPos; 165 private PlayheadView mPlayheadView; 166 private TextView mTimeView; 167 private ImageButton mPreviewPlayButton; 168 private ImageButton mPreviewRewindButton, mPreviewNextButton, mPreviewPrevButton; 169 private int mActivityWidth; 170 private String mInsertMediaItemAfterMediaItemId; 171 private long mCurrentPlayheadPosMs; 172 private ProgressDialog mExportProgressDialog; 173 private ZoomControl mZoomControl; 174 private PowerManager.WakeLock mCpuWakeLock; 175 176 // Variables used in onActivityResult 177 private Uri mAddMediaItemVideoUri; 178 private Uri mAddMediaItemImageUri; 179 private Uri mAddAudioTrackUri; 180 private String mAddTransitionAfterMediaId; 181 private int mAddTransitionType; 182 private long mAddTransitionDurationMs; 183 private String mEditTransitionAfterMediaId, mEditTransitionId; 184 private int mEditTransitionType; 185 private long mEditTransitionDurationMs; 186 private String mAddOverlayMediaItemId; 187 private Bundle mAddOverlayUserAttributes; 188 private String mEditOverlayMediaItemId; 189 private String mEditOverlayId; 190 private Bundle mEditOverlayUserAttributes; 191 private String mAddEffectMediaItemId; 192 private int mAddEffectType; 193 private Rect mAddKenBurnsStartRect; 194 private Rect mAddKenBurnsEndRect; 195 private boolean mRestartPreview; 196 private Uri mCaptureMediaUri; 197 198 @Override 199 public void onCreate(Bundle savedInstanceState) { 200 super.onCreate(savedInstanceState); 201 202 final ActionBar actionBar = getActionBar(); 203 DisplayMetrics displayMetrics = new DisplayMetrics(); 204 getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); 205 // Only show title on large screens (width >= 1000 dip). 206 int widthDip = (int) (displayMetrics.widthPixels / displayMetrics.scaledDensity); 207 if (widthDip >= SHOW_TITLE_THRESHOLD_WIDTH_DIP) { 208 actionBar.setDisplayOptions(actionBar.getDisplayOptions() | ActionBar.DISPLAY_SHOW_TITLE); 209 actionBar.setTitle(R.string.full_app_name); 210 } 211 212 // Prepare the surface holder 213 mSurfaceView = (PreviewSurfaceView) findViewById(R.id.video_view); 214 mSurfaceHolder = mSurfaceView.getHolder(); 215 mSurfaceHolder.addCallback(this); 216 mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 217 218 mOverlayView = (ImageView)findViewById(R.id.overlay_layer); 219 220 mEditorProjectView = findViewById(R.id.editor_project_view); 221 mEditorEmptyView = findViewById(R.id.empty_project_view); 222 223 mTimelineScroller = (TimelineHorizontalScrollView)findViewById(R.id.timeline_scroller); 224 mTimelineLayout = (TimelineRelativeLayout)findViewById(R.id.timeline); 225 mMediaLayout = (MediaLinearLayout)findViewById(R.id.timeline_media); 226 mOverlayLayout = (OverlayLinearLayout)findViewById(R.id.timeline_overlays); 227 mAudioTrackLayout = (AudioTrackLinearLayout)findViewById(R.id.timeline_audio_tracks); 228 mPlayheadView = (PlayheadView)findViewById(R.id.timeline_playhead); 229 230 mPreviewPlayButton = (ImageButton)findViewById(R.id.editor_play); 231 mPreviewRewindButton = (ImageButton)findViewById(R.id.editor_rewind); 232 mPreviewNextButton = (ImageButton)findViewById(R.id.editor_next); 233 mPreviewPrevButton = (ImageButton)findViewById(R.id.editor_prev); 234 235 mTimeView = (TextView)findViewById(R.id.editor_time); 236 237 actionBar.setDisplayHomeAsUpEnabled(true); 238 239 mMediaLayout.setListener(new MediaLinearLayoutListener() { 240 @Override 241 public void onRequestScrollBy(int scrollBy, boolean smooth) { 242 mTimelineScroller.appScrollBy(scrollBy, smooth); 243 } 244 245 @Override 246 public void onRequestMovePlayhead(long scrollToTime, boolean smooth) { 247 movePlayhead(scrollToTime); 248 } 249 250 @Override 251 public void onAddMediaItem(String afterMediaItemId) { 252 mInsertMediaItemAfterMediaItemId = afterMediaItemId; 253 254 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 255 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 256 intent.setType("video/*"); 257 startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO); 258 } 259 260 @Override 261 public void onTrimMediaItemBegin(MovieMediaItem mediaItem) { 262 onProjectEditStateChange(true); 263 } 264 265 @Override 266 public void onTrimMediaItem(MovieMediaItem mediaItem, long timeMs) { 267 updateTimelineDuration(); 268 if (mProject != null && isPreviewPlaying()) { 269 if (mediaItem.isVideoClip()) { 270 if (timeMs >= 0) { 271 mPreviewThread.renderMediaItemFrame(mediaItem, timeMs); 272 } 273 } else { 274 mPreviewThread.previewFrame(mProject, 275 mProject.getMediaItemBeginTime(mediaItem.getId()) + timeMs, 276 mProject.getMediaItemCount() == 0); 277 } 278 } 279 } 280 281 @Override 282 public void onTrimMediaItemEnd(MovieMediaItem mediaItem, long timeMs) { 283 onProjectEditStateChange(false); 284 // We need to repaint the timeline layout to clear the old 285 // playhead position (the one drawn during trimming). 286 mTimelineLayout.invalidate(); 287 showPreviewFrame(); 288 } 289 }); 290 291 mAudioTrackLayout.setListener(new AudioTrackLinearLayout.AudioTracksLayoutListener() { 292 @Override 293 public void onAddAudioTrack() { 294 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 295 intent.setType("audio/*"); 296 startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC); 297 } 298 }); 299 300 mTimelineScroller.addScrollListener(new ScrollViewListener() { 301 // Instance variables 302 private int mActiveWidth; 303 private long mDurationMs; 304 305 @Override 306 public void onScrollBegin(View view, int scrollX, int scrollY, boolean appScroll) { 307 if (!appScroll && mProject != null) { 308 mActiveWidth = mMediaLayout.getWidth() - mActivityWidth; 309 mDurationMs = mProject.computeDuration(); 310 } else { 311 mActiveWidth = 0; 312 } 313 } 314 315 @Override 316 public void onScrollProgress(View view, int scrollX, int scrollY, boolean appScroll) { 317 } 318 319 @Override 320 public void onScrollEnd(View view, int scrollX, int scrollY, boolean appScroll) { 321 // We check if the project is valid since the project may 322 // close while scrolling 323 if (!appScroll && mActiveWidth > 0 && mProject != null) { 324 final long timeMs = (scrollX * mDurationMs) / mActiveWidth; 325 if (setPlayhead(timeMs < 0 ? 0 : timeMs)) { 326 showPreviewFrame(); 327 } 328 } 329 } 330 }); 331 332 mTimelineScroller.setScaleListener(new ScaleGestureDetector.SimpleOnScaleGestureListener() { 333 // Guard against this many scale events in the opposite direction 334 private static final int SCALE_TOLERANCE = 3; 335 336 private int mLastScaleFactorSign; 337 private float mLastScaleFactor; 338 339 @Override 340 public boolean onScaleBegin(ScaleGestureDetector detector) { 341 mLastScaleFactorSign = 0; 342 return true; 343 } 344 345 @Override 346 public boolean onScale(ScaleGestureDetector detector) { 347 if (mProject == null) { 348 return false; 349 } 350 351 final float scaleFactor = detector.getScaleFactor(); 352 final float deltaScaleFactor = scaleFactor - mLastScaleFactor; 353 if (deltaScaleFactor > 0.01f || deltaScaleFactor < -0.01f) { 354 if (scaleFactor < 1.0f) { 355 if (mLastScaleFactorSign <= 0) { 356 zoomTimeline(mProject.getZoomLevel() - ZOOM_STEP, true); 357 } 358 359 if (mLastScaleFactorSign > -SCALE_TOLERANCE) { 360 mLastScaleFactorSign--; 361 } 362 } else if (scaleFactor > 1.0f) { 363 if (mLastScaleFactorSign >= 0) { 364 zoomTimeline(mProject.getZoomLevel() + ZOOM_STEP, true); 365 } 366 367 if (mLastScaleFactorSign < SCALE_TOLERANCE) { 368 mLastScaleFactorSign++; 369 } 370 } 371 } 372 373 mLastScaleFactor = scaleFactor; 374 return true; 375 } 376 377 @Override 378 public void onScaleEnd(ScaleGestureDetector detector) { 379 } 380 }); 381 382 if (savedInstanceState != null) { 383 mInsertMediaItemAfterMediaItemId = savedInstanceState.getString( 384 STATE_INSERT_AFTER_MEDIA_ITEM_ID); 385 mRestartPreview = savedInstanceState.getBoolean(STATE_PLAYING); 386 mCaptureMediaUri = savedInstanceState.getParcelable(STATE_CAPTURE_URI); 387 mMediaLayoutSelectedPos = savedInstanceState.getInt(STATE_SELECTED_POS_ID, -1); 388 } else { 389 mRestartPreview = false; 390 mMediaLayoutSelectedPos = -1; 391 } 392 393 // Compute the activity width 394 final Display display = getWindowManager().getDefaultDisplay(); 395 mActivityWidth = display.getWidth(); 396 397 mSurfaceView.setGestureListener(new GestureDetector(this, 398 new GestureDetector.SimpleOnGestureListener() { 399 @Override 400 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 401 float velocityY) { 402 if (isPreviewPlaying()) { 403 return false; 404 } 405 406 mTimelineScroller.fling(-(int)velocityX); 407 return true; 408 } 409 410 @Override 411 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, 412 float distanceY) { 413 if (isPreviewPlaying()) { 414 return false; 415 } 416 417 mTimelineScroller.scrollBy((int)distanceX, 0); 418 return true; 419 } 420 })); 421 422 mZoomControl = ((ZoomControl)findViewById(R.id.editor_zoom)); 423 mZoomControl.setMax(MAX_ZOOM_LEVEL); 424 mZoomControl.setOnZoomChangeListener(new ZoomControl.OnZoomChangeListener() { 425 426 @Override 427 public void onProgressChanged(int progress, boolean fromUser) { 428 if (mProject != null) { 429 zoomTimeline(progress, false); 430 } 431 } 432 }); 433 434 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 435 mCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Video Editor Activity CPU Wake Lock"); 436 } 437 438 @Override 439 public void onPause() { 440 super.onPause(); 441 mResumed = false; 442 443 // Stop the preview now (we will stop it in surfaceDestroyed(), but 444 // that may be too late for releasing resources to other activities) 445 stopPreviewThread(); 446 447 // Dismiss the export progress dialog. If the export will still be pending 448 // when we return to this activity, we will display this dialog again. 449 if (mExportProgressDialog != null) { 450 mExportProgressDialog.dismiss(); 451 mExportProgressDialog = null; 452 } 453 } 454 455 @Override 456 public void onResume() { 457 super.onResume(); 458 mResumed = true; 459 460 if (mProject != null) { 461 mMediaLayout.onResume(); 462 mAudioTrackLayout.onResume(); 463 } 464 465 createPreviewThreadIfNeeded(); 466 } 467 468 private void createPreviewThreadIfNeeded() { 469 // We want to have the preview thread if and only if (1) we have a 470 // surface, and (2) we are resumed. 471 if (mHaveSurface && mResumed && mPreviewThread == null) { 472 mPreviewThread = new PreviewThread(mSurfaceHolder); 473 restartPreview(); 474 } 475 } 476 477 @Override 478 public void onSaveInstanceState(Bundle outState) { 479 super.onSaveInstanceState(outState); 480 481 outState.putString(STATE_INSERT_AFTER_MEDIA_ITEM_ID, mInsertMediaItemAfterMediaItemId); 482 outState.putBoolean(STATE_PLAYING, isPreviewPlaying() || mRestartPreview); 483 outState.putParcelable(STATE_CAPTURE_URI, mCaptureMediaUri); 484 outState.putInt(STATE_SELECTED_POS_ID, mMediaLayout.getSelectedViewPos()); 485 } 486 487 @Override 488 public boolean onCreateOptionsMenu(Menu menu) { 489 MenuInflater inflater = getMenuInflater(); 490 inflater.inflate(R.menu.action_bar_menu, menu); 491 return true; 492 } 493 494 @Override 495 public boolean onPrepareOptionsMenu(Menu menu) { 496 final boolean haveProject = (mProject != null); 497 final boolean haveMediaItems = haveProject && mProject.getMediaItemCount() > 0; 498 menu.findItem(R.id.menu_item_capture_video).setVisible(haveProject); 499 menu.findItem(R.id.menu_item_capture_image).setVisible(haveProject); 500 menu.findItem(R.id.menu_item_import_video).setVisible(haveProject); 501 menu.findItem(R.id.menu_item_import_image).setVisible(haveProject); 502 menu.findItem(R.id.menu_item_import_audio).setVisible(haveProject && 503 mProject.getAudioTracks().size() == 0 && haveMediaItems); 504 menu.findItem(R.id.menu_item_change_aspect_ratio).setVisible(haveProject && 505 mProject.hasMultipleAspectRatios()); 506 menu.findItem(R.id.menu_item_edit_project_name).setVisible(haveProject); 507 508 // Check if there is an operation pending or preview is on. 509 boolean enableMenu = haveProject; 510 if (enableMenu && mPreviewThread != null) { 511 // Preview is in progress 512 enableMenu = mPreviewThread.isStopped(); 513 if (enableMenu && mProjectPath != null) { 514 enableMenu = !ApiService.isProjectBeingEdited(mProjectPath); 515 } 516 } 517 518 menu.findItem(R.id.menu_item_export_movie).setVisible(enableMenu && haveMediaItems); 519 menu.findItem(R.id.menu_item_delete_project).setVisible(enableMenu); 520 menu.findItem(R.id.menu_item_play_exported_movie).setVisible(enableMenu && 521 mProject.getExportedMovieUri() != null); 522 menu.findItem(R.id.menu_item_share_movie).setVisible(enableMenu && 523 mProject.getExportedMovieUri() != null); 524 return true; 525 } 526 527 @Override 528 public boolean onOptionsItemSelected(MenuItem item) { 529 switch (item.getItemId()) { 530 case android.R.id.home: { 531 // Returns to project picker if user clicks on the app icon in the action bar. 532 final Intent intent = new Intent(this, ProjectsActivity.class); 533 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 534 startActivity(intent); 535 finish(); 536 return true; 537 } 538 539 case R.id.menu_item_capture_video: { 540 mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId(); 541 542 // Create parameters for Intent with filename 543 final ContentValues values = new ContentValues(); 544 String videoFilename = DIRECTORY + '/' + getVideoOutputMediaFileTitle() + ".mp4"; 545 values.put(MediaStore.Video.Media.DATA, videoFilename); 546 mCaptureMediaUri = getContentResolver().insert( 547 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); 548 final Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 549 intent.putExtra(MediaStore.EXTRA_OUTPUT, mCaptureMediaUri); 550 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); 551 startActivityForResult(intent, REQUEST_CODE_CAPTURE_VIDEO); 552 return true; 553 } 554 555 case R.id.menu_item_capture_image: { 556 mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId(); 557 558 // Create parameters for Intent with filename 559 final ContentValues values = new ContentValues(); 560 mCaptureMediaUri = getContentResolver().insert( 561 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 562 final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 563 intent.putExtra(MediaStore.EXTRA_OUTPUT, mCaptureMediaUri); 564 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); 565 startActivityForResult(intent, REQUEST_CODE_CAPTURE_IMAGE); 566 return true; 567 } 568 569 case R.id.menu_item_import_video: { 570 mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId(); 571 572 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 573 intent.setType("video/*"); 574 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 575 startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO); 576 return true; 577 } 578 579 case R.id.menu_item_import_image: { 580 mInsertMediaItemAfterMediaItemId = mProject.getLastMediaItemId(); 581 582 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 583 intent.setType("image/*"); 584 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 585 startActivityForResult(intent, REQUEST_CODE_IMPORT_IMAGE); 586 return true; 587 } 588 589 case R.id.menu_item_import_audio: { 590 final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 591 intent.setType("audio/*"); 592 startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC); 593 return true; 594 } 595 596 case R.id.menu_item_change_aspect_ratio: { 597 final ArrayList<Integer> aspectRatiosList = mProject.getUniqueAspectRatiosList(); 598 final int size = aspectRatiosList.size(); 599 if (size > 1) { 600 final Bundle bundle = new Bundle(); 601 bundle.putIntegerArrayList(PARAM_ASPECT_RATIOS_LIST, aspectRatiosList); 602 603 // Get the current aspect ratio index 604 final int currentAspectRatio = mProject.getAspectRatio(); 605 int currentAspectRatioIndex = 0; 606 for (int i = 0; i < size; i++) { 607 final int aspectRatio = aspectRatiosList.get(i); 608 if (aspectRatio == currentAspectRatio) { 609 currentAspectRatioIndex = i; 610 break; 611 } 612 } 613 bundle.putInt(PARAM_CURRENT_ASPECT_RATIO_INDEX, currentAspectRatioIndex); 614 showDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID, bundle); 615 } 616 return true; 617 } 618 619 case R.id.menu_item_edit_project_name: { 620 showDialog(DIALOG_EDIT_PROJECT_NAME_ID); 621 return true; 622 } 623 624 case R.id.menu_item_delete_project: { 625 // Confirm project delete 626 showDialog(DIALOG_DELETE_PROJECT_ID); 627 return true; 628 } 629 630 case R.id.menu_item_export_movie: { 631 // Present the user with a dialog to choose export options 632 showDialog(DIALOG_EXPORT_OPTIONS_ID); 633 return true; 634 } 635 636 case R.id.menu_item_play_exported_movie: { 637 final Intent intent = new Intent(Intent.ACTION_VIEW); 638 intent.setDataAndType(mProject.getExportedMovieUri(), "video/*"); 639 intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false); 640 startActivity(intent); 641 return true; 642 } 643 644 case R.id.menu_item_share_movie: { 645 final Intent intent = new Intent(Intent.ACTION_SEND); 646 intent.putExtra(Intent.EXTRA_STREAM, mProject.getExportedMovieUri()); 647 intent.setType("video/*"); 648 startActivity(intent); 649 return true; 650 } 651 652 default: { 653 return false; 654 } 655 } 656 } 657 658 private String getVideoOutputMediaFileTitle() { 659 long dateTaken = System.currentTimeMillis(); 660 Date date = new Date(dateTaken); 661 SimpleDateFormat dateFormat = new SimpleDateFormat("'VID'_yyyyMMdd_HHmmss"); 662 663 return dateFormat.format(date); 664 } 665 666 @Override 667 public Dialog onCreateDialog(int id, final Bundle bundle) { 668 switch (id) { 669 case DIALOG_CHOOSE_ASPECT_RATIO_ID: { 670 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 671 builder.setTitle(getString(R.string.editor_change_aspect_ratio)); 672 final ArrayList<Integer> aspectRatios = 673 bundle.getIntegerArrayList(PARAM_ASPECT_RATIOS_LIST); 674 final int count = aspectRatios.size(); 675 final CharSequence[] aspectRatioStrings = new CharSequence[count]; 676 for (int i = 0; i < count; i++) { 677 int aspectRatio = aspectRatios.get(i); 678 switch (aspectRatio) { 679 case MediaProperties.ASPECT_RATIO_11_9: { 680 aspectRatioStrings[i] = getString(R.string.aspect_ratio_11_9); 681 break; 682 } 683 684 case MediaProperties.ASPECT_RATIO_16_9: { 685 aspectRatioStrings[i] = getString(R.string.aspect_ratio_16_9); 686 break; 687 } 688 689 case MediaProperties.ASPECT_RATIO_3_2: { 690 aspectRatioStrings[i] = getString(R.string.aspect_ratio_3_2); 691 break; 692 } 693 694 case MediaProperties.ASPECT_RATIO_4_3: { 695 aspectRatioStrings[i] = getString(R.string.aspect_ratio_4_3); 696 break; 697 } 698 699 case MediaProperties.ASPECT_RATIO_5_3: { 700 aspectRatioStrings[i] = getString(R.string.aspect_ratio_5_3); 701 break; 702 } 703 704 default: { 705 break; 706 } 707 } 708 } 709 710 builder.setSingleChoiceItems(aspectRatioStrings, 711 bundle.getInt(PARAM_CURRENT_ASPECT_RATIO_INDEX), 712 new DialogInterface.OnClickListener() { 713 @Override 714 public void onClick(DialogInterface dialog, int which) { 715 final int aspectRatio = aspectRatios.get(which); 716 ApiService.setAspectRatio(VideoEditorActivity.this, mProjectPath, 717 aspectRatio); 718 719 removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID); 720 } 721 }); 722 builder.setCancelable(true); 723 builder.setOnCancelListener(new DialogInterface.OnCancelListener() { 724 @Override 725 public void onCancel(DialogInterface dialog) { 726 removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID); 727 } 728 }); 729 return builder.create(); 730 } 731 732 case DIALOG_DELETE_PROJECT_ID: { 733 return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0, 734 getString(R.string.editor_delete_project_question), 735 getString(R.string.yes), 736 new DialogInterface.OnClickListener() { 737 @Override 738 public void onClick(DialogInterface dialog, int which) { 739 ApiService.deleteProject(VideoEditorActivity.this, mProjectPath); 740 mProjectPath = null; 741 mProject = null; 742 enterDisabledState(R.string.editor_no_project); 743 744 removeDialog(DIALOG_DELETE_PROJECT_ID); 745 finish(); 746 } 747 }, getString(R.string.no), new DialogInterface.OnClickListener() { 748 @Override 749 public void onClick(DialogInterface dialog, int which) { 750 removeDialog(DIALOG_DELETE_PROJECT_ID); 751 } 752 }, new DialogInterface.OnCancelListener() { 753 @Override 754 public void onCancel(DialogInterface dialog) { 755 removeDialog(DIALOG_DELETE_PROJECT_ID); 756 } 757 }, true); 758 } 759 760 case DIALOG_DELETE_BAD_PROJECT_ID: { 761 return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0, 762 getString(R.string.editor_load_error), 763 getString(R.string.yes), 764 new DialogInterface.OnClickListener() { 765 @Override 766 public void onClick(DialogInterface dialog, int which) { 767 ApiService.deleteProject(VideoEditorActivity.this, 768 bundle.getString(PARAM_PROJECT_PATH)); 769 770 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 771 finish(); 772 } 773 }, getString(R.string.no), new DialogInterface.OnClickListener() { 774 @Override 775 public void onClick(DialogInterface dialog, int which) { 776 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 777 } 778 }, new DialogInterface.OnCancelListener() { 779 @Override 780 public void onCancel(DialogInterface dialog) { 781 removeDialog(DIALOG_DELETE_BAD_PROJECT_ID); 782 } 783 }, true); 784 } 785 786 case DIALOG_EDIT_PROJECT_NAME_ID: { 787 if (mProject == null) { 788 return null; 789 } 790 791 return AlertDialogs.createEditDialog(this, 792 getString(R.string.editor_edit_project_name), 793 mProject.getName(), 794 getString(android.R.string.ok), 795 new DialogInterface.OnClickListener() { 796 @Override 797 public void onClick(DialogInterface dialog, int which) { 798 final TextView tv = 799 (TextView)((AlertDialog)dialog).findViewById(R.id.text_1); 800 mProject.setProjectName(tv.getText().toString()); 801 getActionBar().setTitle(tv.getText()); 802 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 803 } 804 }, 805 getString(android.R.string.cancel), 806 new DialogInterface.OnClickListener() { 807 @Override 808 public void onClick(DialogInterface dialog, int which) { 809 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 810 } 811 }, 812 new DialogInterface.OnCancelListener() { 813 @Override 814 public void onCancel(DialogInterface dialog) { 815 removeDialog(DIALOG_EDIT_PROJECT_NAME_ID); 816 } 817 }, 818 InputType.TYPE_NULL, 819 32, 820 null); 821 } 822 823 case DIALOG_EXPORT_OPTIONS_ID: { 824 if (mProject == null) { 825 return null; 826 } 827 828 return ExportOptionsDialog.create(this, 829 new ExportOptionsDialog.ExportOptionsListener() { 830 @Override 831 public void onExportOptions(int movieHeight, int movieBitrate) { 832 mPendingExportFilename = FileUtils.createMovieName( 833 MediaProperties.FILE_MP4); 834 ApiService.exportVideoEditor(VideoEditorActivity.this, mProjectPath, 835 mPendingExportFilename, movieHeight, movieBitrate); 836 837 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 838 839 showExportProgress(); 840 } 841 }, new DialogInterface.OnClickListener() { 842 @Override 843 public void onClick(DialogInterface dialog, int which) { 844 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 845 } 846 }, new DialogInterface.OnCancelListener() { 847 @Override 848 public void onCancel(DialogInterface dialog) { 849 removeDialog(DIALOG_EXPORT_OPTIONS_ID); 850 } 851 }, mProject.getAspectRatio()); 852 } 853 854 case DIALOG_REMOVE_MEDIA_ITEM_ID: { 855 return mMediaLayout.onCreateDialog(id, bundle); 856 } 857 858 case DIALOG_CHANGE_RENDERING_MODE_ID: { 859 return mMediaLayout.onCreateDialog(id, bundle); 860 } 861 862 case DIALOG_REMOVE_TRANSITION_ID: { 863 return mMediaLayout.onCreateDialog(id, bundle); 864 } 865 866 case DIALOG_REMOVE_OVERLAY_ID: { 867 return mOverlayLayout.onCreateDialog(id, bundle); 868 } 869 870 case DIALOG_REMOVE_EFFECT_ID: { 871 return mMediaLayout.onCreateDialog(id, bundle); 872 } 873 874 case DIALOG_REMOVE_AUDIO_TRACK_ID: { 875 return mAudioTrackLayout.onCreateDialog(id, bundle); 876 } 877 878 default: { 879 return null; 880 } 881 } 882 } 883 884 885 /** 886 * Called when user clicks on the button in the control panel. 887 * @param target one of the "play", "rewind", "next", 888 * and "prev" buttons in the control panel 889 */ 890 public void onClickHandler(View target) { 891 final long playheadPosMs = mProject.getPlayheadPos(); 892 893 switch (target.getId()) { 894 case R.id.editor_play: { 895 if (mProject != null && mPreviewThread != null) { 896 if (mPreviewThread.isPlaying()) { 897 mPreviewThread.stopPreviewPlayback(); 898 } else if (mProject.getMediaItemCount() > 0) { 899 mPreviewThread.startPreviewPlayback(mProject, playheadPosMs); 900 } 901 } 902 break; 903 } 904 905 case R.id.editor_rewind: { 906 if (mProject != null && mPreviewThread != null) { 907 if (mPreviewThread.isPlaying()) { 908 mPreviewThread.stopPreviewPlayback(); 909 movePlayhead(0); 910 mPreviewThread.startPreviewPlayback(mProject, 0); 911 } else { 912 movePlayhead(0); 913 showPreviewFrame(); 914 } 915 } 916 break; 917 } 918 919 case R.id.editor_next: { 920 if (mProject != null && mPreviewThread != null) { 921 final boolean restartPreview; 922 if (mPreviewThread.isPlaying()) { 923 mPreviewThread.stopPreviewPlayback(); 924 restartPreview = true; 925 } else { 926 restartPreview = false; 927 } 928 929 final MovieMediaItem mediaItem = mProject.getNextMediaItem(playheadPosMs); 930 if (mediaItem != null) { 931 movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId())); 932 if (restartPreview) { 933 mPreviewThread.startPreviewPlayback(mProject, 934 mProject.getPlayheadPos()); 935 } else { 936 showPreviewFrame(); 937 } 938 } else { // Move to the end of the timeline 939 movePlayhead(mProject.computeDuration()); 940 showPreviewFrame(); 941 } 942 } 943 break; 944 } 945 946 case R.id.editor_prev: { 947 if (mProject != null && mPreviewThread != null) { 948 final boolean restartPreview; 949 if (mPreviewThread.isPlaying()) { 950 mPreviewThread.stopPreviewPlayback(); 951 restartPreview = true; 952 } else { 953 restartPreview = false; 954 } 955 956 final MovieMediaItem mediaItem = mProject.getPreviousMediaItem(playheadPosMs); 957 if (mediaItem != null) { 958 movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId())); 959 } else { // Move to the beginning of the timeline 960 movePlayhead(0); 961 } 962 963 if (restartPreview) { 964 mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos()); 965 } else { 966 showPreviewFrame(); 967 } 968 } 969 break; 970 } 971 972 default: { 973 break; 974 } 975 } 976 } 977 978 @Override 979 protected void onActivityResult(int requestCode, int resultCode, Intent extras) { 980 super.onActivityResult(requestCode, resultCode, extras); 981 if (resultCode == RESULT_CANCELED) { 982 switch (requestCode) { 983 case REQUEST_CODE_CAPTURE_VIDEO: 984 case REQUEST_CODE_CAPTURE_IMAGE: { 985 if (mCaptureMediaUri != null) { 986 getContentResolver().delete(mCaptureMediaUri, null, null); 987 mCaptureMediaUri = null; 988 } 989 break; 990 } 991 992 default: { 993 break; 994 } 995 } 996 return; 997 } 998 999 switch (requestCode) { 1000 case REQUEST_CODE_CAPTURE_VIDEO: { 1001 if (mProject != null) { 1002 ApiService.addMediaItemVideoUri(this, mProjectPath, 1003 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1004 mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1005 mProject.getTheme()); 1006 mInsertMediaItemAfterMediaItemId = null; 1007 } else { 1008 // Add this video after the project loads 1009 mAddMediaItemVideoUri = mCaptureMediaUri; 1010 } 1011 mCaptureMediaUri = null; 1012 break; 1013 } 1014 1015 case REQUEST_CODE_CAPTURE_IMAGE: { 1016 if (mProject != null) { 1017 ApiService.addMediaItemImageUri(this, mProjectPath, 1018 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1019 mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1020 MediaItemUtils.getDefaultImageDuration(), 1021 mProject.getTheme()); 1022 mInsertMediaItemAfterMediaItemId = null; 1023 } else { 1024 // Add this image after the project loads 1025 mAddMediaItemImageUri = mCaptureMediaUri; 1026 } 1027 mCaptureMediaUri = null; 1028 break; 1029 } 1030 1031 case REQUEST_CODE_IMPORT_VIDEO: { 1032 final Uri mediaUri = extras.getData(); 1033 if (mProject != null) { 1034 if ("media".equals(mediaUri.getAuthority())) { 1035 ApiService.addMediaItemVideoUri(this, mProjectPath, 1036 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1037 mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1038 mProject.getTheme()); 1039 } else { 1040 // Notify the user that this item needs to be downloaded. 1041 Toast.makeText(this, getString(R.string.editor_video_load), 1042 Toast.LENGTH_LONG).show(); 1043 // When the download is complete insert it into the project. 1044 ApiService.loadMediaItem(this, mProjectPath, mediaUri, "video/*"); 1045 } 1046 mInsertMediaItemAfterMediaItemId = null; 1047 } else { 1048 // Add this video after the project loads 1049 mAddMediaItemVideoUri = mediaUri; 1050 } 1051 break; 1052 } 1053 1054 case REQUEST_CODE_IMPORT_IMAGE: { 1055 final Uri mediaUri = extras.getData(); 1056 if (mProject != null) { 1057 if ("media".equals(mediaUri.getAuthority())) { 1058 ApiService.addMediaItemImageUri(this, mProjectPath, 1059 ApiService.generateId(), mInsertMediaItemAfterMediaItemId, 1060 mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1061 MediaItemUtils.getDefaultImageDuration(), mProject.getTheme()); 1062 } else { 1063 // Notify the user that this item needs to be downloaded. 1064 Toast.makeText(this, getString(R.string.editor_image_load), 1065 Toast.LENGTH_LONG).show(); 1066 // When the download is complete insert it into the project. 1067 ApiService.loadMediaItem(this, mProjectPath, mediaUri, "image/*"); 1068 } 1069 mInsertMediaItemAfterMediaItemId = null; 1070 } else { 1071 // Add this image after the project loads 1072 mAddMediaItemImageUri = mediaUri; 1073 } 1074 break; 1075 } 1076 1077 case REQUEST_CODE_IMPORT_MUSIC: { 1078 final Uri data = extras.getData(); 1079 if (mProject != null) { 1080 ApiService.addAudioTrack(this, mProjectPath, ApiService.generateId(), data, 1081 true); 1082 } else { 1083 mAddAudioTrackUri = data; 1084 } 1085 break; 1086 } 1087 1088 case REQUEST_CODE_EDIT_TRANSITION: { 1089 final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1); 1090 final String afterMediaId = extras.getStringExtra( 1091 TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID); 1092 final String transitionId = extras.getStringExtra( 1093 TransitionsActivity.PARAM_TRANSITION_ID); 1094 final long transitionDurationMs = extras.getLongExtra( 1095 TransitionsActivity.PARAM_TRANSITION_DURATION, 500); 1096 if (mProject != null) { 1097 mMediaLayout.editTransition(afterMediaId, transitionId, type, 1098 transitionDurationMs); 1099 } else { 1100 // Add this transition after you load the project 1101 mEditTransitionAfterMediaId = afterMediaId; 1102 mEditTransitionId = transitionId; 1103 mEditTransitionType = type; 1104 mEditTransitionDurationMs = transitionDurationMs; 1105 } 1106 break; 1107 } 1108 1109 case REQUEST_CODE_PICK_TRANSITION: { 1110 final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1); 1111 final String afterMediaId = extras.getStringExtra( 1112 TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID); 1113 final long transitionDurationMs = extras.getLongExtra( 1114 TransitionsActivity.PARAM_TRANSITION_DURATION, 500); 1115 if (mProject != null) { 1116 mMediaLayout.addTransition(afterMediaId, type, transitionDurationMs); 1117 } else { 1118 // Add this transition after you load the project 1119 mAddTransitionAfterMediaId = afterMediaId; 1120 mAddTransitionType = type; 1121 mAddTransitionDurationMs = transitionDurationMs; 1122 } 1123 break; 1124 } 1125 1126 case REQUEST_CODE_PICK_OVERLAY: { 1127 // If there is no overlay id, it means we are adding a new overlay. 1128 // Otherwise we generate a unique new id for the new overlay. 1129 final String mediaItemId = 1130 extras.getStringExtra(OverlayTitleEditor.PARAM_MEDIA_ITEM_ID); 1131 final String overlayId = 1132 extras.getStringExtra(OverlayTitleEditor.PARAM_OVERLAY_ID); 1133 final Bundle bundle = 1134 extras.getBundleExtra(OverlayTitleEditor.PARAM_OVERLAY_ATTRIBUTES); 1135 if (mProject != null) { 1136 final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId); 1137 if (mediaItem != null) { 1138 if (overlayId == null) { 1139 ApiService.addOverlay(this, mProject.getPath(), mediaItemId, 1140 ApiService.generateId(), bundle, 1141 mediaItem.getAppBoundaryBeginTime(), 1142 OverlayLinearLayout.DEFAULT_TITLE_DURATION); 1143 } else { 1144 ApiService.setOverlayUserAttributes(this, mProject.getPath(), 1145 mediaItemId, overlayId, bundle); 1146 } 1147 mOverlayLayout.invalidateCAB(); 1148 } 1149 } else { 1150 // Add this overlay after you load the project. 1151 mAddOverlayMediaItemId = mediaItemId; 1152 mAddOverlayUserAttributes = bundle; 1153 mEditOverlayId = overlayId; 1154 } 1155 break; 1156 } 1157 1158 case REQUEST_CODE_KEN_BURNS: { 1159 final String mediaItemId = extras.getStringExtra( 1160 KenBurnsActivity.PARAM_MEDIA_ITEM_ID); 1161 final Rect startRect = extras.getParcelableExtra( 1162 KenBurnsActivity.PARAM_START_RECT); 1163 final Rect endRect = extras.getParcelableExtra( 1164 KenBurnsActivity.PARAM_END_RECT); 1165 if (mProject != null) { 1166 mMediaLayout.addEffect(EffectType.EFFECT_KEN_BURNS, mediaItemId, 1167 startRect, endRect); 1168 mMediaLayout.invalidateActionBar(); 1169 } else { 1170 // Add this effect after you load the project. 1171 mAddEffectMediaItemId = mediaItemId; 1172 mAddEffectType = EffectType.EFFECT_KEN_BURNS; 1173 mAddKenBurnsStartRect = startRect; 1174 mAddKenBurnsEndRect = endRect; 1175 } 1176 break; 1177 } 1178 1179 default: { 1180 break; 1181 } 1182 } 1183 } 1184 1185 @Override 1186 public void surfaceCreated(SurfaceHolder holder) { 1187 logd("surfaceCreated"); 1188 1189 mHaveSurface = true; 1190 createPreviewThreadIfNeeded(); 1191 } 1192 1193 @Override 1194 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 1195 logd("surfaceChanged: " + width + "x" + height); 1196 1197 if (mPreviewThread != null) { 1198 mPreviewThread.onSurfaceChanged(width, height); 1199 } 1200 } 1201 1202 @Override 1203 public void surfaceDestroyed(SurfaceHolder holder) { 1204 logd("surfaceDestroyed"); 1205 mHaveSurface = false; 1206 stopPreviewThread(); 1207 } 1208 1209 // Stop the preview playback if pending and quit the preview thread 1210 private void stopPreviewThread() { 1211 if (mPreviewThread != null) { 1212 mPreviewThread.stopPreviewPlayback(); 1213 mPreviewThread.quit(); 1214 mPreviewThread = null; 1215 } 1216 } 1217 1218 @Override 1219 protected void enterTransitionalState(int statusStringId) { 1220 mEditorProjectView.setVisibility(View.GONE); 1221 mEditorEmptyView.setVisibility(View.VISIBLE); 1222 1223 ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId); 1224 findViewById(R.id.empty_project_progress).setVisibility(View.VISIBLE); 1225 } 1226 1227 @Override 1228 protected void enterDisabledState(int statusStringId) { 1229 mEditorProjectView.setVisibility(View.GONE); 1230 mEditorEmptyView.setVisibility(View.VISIBLE); 1231 1232 getActionBar().setTitle(R.string.full_app_name); 1233 1234 ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId); 1235 findViewById(R.id.empty_project_progress).setVisibility(View.GONE); 1236 } 1237 1238 @Override 1239 protected void enterReadyState() { 1240 mEditorProjectView.setVisibility(View.VISIBLE); 1241 mEditorEmptyView.setVisibility(View.GONE); 1242 } 1243 1244 @Override 1245 protected boolean showPreviewFrame() { 1246 if (mPreviewThread == null) { // The surface is not ready yet. 1247 return false; 1248 } 1249 1250 // Regenerate the preview frame 1251 if (mProject != null && !mPreviewThread.isPlaying() && mPendingExportFilename == null) { 1252 // Display the preview frame 1253 mPreviewThread.previewFrame(mProject, mProject.getPlayheadPos(), 1254 mProject.getMediaItemCount() == 0); 1255 } 1256 1257 return true; 1258 } 1259 1260 @Override 1261 protected void updateTimelineDuration() { 1262 if (mProject == null) { 1263 return; 1264 } 1265 1266 final long durationMs = mProject.computeDuration(); 1267 1268 // Resize the timeline according to the new timeline duration 1269 final int zoomWidth = mActivityWidth + timeToDimension(durationMs); 1270 final int childrenCount = mTimelineLayout.getChildCount(); 1271 for (int i = 0; i < childrenCount; i++) { 1272 final View child = mTimelineLayout.getChildAt(i); 1273 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 1274 lp.width = zoomWidth; 1275 child.setLayoutParams(lp); 1276 } 1277 1278 mTimelineLayout.requestLayout(mLayoutCallback); 1279 1280 // Since the duration has changed make sure that the playhead 1281 // position is valid. 1282 if (mProject.getPlayheadPos() > durationMs) { 1283 movePlayhead(durationMs); 1284 } 1285 1286 mAudioTrackLayout.updateTimelineDuration(); 1287 } 1288 1289 /** 1290 * Convert the time to dimension 1291 * At zoom level 1: one activity width = 1200 seconds 1292 * At zoom level 2: one activity width = 600 seconds 1293 * ... 1294 * At zoom level 100: one activity width = 12 seconds 1295 * 1296 * At zoom level 1000: one activity width = 1.2 seconds 1297 * 1298 * @param durationMs The time 1299 * 1300 * @return The dimension 1301 */ 1302 private int timeToDimension(long durationMs) { 1303 return (int)((mProject.getZoomLevel() * mActivityWidth * durationMs) / 1200000); 1304 } 1305 1306 /** 1307 * Zoom the timeline 1308 * 1309 * @param level The zoom level 1310 * @param updateControl true to set the control position to match the 1311 * zoom level 1312 */ 1313 private int zoomTimeline(int level, boolean updateControl) { 1314 if (level < 1 || level > MAX_ZOOM_LEVEL) { 1315 return mProject.getZoomLevel(); 1316 } 1317 1318 mProject.setZoomLevel(level); 1319 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1320 Log.v(TAG, "zoomTimeline level: " + level + " -> " + timeToDimension(1000) + " pix/s"); 1321 } 1322 1323 updateTimelineDuration(); 1324 1325 if (updateControl) { 1326 mZoomControl.setProgress(level); 1327 } 1328 return level; 1329 } 1330 1331 @Override 1332 protected void movePlayhead(long timeMs) { 1333 if (mProject == null) { 1334 return; 1335 } 1336 1337 if (setPlayhead(timeMs)) { 1338 // Scroll the timeline such that the specified position 1339 // is in the center of the screen 1340 mTimelineScroller.appScrollTo(timeToDimension(timeMs), true); 1341 } 1342 } 1343 1344 /** 1345 * Set the playhead at the specified time position 1346 * 1347 * @param timeMs The time position 1348 * 1349 * @return true if the playhead was set at the specified time position 1350 */ 1351 private boolean setPlayhead(long timeMs) { 1352 // Check if the position would change 1353 if (mCurrentPlayheadPosMs == timeMs) { 1354 return false; 1355 } 1356 1357 // Check if the time is valid. Note that invalid values are common due 1358 // to overscrolling the timeline 1359 if (timeMs < 0) { 1360 return false; 1361 } else if (timeMs > mProject.computeDuration()) { 1362 return false; 1363 } 1364 1365 mCurrentPlayheadPosMs = timeMs; 1366 1367 mTimeView.setText(StringUtils.getTimestampAsString(this, timeMs)); 1368 mProject.setPlayheadPos(timeMs); 1369 return true; 1370 } 1371 1372 @Override 1373 protected void setAspectRatio(final int aspectRatio) { 1374 final FrameLayout.LayoutParams lp = 1375 (FrameLayout.LayoutParams)mSurfaceView.getLayoutParams(); 1376 1377 switch (aspectRatio) { 1378 case MediaProperties.ASPECT_RATIO_5_3: { 1379 lp.width = (lp.height * 5) / 3; 1380 break; 1381 } 1382 1383 case MediaProperties.ASPECT_RATIO_4_3: { 1384 lp.width = (lp.height * 4) / 3; 1385 break; 1386 } 1387 1388 case MediaProperties.ASPECT_RATIO_3_2: { 1389 lp.width = (lp.height * 3) / 2; 1390 break; 1391 } 1392 1393 case MediaProperties.ASPECT_RATIO_11_9: { 1394 lp.width = (lp.height * 11) / 9; 1395 break; 1396 } 1397 1398 case MediaProperties.ASPECT_RATIO_16_9: { 1399 lp.width = (lp.height * 16) / 9; 1400 break; 1401 } 1402 1403 default: { 1404 break; 1405 } 1406 } 1407 1408 logd("setAspectRatio: " + aspectRatio + ", size: " + lp.width + "x" + lp.height); 1409 mSurfaceView.setLayoutParams(lp); 1410 mOverlayView.setLayoutParams(lp); 1411 } 1412 1413 @Override 1414 protected MediaLinearLayout getMediaLayout() { 1415 return mMediaLayout; 1416 } 1417 1418 @Override 1419 protected OverlayLinearLayout getOverlayLayout() { 1420 return mOverlayLayout; 1421 } 1422 1423 @Override 1424 protected AudioTrackLinearLayout getAudioTrackLayout() { 1425 return mAudioTrackLayout; 1426 } 1427 1428 @Override 1429 protected void onExportProgress(int progress) { 1430 if (mExportProgressDialog != null) { 1431 mExportProgressDialog.setProgress(progress); 1432 } 1433 } 1434 1435 @Override 1436 protected void onExportComplete() { 1437 if (mExportProgressDialog != null) { 1438 mExportProgressDialog.dismiss(); 1439 mExportProgressDialog = null; 1440 } 1441 } 1442 1443 @Override 1444 protected void onProjectEditStateChange(boolean projectEdited) { 1445 logd("onProjectEditStateChange: " + projectEdited); 1446 1447 mPreviewPlayButton.setAlpha(projectEdited ? 100 : 255); 1448 mPreviewPlayButton.setEnabled(!projectEdited); 1449 mPreviewRewindButton.setEnabled(!projectEdited); 1450 mPreviewNextButton.setEnabled(!projectEdited); 1451 mPreviewPrevButton.setEnabled(!projectEdited); 1452 1453 mMediaLayout.invalidateActionBar(); 1454 mOverlayLayout.invalidateCAB(); 1455 invalidateOptionsMenu(); 1456 } 1457 1458 @Override 1459 protected void initializeFromProject(boolean updateUI) { 1460 logd("Project was clean: " + mProject.isClean()); 1461 1462 if (updateUI || !mProject.isClean()) { 1463 getActionBar().setTitle(mProject.getName()); 1464 1465 // Clear the media related to the previous project and 1466 // add the media for the current project. 1467 mMediaLayout.setParentTimelineScrollView(mTimelineScroller); 1468 mMediaLayout.setProject(mProject); 1469 mOverlayLayout.setProject(mProject); 1470 mAudioTrackLayout.setProject(mProject); 1471 mPlayheadView.setProject(mProject); 1472 1473 // Add the media items to the media item layout 1474 mMediaLayout.addMediaItems(mProject.getMediaItems()); 1475 mMediaLayout.setSelectedView(mMediaLayoutSelectedPos); 1476 1477 // Add the media items to the overlay layout 1478 mOverlayLayout.addMediaItems(mProject.getMediaItems()); 1479 1480 // Add the audio tracks to the audio tracks layout 1481 mAudioTrackLayout.addAudioTracks(mProject.getAudioTracks()); 1482 1483 setAspectRatio(mProject.getAspectRatio()); 1484 } 1485 1486 updateTimelineDuration(); 1487 zoomTimeline(mProject.getZoomLevel(), true); 1488 1489 // Set the playhead position. We need to wait for the layout to 1490 // complete before we can scroll to the playhead position. 1491 final Handler handler = new Handler(); 1492 handler.post(new Runnable() { 1493 private final long DELAY = 100; 1494 private final int ATTEMPTS = 20; 1495 private int mAttempts = ATTEMPTS; 1496 1497 @Override 1498 public void run() { 1499 if (mAttempts == ATTEMPTS) { // Only scroll once 1500 movePlayhead(mProject.getPlayheadPos()); 1501 } 1502 1503 // If the surface is not yet created (showPreviewFrame() 1504 // returns false) wait for a while (DELAY * ATTEMPTS). 1505 if (showPreviewFrame() == false && mAttempts >= 0) { 1506 mAttempts--; 1507 if (mAttempts >= 0) { 1508 handler.postDelayed(this, DELAY); 1509 } 1510 } 1511 } 1512 }); 1513 1514 if (mAddMediaItemVideoUri != null) { 1515 ApiService.addMediaItemVideoUri(this, mProjectPath, ApiService.generateId(), 1516 mInsertMediaItemAfterMediaItemId, 1517 mAddMediaItemVideoUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1518 mProject.getTheme()); 1519 mAddMediaItemVideoUri = null; 1520 mInsertMediaItemAfterMediaItemId = null; 1521 } 1522 1523 if (mAddMediaItemImageUri != null) { 1524 ApiService.addMediaItemImageUri(this, mProjectPath, ApiService.generateId(), 1525 mInsertMediaItemAfterMediaItemId, 1526 mAddMediaItemImageUri, MediaItem.RENDERING_MODE_BLACK_BORDER, 1527 MediaItemUtils.getDefaultImageDuration(), mProject.getTheme()); 1528 mAddMediaItemImageUri = null; 1529 mInsertMediaItemAfterMediaItemId = null; 1530 } 1531 1532 if (mAddAudioTrackUri != null) { 1533 ApiService.addAudioTrack(this, mProject.getPath(), ApiService.generateId(), 1534 mAddAudioTrackUri, true); 1535 mAddAudioTrackUri = null; 1536 } 1537 1538 if (mAddTransitionAfterMediaId != null) { 1539 mMediaLayout.addTransition(mAddTransitionAfterMediaId, mAddTransitionType, 1540 mAddTransitionDurationMs); 1541 mAddTransitionAfterMediaId = null; 1542 } 1543 1544 if (mEditTransitionId != null) { 1545 mMediaLayout.editTransition(mEditTransitionAfterMediaId, mEditTransitionId, 1546 mEditTransitionType, mEditTransitionDurationMs); 1547 mEditTransitionId = null; 1548 mEditTransitionAfterMediaId = null; 1549 } 1550 1551 if (mAddOverlayMediaItemId != null) { 1552 ApiService.addOverlay(this, mProject.getPath(), mAddOverlayMediaItemId, 1553 ApiService.generateId(), mAddOverlayUserAttributes, 0, 1554 OverlayLinearLayout.DEFAULT_TITLE_DURATION); 1555 mAddOverlayMediaItemId = null; 1556 mAddOverlayUserAttributes = null; 1557 } 1558 1559 if (mEditOverlayMediaItemId != null) { 1560 ApiService.setOverlayUserAttributes(this, mProject.getPath(), mEditOverlayMediaItemId, 1561 mEditOverlayId, mEditOverlayUserAttributes); 1562 mEditOverlayMediaItemId = null; 1563 mEditOverlayId = null; 1564 mEditOverlayUserAttributes = null; 1565 } 1566 1567 if (mAddEffectMediaItemId != null) { 1568 mMediaLayout.addEffect(mAddEffectType, mAddEffectMediaItemId, 1569 mAddKenBurnsStartRect, mAddKenBurnsEndRect); 1570 mAddEffectMediaItemId = null; 1571 } 1572 1573 enterReadyState(); 1574 1575 if (mPendingExportFilename != null) { 1576 if (ApiService.isVideoEditorExportPending(mProjectPath, mPendingExportFilename)) { 1577 // The export is still pending 1578 // Display the export project dialog 1579 showExportProgress(); 1580 } else { 1581 // The export completed while the Activity was paused 1582 mPendingExportFilename = null; 1583 } 1584 } 1585 1586 invalidateOptionsMenu(); 1587 1588 restartPreview(); 1589 } 1590 1591 /** 1592 * Restarts preview. 1593 */ 1594 private void restartPreview() { 1595 if (mRestartPreview == false) { 1596 return; 1597 } 1598 1599 if (mProject == null) { 1600 return; 1601 } 1602 1603 if (mPreviewThread != null) { 1604 mRestartPreview = false; 1605 mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos()); 1606 } 1607 } 1608 1609 /** 1610 * Shows progress dialog during export operation. 1611 */ 1612 private void showExportProgress() { 1613 // Keep the CPU on throughout the export operation. 1614 mExportProgressDialog = new ProgressDialog(this) { 1615 @Override 1616 public void onStart() { 1617 super.onStart(); 1618 mCpuWakeLock.acquire(); 1619 } 1620 @Override 1621 public void onStop() { 1622 super.onStop(); 1623 mCpuWakeLock.release(); 1624 } 1625 }; 1626 mExportProgressDialog.setTitle(getString(R.string.export_dialog_export)); 1627 mExportProgressDialog.setMessage(null); 1628 mExportProgressDialog.setIndeterminate(false); 1629 // Allow cancellation with BACK button. 1630 mExportProgressDialog.setCancelable(true); 1631 mExportProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 1632 @Override 1633 public void onCancel(DialogInterface dialog) { 1634 cancelExport(); 1635 } 1636 }); 1637 mExportProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 1638 mExportProgressDialog.setMax(100); 1639 mExportProgressDialog.setCanceledOnTouchOutside(false); 1640 mExportProgressDialog.setButton(getString(android.R.string.cancel), 1641 new DialogInterface.OnClickListener() { 1642 @Override 1643 public void onClick(DialogInterface dialog, int which) { 1644 cancelExport(); 1645 } 1646 } 1647 ); 1648 mExportProgressDialog.setCanceledOnTouchOutside(false); 1649 mExportProgressDialog.show(); 1650 mExportProgressDialog.setProgressNumberFormat(""); 1651 } 1652 1653 private void cancelExport() { 1654 ApiService.cancelExportVideoEditor(VideoEditorActivity.this, mProjectPath, 1655 mPendingExportFilename); 1656 mPendingExportFilename = null; 1657 mExportProgressDialog = null; 1658 } 1659 1660 private boolean isPreviewPlaying() { 1661 if (mPreviewThread == null) 1662 return false; 1663 1664 return mPreviewThread.isPlaying(); 1665 } 1666 1667 /** 1668 * The preview thread 1669 */ 1670 private class PreviewThread extends Thread { 1671 // Preview states 1672 private final int PREVIEW_STATE_STOPPED = 0; 1673 private final int PREVIEW_STATE_STARTING = 1; 1674 private final int PREVIEW_STATE_STARTED = 2; 1675 private final int PREVIEW_STATE_STOPPING = 3; 1676 1677 private final int OVERLAY_DATA_COUNT = 16; 1678 1679 private final Handler mMainHandler; 1680 private final Queue<Runnable> mQueue; 1681 private final SurfaceHolder mSurfaceHolder; 1682 private final Queue<VideoEditor.OverlayData> mOverlayDataQueue; 1683 private Handler mThreadHandler; 1684 private int mPreviewState; 1685 private Bitmap mOverlayBitmap; 1686 1687 private final Runnable mProcessQueueRunnable = new Runnable() { 1688 @Override 1689 public void run() { 1690 // Process whatever accumulated in the queue 1691 Runnable runnable; 1692 while ((runnable = mQueue.poll()) != null) { 1693 runnable.run(); 1694 } 1695 } 1696 }; 1697 1698 /** 1699 * Constructor 1700 * 1701 * @param surfaceHolder The surface holder 1702 */ 1703 public PreviewThread(SurfaceHolder surfaceHolder) { 1704 mMainHandler = new Handler(Looper.getMainLooper()); 1705 mQueue = new LinkedBlockingQueue<Runnable>(); 1706 mSurfaceHolder = surfaceHolder; 1707 mPreviewState = PREVIEW_STATE_STOPPED; 1708 1709 mOverlayDataQueue = new LinkedBlockingQueue<VideoEditor.OverlayData>(); 1710 for (int i = 0; i < OVERLAY_DATA_COUNT; i++) { 1711 mOverlayDataQueue.add(new VideoEditor.OverlayData()); 1712 } 1713 1714 start(); 1715 } 1716 1717 /** 1718 * Preview the specified frame 1719 * 1720 * @param project The video editor project 1721 * @param timeMs The frame time 1722 * @param clear true to clear the output 1723 */ 1724 public void previewFrame(final VideoEditorProject project, final long timeMs, 1725 final boolean clear) { 1726 if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) { 1727 stopPreviewPlayback(); 1728 } 1729 1730 logd("Preview frame at: " + timeMs + " " + clear); 1731 1732 // We only need to see the last frame 1733 mQueue.clear(); 1734 1735 mQueue.add(new Runnable() { 1736 @Override 1737 public void run() { 1738 if (clear) { 1739 try { 1740 project.clearSurface(mSurfaceHolder); 1741 } catch (Exception ex) { 1742 Log.w(TAG, "Surface cannot be cleared"); 1743 } 1744 1745 mMainHandler.post(new Runnable() { 1746 @Override 1747 public void run() { 1748 if (mOverlayBitmap != null) { 1749 mOverlayBitmap.eraseColor(Color.TRANSPARENT); 1750 mOverlayView.invalidate(); 1751 } 1752 } 1753 }); 1754 } else { 1755 final VideoEditor.OverlayData overlayData; 1756 try { 1757 overlayData = mOverlayDataQueue.remove(); 1758 } catch (NoSuchElementException ex) { 1759 Log.e(TAG, "Out of OverlayData elements"); 1760 return; 1761 } 1762 1763 try { 1764 if (project.renderPreviewFrame(mSurfaceHolder, timeMs, overlayData) 1765 < 0) { 1766 logd("Cannot render preview frame at: " + timeMs + 1767 " of " + mProject.computeDuration()); 1768 1769 mOverlayDataQueue.add(overlayData); 1770 } else { 1771 if (overlayData.needsRendering()) { 1772 mMainHandler.post(new Runnable() { 1773 /* 1774 * {@inheritDoc} 1775 */ 1776 @Override 1777 public void run() { 1778 if (mOverlayBitmap != null) { 1779 overlayData.renderOverlay(mOverlayBitmap); 1780 mOverlayView.invalidate(); 1781 } else { 1782 overlayData.release(); 1783 } 1784 1785 mOverlayDataQueue.add(overlayData); 1786 } 1787 }); 1788 } else { 1789 mOverlayDataQueue.add(overlayData); 1790 } 1791 } 1792 } catch (Exception ex) { 1793 logd("renderPreviewFrame failed at timeMs: " + timeMs + "\n" + ex); 1794 mOverlayDataQueue.add(overlayData); 1795 } 1796 } 1797 } 1798 }); 1799 1800 if (mThreadHandler != null) { 1801 mThreadHandler.post(mProcessQueueRunnable); 1802 } 1803 } 1804 1805 /** 1806 * Display the frame at the specified time position 1807 * 1808 * @param mediaItem The media item 1809 * @param timeMs The frame time 1810 */ 1811 public void renderMediaItemFrame(final MovieMediaItem mediaItem, final long timeMs) { 1812 if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) { 1813 stopPreviewPlayback(); 1814 } 1815 1816 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1817 Log.v(TAG, "Render media item frame at: " + timeMs); 1818 } 1819 1820 // We only need to see the last frame 1821 mQueue.clear(); 1822 1823 mQueue.add(new Runnable() { 1824 @Override 1825 public void run() { 1826 try { 1827 if (mProject.renderMediaItemFrame(mSurfaceHolder, mediaItem.getId(), 1828 timeMs) < 0) { 1829 logd("Cannot render media item frame at: " + timeMs + 1830 " of " + mediaItem.getDuration()); 1831 } 1832 } catch (Exception ex) { 1833 logd("Cannot render preview frame at: " + timeMs + "\n" + ex); 1834 } 1835 } 1836 }); 1837 1838 if (mThreadHandler != null) { 1839 mThreadHandler.post(mProcessQueueRunnable); 1840 } 1841 } 1842 1843 /** 1844 * Starts the preview playback. 1845 * 1846 * @param project The video editor project 1847 * @param fromMs Start playing from the specified position 1848 */ 1849 private void startPreviewPlayback(final VideoEditorProject project, final long fromMs) { 1850 if (mPreviewState != PREVIEW_STATE_STOPPED) { 1851 logd("Preview did not start: " + mPreviewState); 1852 return; 1853 } 1854 1855 previewStarted(project); 1856 logd("Start preview at: " + fromMs); 1857 1858 // Clear any pending preview frames 1859 mQueue.clear(); 1860 mQueue.add(new Runnable() { 1861 @Override 1862 public void run() { 1863 try { 1864 project.startPreview(mSurfaceHolder, fromMs, -1, false, 3, 1865 new VideoEditor.PreviewProgressListener() { 1866 @Override 1867 public void onStart(VideoEditor videoEditor) { 1868 } 1869 1870 @Override 1871 public void onProgress(VideoEditor videoEditor, final long timeMs, 1872 final VideoEditor.OverlayData overlayData) { 1873 mMainHandler.post(new Runnable() { 1874 @Override 1875 public void run() { 1876 if (overlayData != null && overlayData.needsRendering()) { 1877 if (mOverlayBitmap != null) { 1878 overlayData.renderOverlay(mOverlayBitmap); 1879 mOverlayView.invalidate(); 1880 } else { 1881 overlayData.release(); 1882 } 1883 } 1884 1885 if (mPreviewState == PREVIEW_STATE_STARTED || 1886 mPreviewState == PREVIEW_STATE_STOPPING) { 1887 movePlayhead(timeMs); 1888 } 1889 } 1890 }); 1891 } 1892 1893 @Override 1894 public void onStop(VideoEditor videoEditor) { 1895 mMainHandler.post(new Runnable() { 1896 @Override 1897 public void run() { 1898 if (mPreviewState == PREVIEW_STATE_STARTED || 1899 mPreviewState == PREVIEW_STATE_STOPPING) { 1900 previewStopped(false); 1901 } 1902 } 1903 }); 1904 } 1905 }); 1906 1907 mMainHandler.post(new Runnable() { 1908 @Override 1909 public void run() { 1910 mPreviewState = PREVIEW_STATE_STARTED; 1911 } 1912 }); 1913 } catch (Exception ex) { 1914 // This exception may occur when trying to play frames 1915 // at the end of the timeline 1916 // (e.g. when fromMs == clip duration) 1917 Log.w(TAG, "Cannot start preview at: " + fromMs + "\n" + ex); 1918 1919 mMainHandler.post(new Runnable() { 1920 @Override 1921 public void run() { 1922 mPreviewState = PREVIEW_STATE_STARTED; 1923 previewStopped(true); 1924 } 1925 }); 1926 } 1927 } 1928 }); 1929 1930 if (mThreadHandler != null) { 1931 mThreadHandler.post(mProcessQueueRunnable); 1932 } 1933 } 1934 1935 /** 1936 * The preview started. 1937 * This method is always invoked from the UI thread. 1938 * 1939 * @param project The project 1940 */ 1941 private void previewStarted(VideoEditorProject project) { 1942 // Change the button image back to a pause icon 1943 mPreviewPlayButton.setImageResource(R.drawable.btn_playback_ic_pause); 1944 1945 mTimelineScroller.enableUserScrolling(false); 1946 mMediaLayout.setPlaybackInProgress(true); 1947 mOverlayLayout.setPlaybackInProgress(true); 1948 mAudioTrackLayout.setPlaybackInProgress(true); 1949 1950 mPreviewState = PREVIEW_STATE_STARTING; 1951 1952 // Keep the screen on during the preview. 1953 VideoEditorActivity.this.getWindow().addFlags( 1954 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1955 } 1956 1957 /** 1958 * Stops the preview. 1959 */ 1960 private void stopPreviewPlayback() { 1961 switch (mPreviewState) { 1962 case PREVIEW_STATE_STOPPED: { 1963 logd("stopPreviewPlayback: State was PREVIEW_STATE_STOPPED"); 1964 return; 1965 } 1966 1967 case PREVIEW_STATE_STOPPING: { 1968 logd("stopPreviewPlayback: State was PREVIEW_STATE_STOPPING"); 1969 return; 1970 } 1971 1972 case PREVIEW_STATE_STARTING: { 1973 logd("stopPreviewPlayback: State was PREVIEW_STATE_STARTING " + 1974 "now PREVIEW_STATE_STOPPING"); 1975 mPreviewState = PREVIEW_STATE_STOPPING; 1976 1977 // We need to wait until the preview starts 1978 mMainHandler.postDelayed(new Runnable() { 1979 @Override 1980 public void run() { 1981 if (mPreviewState == PREVIEW_STATE_STARTED) { 1982 logd("stopPreviewPlayback: Now PREVIEW_STATE_STARTED"); 1983 previewStopped(false); 1984 } else if (mPreviewState == PREVIEW_STATE_STOPPING) { 1985 // Keep waiting 1986 mMainHandler.postDelayed(this, 100); 1987 logd("stopPreviewPlayback: Waiting for PREVIEW_STATE_STARTED"); 1988 } else { 1989 logd("stopPreviewPlayback: PREVIEW_STATE_STOPPED while waiting"); 1990 } 1991 } 1992 }, 50); 1993 break; 1994 } 1995 1996 case PREVIEW_STATE_STARTED: { 1997 logd("stopPreviewPlayback: State was PREVIEW_STATE_STARTED"); 1998 1999 // We need to stop 2000 previewStopped(false); 2001 return; 2002 } 2003 2004 default: { 2005 throw new IllegalArgumentException("stopPreviewPlayback state: " + 2006 mPreviewState); 2007 } 2008 } 2009 } 2010 2011 /** 2012 * The surface size has changed 2013 * 2014 * @param width The new surface width 2015 * @param height The new surface height 2016 */ 2017 private void onSurfaceChanged(int width, int height) { 2018 if (mOverlayBitmap != null) { 2019 if (mOverlayBitmap.getWidth() == width && mOverlayBitmap.getHeight() == height) { 2020 // The size has not changed 2021 return; 2022 } 2023 2024 mOverlayView.setImageBitmap(null); 2025 mOverlayBitmap.recycle(); 2026 mOverlayBitmap = null; 2027 } 2028 2029 // Create the overlay bitmap 2030 logd("Overlay size: " + width + " x " + height); 2031 2032 mOverlayBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); 2033 mOverlayView.setImageBitmap(mOverlayBitmap); 2034 } 2035 2036 /** 2037 * Preview stopped. This method is always invoked from the UI thread. 2038 * 2039 * @param error true if the preview stopped due to an error 2040 */ 2041 private void previewStopped(boolean error) { 2042 if (mProject == null) { 2043 Log.w(TAG, "previewStopped: project was deleted."); 2044 return; 2045 } 2046 2047 if (mPreviewState != PREVIEW_STATE_STARTED) { 2048 throw new IllegalStateException("previewStopped in state: " + mPreviewState); 2049 } 2050 2051 // Change the button image back to a play icon 2052 mPreviewPlayButton.setImageResource(R.drawable.btn_playback_ic_play); 2053 2054 if (error == false) { 2055 // Set the playhead position at the position where the playback stopped 2056 final long stopTimeMs = mProject.stopPreview(); 2057 movePlayhead(stopTimeMs); 2058 logd("PREVIEW_STATE_STOPPED: " + stopTimeMs); 2059 } else { 2060 logd("PREVIEW_STATE_STOPPED due to error"); 2061 } 2062 2063 mPreviewState = PREVIEW_STATE_STOPPED; 2064 2065 // The playback has stopped 2066 mTimelineScroller.enableUserScrolling(true); 2067 mMediaLayout.setPlaybackInProgress(false); 2068 mAudioTrackLayout.setPlaybackInProgress(false); 2069 mOverlayLayout.setPlaybackInProgress(false); 2070 2071 // Do not keep the screen on if there is no preview in progress. 2072 VideoEditorActivity.this.getWindow().clearFlags( 2073 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 2074 } 2075 2076 /** 2077 * @return true if preview playback is in progress 2078 */ 2079 private boolean isPlaying() { 2080 return mPreviewState == PREVIEW_STATE_STARTING || 2081 mPreviewState == PREVIEW_STATE_STARTED; 2082 } 2083 2084 /** 2085 * @return true if the preview is stopped 2086 */ 2087 private boolean isStopped() { 2088 return mPreviewState == PREVIEW_STATE_STOPPED; 2089 } 2090 2091 @Override 2092 public void run() { 2093 setPriority(MAX_PRIORITY); 2094 Looper.prepare(); 2095 mThreadHandler = new Handler(); 2096 2097 // Ensure that the queued items are processed 2098 mThreadHandler.post(mProcessQueueRunnable); 2099 2100 // Run the loop 2101 Looper.loop(); 2102 } 2103 2104 /** 2105 * Quits the thread 2106 */ 2107 public void quit() { 2108 // Release the overlay bitmap 2109 if (mOverlayBitmap != null) { 2110 mOverlayView.setImageBitmap(null); 2111 mOverlayBitmap.recycle(); 2112 mOverlayBitmap = null; 2113 } 2114 2115 if (mThreadHandler != null) { 2116 mThreadHandler.getLooper().quit(); 2117 try { 2118 // Wait for the thread to quit. An ANR waiting to happen. 2119 mThreadHandler.getLooper().getThread().join(); 2120 } catch (InterruptedException ex) { 2121 } 2122 } 2123 2124 mQueue.clear(); 2125 } 2126 } 2127 2128 private static void logd(String message) { 2129 if (Log.isLoggable(TAG, Log.DEBUG)) { 2130 Log.d(TAG, message); 2131 } 2132 } 2133} 2134