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