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