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