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