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