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