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