VideoEditorActivity.java revision a34fab86f98fe477173f1d64b54de760476ec662
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                intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
664                startActivityForResult(intent, REQUEST_CODE_IMPORT_VIDEO);
665                return true;
666            }
667
668            case MENU_IMPORT_IMAGE_ID: {
669                final MovieMediaItem mediaItem = mProject.getInsertAfterMediaItem(
670                        mProject.getPlayheadPos());
671                if (mediaItem != null) {
672                    mInsertMediaItemAfterMediaItemId = mediaItem.getId();
673                } else {
674                    mInsertMediaItemAfterMediaItemId = null;
675                }
676
677                final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
678                intent.setType("image/*");
679                intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
680                startActivityForResult(intent, REQUEST_CODE_IMPORT_IMAGE);
681                return true;
682            }
683
684            case MENU_IMPORT_AUDIO_ID: {
685                final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
686                intent.setType("audio/*");
687                startActivityForResult(intent, REQUEST_CODE_IMPORT_MUSIC);
688                return true;
689            }
690
691            case MENU_CHANGE_ASPECT_RATIO_ID: {
692                final ArrayList<Integer> aspectRatiosList = mProject.getUniqueAspectRatiosList();
693                final int size = aspectRatiosList.size();
694                if (size > 1) {
695                    final Bundle bundle = new Bundle();
696                    bundle.putIntegerArrayList(PARAM_ASPECT_RATIOS_LIST, aspectRatiosList);
697
698                    // Get the current aspect ratio index
699                    final int currentAspectRatio = mProject.getAspectRatio();
700                    int currentAspectRatioIndex = 0;
701                    for (int i = 0; i < size; i++) {
702                        final int aspectRatio = aspectRatiosList.get(i);
703                        if (aspectRatio == currentAspectRatio) {
704                            currentAspectRatioIndex = i;
705                            break;
706                        }
707                    }
708                    bundle.putInt(PARAM_CURRENT_ASPECT_RATIO_INDEX, currentAspectRatioIndex);
709                    showDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID, bundle);
710                }
711                return true;
712            }
713
714            case MENU_EDIT_PROJECT_NAME_ID: {
715                showDialog(DIALOG_EDIT_PROJECT_NAME_ID);
716                return true;
717            }
718
719            case MENU_DELETE_PROJECT_ID: {
720                // Confirm project delete
721                showDialog(DIALOG_DELETE_PROJECT_ID);
722                return true;
723            }
724
725            case MENU_EXPORT_MOVIE_ID: {
726                // Present the user with a dialog to choose export options
727                showDialog(DIALOG_EXPORT_OPTIONS_ID);
728                return true;
729            }
730
731            case MENU_PLAY_EXPORTED_MOVIE: {
732                final Intent intent = new Intent(Intent.ACTION_VIEW);
733                intent.setDataAndType(mProject.getExportedMovieUri(), "video/*");
734                intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
735                startActivity(intent);
736                return true;
737            }
738
739            case MENU_SHARE_VIDEO: {
740                final Intent intent = new Intent(Intent.ACTION_SEND);
741                intent.putExtra(Intent.EXTRA_STREAM, mProject.getExportedMovieUri());
742                intent.setType("video/*");
743                startActivity(intent);
744                return true;
745            }
746
747            default: {
748                return false;
749            }
750        }
751    }
752
753    /*
754     * {@inheritDoc}
755     */
756    @Override
757    public Dialog onCreateDialog(int id, final Bundle bundle) {
758        switch (id) {
759            case DIALOG_CHOOSE_ASPECT_RATIO_ID: {
760                final AlertDialog.Builder builder = new AlertDialog.Builder(this);
761                builder.setTitle(getString(R.string.editor_change_aspect_ratio));
762                final ArrayList<Integer> aspectRatios =
763                    bundle.getIntegerArrayList(PARAM_ASPECT_RATIOS_LIST);
764                final int count = aspectRatios.size();
765                final CharSequence[] aspectRatioStrings = new CharSequence[count];
766                for (int i = 0; i < count; i++) {
767                    int aspectRatio = aspectRatios.get(i);
768                    switch (aspectRatio) {
769                        case MediaProperties.ASPECT_RATIO_11_9: {
770                            aspectRatioStrings[i] = getString(R.string.aspect_ratio_11_9);
771                            break;
772                        }
773
774                        case MediaProperties.ASPECT_RATIO_16_9: {
775                            aspectRatioStrings[i] = getString(R.string.aspect_ratio_16_9);
776                            break;
777                        }
778
779                        case MediaProperties.ASPECT_RATIO_3_2: {
780                            aspectRatioStrings[i] = getString(R.string.aspect_ratio_3_2);
781                            break;
782                        }
783
784                        case MediaProperties.ASPECT_RATIO_4_3: {
785                            aspectRatioStrings[i] = getString(R.string.aspect_ratio_4_3);
786                            break;
787                        }
788
789                        case MediaProperties.ASPECT_RATIO_5_3: {
790                            aspectRatioStrings[i] = getString(R.string.aspect_ratio_5_3);
791                            break;
792                        }
793
794                        default: {
795                            break;
796                        }
797                    }
798                }
799
800                builder.setSingleChoiceItems(aspectRatioStrings,
801                        bundle.getInt(PARAM_CURRENT_ASPECT_RATIO_INDEX),
802                        new DialogInterface.OnClickListener() {
803                    /*
804                     * {@inheritDoc}
805                     */
806                    public void onClick(DialogInterface dialog, int which) {
807                        final int aspectRatio = aspectRatios.get(which);
808                        ApiService.setAspectRatio(VideoEditorActivity.this, mProjectPath,
809                                aspectRatio);
810
811                        removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID);
812                    }
813                });
814                builder.setCancelable(true);
815                builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
816                    /*
817                     * {@inheritDoc}
818                     */
819                    public void onCancel(DialogInterface dialog) {
820                        removeDialog(DIALOG_CHOOSE_ASPECT_RATIO_ID);
821                    }
822                });
823                return builder.create();
824            }
825
826            case DIALOG_DELETE_PROJECT_ID: {
827                return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0,
828                                getString(R.string.editor_delete_project_question),
829                                    getString(R.string.yes),
830                        new DialogInterface.OnClickListener() {
831                    /*
832                     * {@inheritDoc}
833                     */
834                    public void onClick(DialogInterface dialog, int which) {
835                        ApiService.deleteProject(VideoEditorActivity.this, mProjectPath);
836                        mProjectPath = null;
837                        mProject = null;
838                        enterDisabledState(R.string.editor_no_project);
839
840                        removeDialog(DIALOG_DELETE_PROJECT_ID);
841                        finish();
842                    }
843                }, getString(R.string.no), new DialogInterface.OnClickListener() {
844                    /*
845                     * {@inheritDoc}
846                     */
847                    public void onClick(DialogInterface dialog, int which) {
848                        removeDialog(DIALOG_DELETE_PROJECT_ID);
849                    }
850                }, new DialogInterface.OnCancelListener() {
851                    /*
852                     * {@inheritDoc}
853                     */
854                    public void onCancel(DialogInterface dialog) {
855                        removeDialog(DIALOG_DELETE_PROJECT_ID);
856                    }
857                }, true);
858            }
859
860            case DIALOG_DELETE_BAD_PROJECT_ID: {
861                return AlertDialogs.createAlert(this, getString(R.string.editor_delete_project), 0,
862                                getString(R.string.editor_load_error),
863                                    getString(R.string.yes),
864                        new DialogInterface.OnClickListener() {
865                    /*
866                     * {@inheritDoc}
867                     */
868                    public void onClick(DialogInterface dialog, int which) {
869                        ApiService.deleteProject(VideoEditorActivity.this,
870                                bundle.getString(PARAM_PROJECT_PATH));
871
872                        removeDialog(DIALOG_DELETE_BAD_PROJECT_ID);
873                        finish();
874                    }
875                }, getString(R.string.no), new DialogInterface.OnClickListener() {
876                    /*
877                     * {@inheritDoc}
878                     */
879                    public void onClick(DialogInterface dialog, int which) {
880                        removeDialog(DIALOG_DELETE_BAD_PROJECT_ID);
881                    }
882                }, new DialogInterface.OnCancelListener() {
883                    /*
884                     * {@inheritDoc}
885                     */
886                    public void onCancel(DialogInterface dialog) {
887                        removeDialog(DIALOG_DELETE_BAD_PROJECT_ID);
888                    }
889                }, true);
890            }
891
892            case DIALOG_EDIT_PROJECT_NAME_ID: {
893                if (mProject == null) {
894                    return null;
895                }
896
897                return AlertDialogs.createEditDialog(this,
898                    getString(R.string.editor_edit_project_name),
899                    mProject.getName(), getString(android.R.string.ok),
900                    new DialogInterface.OnClickListener() {
901                        /*
902                         * {@inheritDoc}
903                         */
904                        public void onClick(DialogInterface dialog, int which) {
905                            final TextView tv =
906                                (TextView)((AlertDialog)dialog).findViewById(R.id.text_1);
907                            mProject.setProjectName(tv.getText().toString());
908                            getActionBar().setTitle(tv.getText());
909                            removeDialog(DIALOG_EDIT_PROJECT_NAME_ID);
910                        }
911                    }, getString(android.R.string.cancel),
912                    new DialogInterface.OnClickListener() {
913                        /*
914                         * {@inheritDoc}
915                         */
916                        public void onClick(DialogInterface dialog, int which) {
917                            removeDialog(DIALOG_EDIT_PROJECT_NAME_ID);
918                        }
919                    }, new DialogInterface.OnCancelListener() {
920                        /*
921                         * {@inheritDoc}
922                         */
923                        public void onCancel(DialogInterface dialog) {
924                            removeDialog(DIALOG_EDIT_PROJECT_NAME_ID);
925                        }
926                    }, InputType.TYPE_NULL, 32);
927            }
928
929            case DIALOG_EXPORT_OPTIONS_ID: {
930                if (mProject == null) {
931                    return null;
932                }
933
934                return ExportOptionsDialog.create(this,
935                        new ExportOptionsDialog.ExportOptionsListener() {
936                    /*
937                     * {@inheritDoc}
938                     */
939                    public void onExportOptions(int movieHeight, int movieBitrate) {
940                        mPendingExportFilename = FileUtils.createMovieName(
941                                MediaProperties.FILE_MP4);
942                        ApiService.exportVideoEditor(VideoEditorActivity.this, mProjectPath,
943                                mPendingExportFilename, movieHeight, movieBitrate);
944
945                        removeDialog(DIALOG_EXPORT_OPTIONS_ID);
946
947                        showExportProgress();
948                    }
949                }, new DialogInterface.OnClickListener() {
950                    /*
951                     * {@inheritDoc}
952                     */
953                    public void onClick(DialogInterface dialog, int which) {
954                        removeDialog(DIALOG_EXPORT_OPTIONS_ID);
955                    }
956                }, new DialogInterface.OnCancelListener() {
957                    /*
958                     * {@inheritDoc}
959                     */
960                    public void onCancel(DialogInterface dialog) {
961                        removeDialog(DIALOG_EXPORT_OPTIONS_ID);
962                    }
963                }, mProject.getAspectRatio());
964            }
965
966            case DIALOG_REMOVE_MEDIA_ITEM_ID: {
967                return mMediaLayout.onCreateDialog(id, bundle);
968            }
969
970            case DIALOG_CHANGE_RENDERING_MODE_ID: {
971                return mMediaLayout.onCreateDialog(id, bundle);
972            }
973
974            case DIALOG_REMOVE_TRANSITION_ID: {
975                return mMediaLayout.onCreateDialog(id, bundle);
976            }
977
978            case DIALOG_REMOVE_OVERLAY_ID: {
979                return mOverlayLayout.onCreateDialog(id, bundle);
980            }
981
982            case DIALOG_REMOVE_EFFECT_ID: {
983                return mMediaLayout.onCreateDialog(id, bundle);
984            }
985
986            case DIALOG_REMOVE_AUDIO_TRACK_ID: {
987                return mAudioTrackLayout.onCreateDialog(id, bundle);
988            }
989
990            default: {
991                return null;
992            }
993        }
994    }
995
996    /*
997     * {@inheritDoc}
998     */
999    public void onClickHandler(View target) {
1000        final long playheadPosMs = mProject.getPlayheadPos();
1001
1002        switch (target.getId()) {
1003            case R.id.editor_play: {
1004                if (mProject != null && mPreviewThread != null) {
1005                    if (mPreviewThread.isPlaying()) {
1006                        mPreviewThread.stopPreviewPlayback();
1007                    } else if (mProject.getMediaItemCount() > 0){
1008                        mPreviewThread.startPreviewPlayback(mProject, playheadPosMs);
1009                    }
1010                }
1011                break;
1012            }
1013
1014            case R.id.editor_rewind: {
1015                if (mProject != null && mPreviewThread != null) {
1016                    if (mPreviewThread.isPlaying()) {
1017                        mPreviewThread.stopPreviewPlayback();
1018                        movePlayhead(0);
1019                        mPreviewThread.startPreviewPlayback(mProject, 0);
1020                    } else {
1021                        movePlayhead(0);
1022                        showPreviewFrame();
1023                    }
1024                }
1025                break;
1026            }
1027
1028            case R.id.editor_next: {
1029                if (mProject != null && mPreviewThread != null) {
1030                    final boolean restartPreview;
1031                    if (mPreviewThread.isPlaying()) {
1032                        mPreviewThread.stopPreviewPlayback();
1033                        restartPreview = true;
1034                    } else {
1035                        restartPreview = false;
1036                    }
1037
1038                    final MovieMediaItem mediaItem = mProject.getNextMediaItem(playheadPosMs);
1039                    if (mediaItem != null) {
1040                        movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId()));
1041                        if (restartPreview) {
1042                            mPreviewThread.startPreviewPlayback(mProject,
1043                                    mProject.getPlayheadPos());
1044                        } else {
1045                            showPreviewFrame();
1046                        }
1047                    } else { // Move to the end of the timeline
1048                        movePlayhead(mProject.computeDuration());
1049                        showPreviewFrame();
1050                    }
1051                }
1052                break;
1053            }
1054
1055            case R.id.editor_prev: {
1056                if (mProject != null && mPreviewThread != null) {
1057                    final boolean restartPreview;
1058                    if (mPreviewThread.isPlaying()) {
1059                        mPreviewThread.stopPreviewPlayback();
1060                        restartPreview = true;
1061                    } else {
1062                        restartPreview = false;
1063                    }
1064
1065                    final MovieMediaItem mediaItem = mProject.getPreviousMediaItem(playheadPosMs);
1066                    if (mediaItem != null) {
1067                        movePlayhead(mProject.getMediaItemBeginTime(mediaItem.getId()));
1068                    } else { // Move to the beginning of the timeline
1069                        movePlayhead(0);
1070                    }
1071
1072                    if (restartPreview) {
1073                        mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos());
1074                    } else {
1075                        showPreviewFrame();
1076                    }
1077                }
1078                break;
1079            }
1080
1081            default: {
1082                break;
1083            }
1084        }
1085    }
1086
1087    /*
1088     * {@inheritDoc}
1089     */
1090    @Override
1091    protected void onActivityResult(int requestCode, int resultCode, Intent extras) {
1092        super.onActivityResult(requestCode, resultCode, extras);
1093        if (resultCode == RESULT_CANCELED) {
1094            switch (requestCode) {
1095                case REQUEST_CODE_CAPTURE_VIDEO:
1096                case REQUEST_CODE_CAPTURE_IMAGE: {
1097                    if (mCaptureMediaUri != null) {
1098                        getContentResolver().delete(mCaptureMediaUri, null, null);
1099                        mCaptureMediaUri = null;
1100                    }
1101                    break;
1102                }
1103
1104                default: {
1105                    break;
1106                }
1107            }
1108            return;
1109        }
1110
1111        switch (requestCode) {
1112            case REQUEST_CODE_CAPTURE_VIDEO: {
1113                if (mProject != null) {
1114                    ApiService.addMediaItemVideoUri(this, mProjectPath,
1115                            ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
1116                            mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
1117                            mProject.getTheme());
1118                    mInsertMediaItemAfterMediaItemId = null;
1119                } else {
1120                    // Add this video after the project loads
1121                    mAddMediaItemVideoUri = mCaptureMediaUri;
1122                }
1123                mCaptureMediaUri = null;
1124                break;
1125            }
1126
1127            case REQUEST_CODE_CAPTURE_IMAGE: {
1128                if (mProject != null) {
1129                    ApiService.addMediaItemImageUri(this, mProjectPath,
1130                            ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
1131                            mCaptureMediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
1132                            MediaItemUtils.getDefaultImageDuration(),
1133                            mProject.getTheme());
1134                    mInsertMediaItemAfterMediaItemId = null;
1135                } else {
1136                    // Add this image after the project loads
1137                    mAddMediaItemImageUri = mCaptureMediaUri;
1138                }
1139                mCaptureMediaUri = null;
1140                break;
1141            }
1142
1143            case REQUEST_CODE_IMPORT_VIDEO: {
1144                final Uri mediaUri = extras.getData();
1145                if (mProject != null) {
1146                    if ("media".equals(mediaUri.getAuthority())) {
1147                        ApiService.addMediaItemVideoUri(this, mProjectPath,
1148                                ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
1149                                mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
1150                                mProject.getTheme());
1151                    } else {
1152                        // Notify the user that this item needs to be downloaded.
1153                        Toast.makeText(this, getString(R.string.editor_video_load),
1154                                Toast.LENGTH_LONG).show();
1155                        // When the download is complete insert it into the project.
1156                        ApiService.loadMediaItem(this, mProjectPath, mediaUri, "video/*");
1157                    }
1158                    mInsertMediaItemAfterMediaItemId = null;
1159                } else {
1160                    // Add this video after the project loads
1161                    mAddMediaItemVideoUri = mediaUri;
1162                }
1163                break;
1164            }
1165
1166            case REQUEST_CODE_IMPORT_IMAGE: {
1167                final Uri mediaUri = extras.getData();
1168                if (mProject != null) {
1169                    if ("media".equals(mediaUri.getAuthority())) {
1170                        ApiService.addMediaItemImageUri(this, mProjectPath,
1171                                ApiService.generateId(), mInsertMediaItemAfterMediaItemId,
1172                                mediaUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
1173                                MediaItemUtils.getDefaultImageDuration(), mProject.getTheme());
1174                    } else {
1175                        // Notify the user that this item needs to be downloaded.
1176                        Toast.makeText(this, getString(R.string.editor_image_load),
1177                                Toast.LENGTH_LONG).show();
1178                        // When the download is complete insert it into the project.
1179                        ApiService.loadMediaItem(this, mProjectPath, mediaUri, "image/*");
1180                    }
1181                    mInsertMediaItemAfterMediaItemId = null;
1182                } else {
1183                    // Add this image after the project loads
1184                    mAddMediaItemImageUri = mediaUri;
1185                }
1186                break;
1187            }
1188
1189            case REQUEST_CODE_IMPORT_MUSIC: {
1190                final Uri data = extras.getData();
1191                if (mProject != null) {
1192                    ApiService.addAudioTrack(this, mProjectPath, ApiService.generateId(), data,
1193                            true);
1194                } else {
1195                    mAddAudioTrackUri = data;
1196                }
1197                break;
1198            }
1199
1200            case REQUEST_CODE_EDIT_TRANSITION: {
1201                final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1);
1202                final String afterMediaId = extras.getStringExtra(
1203                        TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID);
1204                final String transitionId = extras.getStringExtra(
1205                        TransitionsActivity.PARAM_TRANSITION_ID);
1206                final long transitionDurationMs = extras.getLongExtra(
1207                        TransitionsActivity.PARAM_TRANSITION_DURATION, 500);
1208                if (mProject != null) {
1209                    mMediaLayout.editTransition(afterMediaId, transitionId, type,
1210                            transitionDurationMs);
1211                } else {
1212                    // Add this transition after you load the project
1213                    mEditTransitionAfterMediaId = afterMediaId;
1214                    mEditTransitionId = transitionId;
1215                    mEditTransitionType = type;
1216                    mEditTransitionDurationMs = transitionDurationMs;
1217                }
1218                break;
1219            }
1220
1221            case REQUEST_CODE_PICK_TRANSITION: {
1222                final int type = extras.getIntExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, -1);
1223                final String afterMediaId = extras.getStringExtra(
1224                        TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID);
1225                final long transitionDurationMs = extras.getLongExtra(
1226                        TransitionsActivity.PARAM_TRANSITION_DURATION, 500);
1227                if (mProject != null) {
1228                    mMediaLayout.addTransition(afterMediaId, type, transitionDurationMs);
1229                } else {
1230                    // Add this transition after you load the project
1231                    mAddTransitionAfterMediaId = afterMediaId;
1232                    mAddTransitionType = type;
1233                    mAddTransitionDurationMs = transitionDurationMs;
1234                }
1235                break;
1236            }
1237
1238            case REQUEST_CODE_PICK_OVERLAY: {
1239                final String mediaItemId =
1240                    extras.getStringExtra(OverlayTitleActivity.PARAM_MEDIA_ITEM_ID);
1241                final Bundle bundle =
1242                    extras.getBundleExtra(OverlayTitleActivity.PARAM_OVERLAY_ATTRIBUTES);
1243                if (mProject != null) {
1244                    final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId);
1245                    if (mediaItem != null) {
1246                        ApiService.addOverlay(this, mProject.getPath(), mediaItemId,
1247                                ApiService.generateId(), bundle,
1248                                mediaItem.getAppBoundaryBeginTime(),
1249                                OverlayLinearLayout.DEFAULT_TITLE_DURATION);
1250                        mOverlayLayout.invalidateCAB();
1251                    }
1252                } else {
1253                    // Add this overlay after you load the project
1254                    mAddOverlayMediaItemId = mediaItemId;
1255                    mAddOverlayUserAttributes = bundle;
1256                }
1257                break;
1258            }
1259
1260            case REQUEST_CODE_EDIT_OVERLAY: {
1261                final Bundle bundle =
1262                    extras.getBundleExtra(OverlayTitleActivity.PARAM_OVERLAY_ATTRIBUTES);
1263                final String mediaItemId =
1264                    extras.getStringExtra(OverlayTitleActivity.PARAM_MEDIA_ITEM_ID);
1265                final String overlayId =
1266                    extras.getStringExtra(OverlayTitleActivity.PARAM_OVERLAY_ID);
1267                if (mProject != null) {
1268                    ApiService.setOverlayUserAttributes(this, mProject.getPath(), mediaItemId,
1269                            overlayId, bundle);
1270                    mOverlayLayout.invalidateCAB();
1271                } else {
1272                    // Edit this overlay after you load the project
1273                    mEditOverlayMediaItemId = mediaItemId;
1274                    mEditOverlayId = overlayId;
1275                    mEditOverlayUserAttributes = bundle;
1276                }
1277                break;
1278            }
1279
1280            case REQUEST_CODE_PICK_EFFECT: {
1281                final String mediaItemId =
1282                    extras.getStringExtra(EffectsActivity.PARAM_MEDIA_ITEM_ID);
1283                final int type = extras.getIntExtra(EffectsActivity.PARAM_EFFECT_TYPE,
1284                        EffectType.EFFECT_COLOR_GRADIENT);
1285                final Rect startRect = extras.getParcelableExtra(EffectsActivity.PARAM_START_RECT);
1286                final Rect endRect = extras.getParcelableExtra(EffectsActivity.PARAM_END_RECT);
1287                if (mProject != null) {
1288                    mMediaLayout.addEffect(type, mediaItemId, startRect, endRect);
1289                } else {
1290                    // Add this effect after you load the project
1291                    mAddEffectMediaItemId = mediaItemId;
1292                    mAddEffectType = type;
1293                    mAddKenBurnsStartRect = startRect;
1294                    mAddKenBurnsEndRect = endRect;
1295                }
1296                break;
1297            }
1298
1299            case REQUEST_CODE_EDIT_EFFECT: {
1300                final String mediaItemId =
1301                    extras.getStringExtra(EffectsActivity.PARAM_MEDIA_ITEM_ID);
1302                final int type = extras.getIntExtra(EffectsActivity.PARAM_EFFECT_TYPE,
1303                        EffectType.EFFECT_COLOR_GRADIENT);
1304                final Rect startRect = extras.getParcelableExtra(EffectsActivity.PARAM_START_RECT);
1305                final Rect endRect = extras.getParcelableExtra(EffectsActivity.PARAM_END_RECT);
1306                if (mProject != null) {
1307                    mMediaLayout.editEffect(type, mediaItemId, startRect, endRect);
1308                } else {
1309                    // Add this effect after you load the project
1310                    mEditEffectMediaItemId = mediaItemId;
1311                    mEditEffectType = type;
1312                    mEditKenBurnsStartRect = startRect;
1313                    mEditKenBurnsEndRect = endRect;
1314                }
1315                break;
1316            }
1317
1318            default: {
1319                break;
1320            }
1321        }
1322    }
1323
1324    /*
1325     * {@inheritDoc}
1326     */
1327    public void surfaceCreated(SurfaceHolder holder) {
1328        if (Log.isLoggable(TAG, Log.DEBUG)) {
1329            Log.d(TAG, "surfaceCreated");
1330        }
1331
1332        mPreviewThread = new PreviewThread(mSurfaceHolder);
1333
1334        restartPreview();
1335    }
1336
1337    /*
1338     * {@inheritDoc}
1339     */
1340    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
1341        if (Log.isLoggable(TAG, Log.DEBUG)) {
1342            Log.d(TAG, "surfaceChanged: " + width + "x" + height);
1343        }
1344
1345        if (mPreviewThread != null) {
1346            mPreviewThread.onSurfaceChanged(width, height);
1347        }
1348    }
1349
1350    /*
1351     * {@inheritDoc}
1352     */
1353    public void surfaceDestroyed(SurfaceHolder holder) {
1354        if (Log.isLoggable(TAG, Log.DEBUG)) {
1355            Log.d(TAG, "surfaceDestroyed");
1356        }
1357
1358        // Stop the preview playback if pending and quit the preview thread
1359        if (mPreviewThread != null) {
1360            mPreviewThread.stopPreviewPlayback();
1361            mPreviewThread.quit();
1362            mPreviewThread = null;
1363        }
1364    }
1365
1366    /*
1367     * {@inheritDoc}
1368     */
1369    @Override
1370    protected void enterTransitionalState(int statusStringId) {
1371        mEditorProjectView.setVisibility(View.GONE);
1372        mEditorEmptyView.setVisibility(View.VISIBLE);
1373
1374        ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId);
1375        findViewById(R.id.empty_project_progress).setVisibility(View.VISIBLE);
1376    }
1377
1378    /*
1379     * {@inheritDoc}
1380     */
1381    @Override
1382    protected void enterDisabledState(int statusStringId) {
1383        mEditorProjectView.setVisibility(View.GONE);
1384        mEditorEmptyView.setVisibility(View.VISIBLE);
1385
1386        getActionBar().setTitle(R.string.full_app_name);
1387
1388        ((TextView)findViewById(R.id.empty_project_text)).setText(statusStringId);
1389        findViewById(R.id.empty_project_progress).setVisibility(View.GONE);
1390    }
1391
1392    /*
1393     * {@inheritDoc}
1394     */
1395    @Override
1396    protected void enterReadyState() {
1397        mEditorProjectView.setVisibility(View.VISIBLE);
1398        mEditorEmptyView.setVisibility(View.GONE);
1399    }
1400
1401    /*
1402     * {@inheritDoc}
1403     */
1404    @Override
1405    protected boolean showPreviewFrame() {
1406        if (mPreviewThread == null) { // The surface is not ready
1407            return false;
1408        }
1409
1410        // Regenerate the preview frame
1411        if (mProject != null && !mPreviewThread.isPlaying() && mPendingExportFilename == null) {
1412            // Display the preview frame
1413            mPreviewThread.previewFrame(mProject, mProject.getPlayheadPos(),
1414                    mProject.getMediaItemCount() == 0);
1415        }
1416
1417        return true;
1418    }
1419
1420    /*
1421     * {@inheritDoc}
1422     */
1423    @Override
1424    protected void updateTimelineDuration() {
1425        if (mProject == null) {
1426            return;
1427        }
1428
1429        final long durationMs = mProject.computeDuration();
1430
1431        // Resize the timeline according to the new timeline duration
1432        final int zoomWidth = mActivityWidth + timeToDimension(durationMs);
1433        final int childrenCount = mTimelineLayout.getChildCount();
1434        for (int i = 0; i < childrenCount; i++) {
1435            final View child = mTimelineLayout.getChildAt(i);
1436            final ViewGroup.LayoutParams lp = child.getLayoutParams();
1437            lp.width = zoomWidth;
1438            child.setLayoutParams(lp);
1439        }
1440
1441        mTimelineLayout.requestLayout(mLayoutCallback);
1442
1443        // Since the duration has changed make sure that the playhead
1444        // position is valid.
1445        if (mProject.getPlayheadPos() > durationMs) {
1446            movePlayhead(durationMs);
1447        }
1448
1449        mAudioTrackLayout.updateTimelineDuration();
1450    }
1451
1452    /**
1453     * Convert the time to dimension
1454     * At zoom level 1: one activity width = 1200 seconds
1455     * At zoom level 2: one activity width = 600 seconds
1456     * ...
1457     * At zoom level 100: one activity width = 12 seconds
1458     *
1459     * At zoom level 1000: one activity width = 1.2 seconds
1460     *
1461     * @param durationMs The time
1462     *
1463     * @return The dimension
1464     */
1465    private int timeToDimension(long durationMs) {
1466        return (int)((mProject.getZoomLevel() * mActivityWidth * durationMs) / 1200000);
1467    }
1468
1469    /**
1470     * Zoom the timeline
1471     *
1472     * @param level The zoom level
1473     * @param updateControl true to set the control position to match the
1474     *      zoom level
1475     */
1476    private int zoomTimeline(int level, boolean updateControl) {
1477        if (level < 1 || level > MAX_ZOOM_LEVEL) {
1478            return mProject.getZoomLevel();
1479        }
1480
1481        mProject.setZoomLevel(level);
1482        if (Log.isLoggable(TAG, Log.VERBOSE)) {
1483            Log.v(TAG, "zoomTimeline level: " + level + " -> " + timeToDimension(1000) + " pix/s");
1484        }
1485
1486        updateTimelineDuration();
1487
1488        if (updateControl) {
1489            mZoomControl.setProgress(level);
1490        }
1491        return level;
1492    }
1493
1494    /*
1495     * {@inheritDoc}
1496     */
1497    @Override
1498    protected void movePlayhead(long timeMs) {
1499        if (mProject == null) {
1500            return;
1501        }
1502
1503        if (setPlayhead(timeMs)) {
1504            // Scroll the timeline such that the specified position
1505            // is in the center of the screen
1506            mTimelineScroller.appScrollTo(timeToDimension(timeMs), true);
1507        }
1508    }
1509
1510    /**
1511     * Set the playhead at the specified time position
1512     *
1513     * @param timeMs The time position
1514     *
1515     * @return true if the playhead was set at the specified time position
1516     */
1517    private boolean setPlayhead(long timeMs) {
1518        // Check if the position would change
1519        if (mCurrentPlayheadPosMs == timeMs) {
1520            return false;
1521        }
1522
1523        // Check if the time is valid. Note that invalid values are common due
1524        // to overscrolling the timeline
1525        if (timeMs < 0) {
1526            return false;
1527        } else if (timeMs > mProject.computeDuration()) {
1528            return false;
1529        }
1530
1531        mCurrentPlayheadPosMs = timeMs;
1532
1533        mTimeView.setText(StringUtils.getTimestampAsString(this, timeMs));
1534        mProject.setPlayheadPos(timeMs);
1535        return true;
1536    }
1537
1538    /*
1539     * {@inheritDoc}
1540     */
1541    @Override
1542    protected void setAspectRatio(final int aspectRatio) {
1543        final FrameLayout.LayoutParams lp =
1544            (FrameLayout.LayoutParams)mSurfaceView.getLayoutParams();
1545
1546        switch (aspectRatio) {
1547            case MediaProperties.ASPECT_RATIO_5_3: {
1548                lp.width = (lp.height * 5) / 3;
1549                break;
1550            }
1551
1552            case MediaProperties.ASPECT_RATIO_4_3: {
1553                lp.width = (lp.height * 4) / 3;
1554                break;
1555            }
1556
1557            case MediaProperties.ASPECT_RATIO_3_2: {
1558                lp.width = (lp.height * 3) / 2;
1559                break;
1560            }
1561
1562            case MediaProperties.ASPECT_RATIO_11_9: {
1563                lp.width = (lp.height * 11) / 9;
1564                break;
1565            }
1566
1567            case MediaProperties.ASPECT_RATIO_16_9: {
1568                lp.width = (lp.height * 16) / 9;
1569                break;
1570            }
1571
1572            default: {
1573                break;
1574            }
1575        }
1576
1577        if (Log.isLoggable(TAG, Log.DEBUG)) {
1578            Log.d(TAG, "setAspectRatio: " + aspectRatio + ", size: " + lp.width + "x" + lp.height);
1579        }
1580        mSurfaceView.setLayoutParams(lp);
1581        mOverlayView.setLayoutParams(lp);
1582    }
1583
1584    /*
1585     * {@inheritDoc}
1586     */
1587    @Override
1588    protected MediaLinearLayout getMediaLayout() {
1589        return mMediaLayout;
1590    }
1591
1592    /*
1593     * {@inheritDoc}
1594     */
1595    @Override
1596    protected OverlayLinearLayout getOverlayLayout() {
1597        return mOverlayLayout;
1598    }
1599
1600    /*
1601     * {@inheritDoc}
1602     */
1603    @Override
1604    protected AudioTrackLinearLayout getAudioTrackLayout() {
1605        return mAudioTrackLayout;
1606    }
1607
1608    /*
1609     * {@inheritDoc}
1610     */
1611    @Override
1612    protected void onExportProgress(int progress) {
1613        if (mExportProgressDialog != null) {
1614            mExportProgressDialog.setProgress(progress);
1615        }
1616    }
1617
1618    /*
1619     * {@inheritDoc}
1620     */
1621    @Override
1622    protected void onExportComplete() {
1623        if (mExportProgressDialog != null) {
1624            mExportProgressDialog.dismiss();
1625            mExportProgressDialog = null;
1626        }
1627    }
1628
1629    /*
1630     * {@inheritDoc}
1631     */
1632    @Override
1633    protected void onProjectEditStateChange(boolean projectEdited) {
1634        if (Log.isLoggable(TAG, Log.DEBUG)) {
1635            Log.d(TAG, "onProjectEditStateChange: " + projectEdited);
1636        }
1637
1638        mPreviewPlayButton.setAlpha(projectEdited ? 100 : 255);
1639        mPreviewPlayButton.setEnabled(!projectEdited);
1640        mPreviewRewindButton.setEnabled(!projectEdited);
1641        mPreviewNextButton.setEnabled(!projectEdited);
1642        mPreviewPrevButton.setEnabled(!projectEdited);
1643
1644        mMediaLayout.invalidateCAB();
1645        mOverlayLayout.invalidateCAB();
1646    }
1647
1648    /*
1649     * {@inheritDoc}
1650     */
1651    @Override
1652    protected void initializeFromProject(boolean updateUI) {
1653        if (Log.isLoggable(TAG, Log.DEBUG)) {
1654            Log.d(TAG, "Project was clean: " + mProject.isClean());
1655        }
1656
1657        if (updateUI || !mProject.isClean()) {
1658            getActionBar().setTitle(mProject.getName());
1659
1660            // Clear the media related to the previous project and
1661            // add the media for the current project.
1662            mMediaLayout.setProject(mProject);
1663            mOverlayLayout.setProject(mProject);
1664            mAudioTrackLayout.setProject(mProject);
1665            mPlayheadView.setProject(mProject);
1666
1667            // Add the media items to the media item layout
1668            mMediaLayout.addMediaItems(mProject.getMediaItems());
1669
1670            // Add the media items to the overlay layout
1671            mOverlayLayout.addMediaItems(mProject.getMediaItems());
1672
1673            // Add the audio tracks to the audio tracks layout
1674            mAudioTrackLayout.addAudioTracks(mProject.getAudioTracks());
1675
1676            setAspectRatio(mProject.getAspectRatio());
1677        }
1678
1679        updateTimelineDuration();
1680        zoomTimeline(mProject.getZoomLevel(), true);
1681
1682        // Set the playhead position. We need to wait for the layout to
1683        // complete before we can scroll to the playhead position.
1684        final Handler handler = new Handler();
1685        handler.post(new Runnable() {
1686            private final long DELAY = 100;
1687            private final int ATTEMPTS = 20;
1688            private int mAttempts = ATTEMPTS;
1689
1690            /*
1691             * {@inheritDoc}
1692             */
1693            public void run() {
1694                if (mAttempts == ATTEMPTS) { // Only scroll once
1695                    movePlayhead(mProject.getPlayheadPos());
1696                }
1697
1698                // If the surface is not yet created (showPreviewFrame()
1699                // returns false) wait for a while (DELAY * ATTEMPTS).
1700                if (showPreviewFrame() == false && mAttempts >= 0) {
1701                    mAttempts--;
1702                    if (mAttempts >= 0) {
1703                        handler.postDelayed(this, DELAY);
1704                    }
1705                }
1706            }
1707        });
1708
1709        if (mAddMediaItemVideoUri != null) {
1710            ApiService.addMediaItemVideoUri(this, mProjectPath, ApiService.generateId(),
1711                    mInsertMediaItemAfterMediaItemId,
1712                    mAddMediaItemVideoUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
1713                    mProject.getTheme());
1714            mAddMediaItemVideoUri = null;
1715            mInsertMediaItemAfterMediaItemId = null;
1716        }
1717
1718        if (mAddMediaItemImageUri != null) {
1719            ApiService.addMediaItemImageUri(this, mProjectPath, ApiService.generateId(),
1720                    mInsertMediaItemAfterMediaItemId,
1721                    mAddMediaItemImageUri, MediaItem.RENDERING_MODE_BLACK_BORDER,
1722                    MediaItemUtils.getDefaultImageDuration(), mProject.getTheme());
1723            mAddMediaItemImageUri = null;
1724            mInsertMediaItemAfterMediaItemId = null;
1725        }
1726
1727        if (mAddAudioTrackUri != null) {
1728            ApiService.addAudioTrack(this, mProject.getPath(), ApiService.generateId(),
1729                    mAddAudioTrackUri, true);
1730            mAddAudioTrackUri = null;
1731        }
1732
1733        if (mAddTransitionAfterMediaId != null) {
1734            mMediaLayout.addTransition(mAddTransitionAfterMediaId, mAddTransitionType,
1735                    mAddTransitionDurationMs);
1736            mAddTransitionAfterMediaId = null;
1737        }
1738
1739        if (mEditTransitionId != null) {
1740            mMediaLayout.editTransition(mEditTransitionAfterMediaId, mEditTransitionId,
1741                    mEditTransitionType, mEditTransitionDurationMs);
1742            mEditTransitionId = null;
1743            mEditTransitionAfterMediaId = null;
1744        }
1745
1746        if (mAddOverlayMediaItemId != null) {
1747            ApiService.addOverlay(this, mProject.getPath(), mAddOverlayMediaItemId,
1748                    ApiService.generateId(), mAddOverlayUserAttributes, 0,
1749                    OverlayLinearLayout.DEFAULT_TITLE_DURATION);
1750            mAddOverlayMediaItemId = null;
1751            mAddOverlayUserAttributes = null;
1752        }
1753
1754        if (mEditOverlayMediaItemId != null) {
1755            ApiService.setOverlayUserAttributes(this, mProject.getPath(), mEditOverlayMediaItemId,
1756                    mEditOverlayId, mEditOverlayUserAttributes);
1757            mEditOverlayMediaItemId = null;
1758            mEditOverlayId = null;
1759            mEditOverlayUserAttributes = null;
1760        }
1761
1762        if (mAddEffectMediaItemId != null) {
1763            mMediaLayout.addEffect(mAddEffectType, mAddEffectMediaItemId,
1764                        mAddKenBurnsStartRect, mAddKenBurnsEndRect);
1765            mAddEffectMediaItemId = null;
1766        }
1767
1768        if (mEditEffectMediaItemId != null) {
1769            mMediaLayout.editEffect(mEditEffectType, mEditEffectMediaItemId,
1770                    mEditKenBurnsStartRect, mEditKenBurnsEndRect);
1771            mEditEffectMediaItemId = null;
1772        }
1773
1774        enterReadyState();
1775
1776        if (mPendingExportFilename != null) {
1777            if (ApiService.isVideoEditorExportPending(mProjectPath, mPendingExportFilename)) {
1778                // The export is still pending
1779                // Display the export project dialog
1780                showExportProgress();
1781            } else {
1782                // The export completed while the Activity was paused
1783                mPendingExportFilename = null;
1784            }
1785        }
1786
1787        invalidateOptionsMenu();
1788
1789        restartPreview();
1790    }
1791
1792    /**
1793     * Restart preview
1794     */
1795    private void restartPreview() {
1796        if (mRestartPreview == false) {
1797            return;
1798        }
1799
1800        if (mProject == null) {
1801            return;
1802        }
1803
1804        if (mPreviewThread != null) {
1805            mRestartPreview = false;
1806            mPreviewThread.startPreviewPlayback(mProject, mProject.getPlayheadPos());
1807        }
1808    }
1809
1810    /**
1811     * Show progress during export operation
1812     */
1813    private void showExportProgress() {
1814        mExportProgressDialog = new ProgressDialog(this);
1815        mExportProgressDialog.setTitle(getString(R.string.export_dialog_export));
1816        mExportProgressDialog.setMessage(null);
1817        mExportProgressDialog.setIndeterminate(false);
1818        mExportProgressDialog.setCancelable(true);
1819        mExportProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
1820        mExportProgressDialog.setMax(100);
1821        mExportProgressDialog.setCanceledOnTouchOutside(false);
1822        mExportProgressDialog.setButton(getString(android.R.string.cancel),
1823                new DialogInterface.OnClickListener() {
1824                /*
1825                 * {@inheritDoc}
1826                 */
1827                public void onClick(DialogInterface dialog, int which) {
1828                    ApiService.cancelExportVideoEditor(VideoEditorActivity.this,
1829                            mProjectPath, mPendingExportFilename);
1830                    mPendingExportFilename = null;
1831                    mExportProgressDialog = null;
1832                }
1833            });
1834        mExportProgressDialog.setCanceledOnTouchOutside(true);
1835        mExportProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
1836            /*
1837             * {@inheritDoc}
1838             */
1839            public void onCancel(DialogInterface dialog) {
1840                ApiService.cancelExportVideoEditor(VideoEditorActivity.this,
1841                        mProjectPath, mPendingExportFilename);
1842                mPendingExportFilename = null;
1843                mExportProgressDialog = null;
1844            }
1845        });
1846        mExportProgressDialog.show();
1847        mExportProgressDialog.setProgressNumberFormat("");
1848    }
1849
1850    /**
1851     * The preview thread
1852     */
1853    private class PreviewThread extends Thread {
1854        // Preview states
1855        private final int PREVIEW_STATE_STOPPED = 0;
1856        private final int PREVIEW_STATE_STARTING = 1;
1857        private final int PREVIEW_STATE_STARTED = 2;
1858        private final int PREVIEW_STATE_STOPPING = 3;
1859
1860        private final int OVERLAY_DATA_COUNT = 16;
1861
1862        private final Handler mMainHandler;
1863        private final Queue<Runnable> mQueue;
1864        private final SurfaceHolder mSurfaceHolder;
1865        private final Queue<VideoEditor.OverlayData> mOverlayDataQueue;
1866        private Handler mThreadHandler;
1867        private int mPreviewState;
1868        private Bitmap mOverlayBitmap;
1869
1870        private final Runnable mProcessQueueRunnable = new Runnable() {
1871            /*
1872             * {@inheritDoc}
1873             */
1874            public void run() {
1875                // Process whatever accumulated in the queue
1876                Runnable runnable;
1877                while ((runnable = mQueue.poll()) != null) {
1878                    runnable.run();
1879                }
1880            }
1881        };
1882
1883        /**
1884         * Constructor
1885         *
1886         * @param surfaceHolder The surface holder
1887         */
1888        public PreviewThread(SurfaceHolder surfaceHolder) {
1889            mMainHandler = new Handler(Looper.getMainLooper());
1890            mQueue = new LinkedBlockingQueue<Runnable>();
1891            mSurfaceHolder = surfaceHolder;
1892            mPreviewState = PREVIEW_STATE_STOPPED;
1893
1894            mOverlayDataQueue = new LinkedBlockingQueue<VideoEditor.OverlayData>();
1895            for (int i = 0; i < OVERLAY_DATA_COUNT; i++) {
1896                mOverlayDataQueue.add(new VideoEditor.OverlayData());
1897            }
1898
1899            start();
1900        }
1901
1902        /**
1903         * Preview the specified frame
1904         *
1905         * @param project The video editor project
1906         * @param timeMs The frame time
1907         * @param clear true to clear the output
1908         */
1909        public void previewFrame(final VideoEditorProject project, final long timeMs,
1910                final boolean clear) {
1911            if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) {
1912                stopPreviewPlayback();
1913            }
1914
1915            if (Log.isLoggable(TAG, Log.DEBUG)) {
1916                Log.d(TAG, "Preview frame at: " + timeMs + " " + clear);
1917            }
1918
1919            // We only need to see the last frame
1920            mQueue.clear();
1921
1922            mQueue.add(new Runnable() {
1923                /*
1924                 * {@inheritDoc}
1925                 */
1926                public void run() {
1927                    if (clear) {
1928                        project.clearSurface(mSurfaceHolder);
1929                        mMainHandler.post(new Runnable() {
1930                            /*
1931                             * {@inheritDoc}
1932                             */
1933                            public void run() {
1934                                if (mOverlayBitmap != null) {
1935                                    mOverlayBitmap.eraseColor(Color.TRANSPARENT);
1936                                    mOverlayView.invalidate();
1937                                }
1938                            }
1939                        });
1940                    } else {
1941                        final VideoEditor.OverlayData overlayData;
1942                        try {
1943                            overlayData = mOverlayDataQueue.remove();
1944                        } catch (NoSuchElementException ex) {
1945                            Log.e(TAG, "Out of OverlayData elements");
1946                            return;
1947                        }
1948
1949                        try {
1950                            if (project.renderPreviewFrame(mSurfaceHolder, timeMs, overlayData)
1951                                    < 0) {
1952                                if (Log.isLoggable(TAG, Log.DEBUG)) {
1953                                    Log.d(TAG, "Cannot render preview frame at: " + timeMs +
1954                                            " of " + mProject.computeDuration());
1955                                }
1956
1957                                mOverlayDataQueue.add(overlayData);
1958                            } else {
1959                                if (overlayData.needsRendering()) {
1960                                    mMainHandler.post(new Runnable() {
1961                                        /*
1962                                         * {@inheritDoc}
1963                                         */
1964                                        public void run() {
1965                                            if (mOverlayBitmap != null) {
1966                                                overlayData.renderOverlay(mOverlayBitmap);
1967                                                mOverlayView.invalidate();
1968                                            } else {
1969                                                overlayData.release();
1970                                            }
1971
1972                                            mOverlayDataQueue.add(overlayData);
1973                                        }
1974                                    });
1975                                } else {
1976                                    mOverlayDataQueue.add(overlayData);
1977                                }
1978                            }
1979                        } catch (Exception ex) {
1980                            if (Log.isLoggable(TAG, Log.DEBUG)) {
1981                                Log.d(TAG, "renderPreviewFrame failed at timeMs: " + timeMs, ex);
1982                            }
1983                            mOverlayDataQueue.add(overlayData);
1984                        }
1985                    }
1986                }
1987            });
1988
1989            if (mThreadHandler != null) {
1990                mThreadHandler.post(mProcessQueueRunnable);
1991            }
1992        }
1993
1994        /**
1995         * Display the frame at the specified time position
1996         *
1997         * @param mediaItem The media item
1998         * @param timeMs The frame time
1999         */
2000        public void renderMediaItemFrame(final MovieMediaItem mediaItem, final long timeMs) {
2001            if (mPreviewState == PREVIEW_STATE_STARTING || mPreviewState == PREVIEW_STATE_STARTED) {
2002                stopPreviewPlayback();
2003            }
2004
2005            if (Log.isLoggable(TAG, Log.VERBOSE)) {
2006                Log.v(TAG, "Render media item frame at: " + timeMs);
2007            }
2008
2009            // We only need to see the last frame
2010            mQueue.clear();
2011
2012            mQueue.add(new Runnable() {
2013                /*
2014                 * {@inheritDoc}
2015                 */
2016                public void run() {
2017                    try {
2018                        if (mProject.renderMediaItemFrame(mSurfaceHolder, mediaItem.getId(),
2019                                timeMs) < 0) {
2020                            if (Log.isLoggable(TAG, Log.DEBUG)) {
2021                                Log.d(TAG, "Cannot render media item frame at: " + timeMs +
2022                                        " of " + mediaItem.getDuration());
2023                            }
2024                        }
2025                    } catch (Exception ex) {
2026                        if (Log.isLoggable(TAG, Log.DEBUG)) {
2027                            Log.d(TAG, "Cannot render preview frame at: " + timeMs, ex);
2028                        }
2029                    }
2030                }
2031            });
2032
2033            if (mThreadHandler != null) {
2034                mThreadHandler.post(mProcessQueueRunnable);
2035            }
2036        }
2037
2038        /**
2039         * Start the preview playback
2040         *
2041         * @param project The video editor project
2042         * @param fromMs Start playing from the specified position
2043         */
2044        private void startPreviewPlayback(final VideoEditorProject project, final long fromMs) {
2045            if (mPreviewState != PREVIEW_STATE_STOPPED) {
2046                if (Log.isLoggable(TAG, Log.DEBUG)) {
2047                    Log.d(TAG, "Preview did not start: " + mPreviewState);
2048                }
2049                return;
2050            }
2051
2052            previewStarted(project);
2053            if (Log.isLoggable(TAG, Log.DEBUG)) {
2054                Log.d(TAG, "Start preview at: " + fromMs);
2055            }
2056
2057            // Clear any pending preview frames
2058            mQueue.clear();
2059            mQueue.add(new Runnable() {
2060                /*
2061                 * {@inheritDoc}
2062                 */
2063                public void run() {
2064                    try {
2065                        project.startPreview(mSurfaceHolder, fromMs, -1, false, 3,
2066                                new VideoEditor.PreviewProgressListener() {
2067                            /*
2068                             * {@inheritDoc}
2069                             */
2070                            public void onStart(VideoEditor videoEditor) {
2071                            }
2072
2073                            /*
2074                             * {@inheritDoc}
2075                             */
2076                            public void onProgress(VideoEditor videoEditor, final long timeMs,
2077                                    final VideoEditor.OverlayData overlayData) {
2078                                mMainHandler.post(new Runnable() {
2079                                    /*
2080                                     * {@inheritDoc}
2081                                     */
2082                                    public void run() {
2083                                        if (overlayData != null && overlayData.needsRendering()) {
2084                                            if (mOverlayBitmap != null) {
2085                                                overlayData.renderOverlay(mOverlayBitmap);
2086                                                mOverlayView.invalidate();
2087                                            } else {
2088                                                overlayData.release();
2089                                            }
2090                                        }
2091
2092                                        if (mPreviewState == PREVIEW_STATE_STARTED ||
2093                                                mPreviewState == PREVIEW_STATE_STOPPING) {
2094                                            movePlayhead(timeMs);
2095                                        }
2096                                    }
2097                                });
2098                            }
2099
2100                            /*
2101                             * {@inheritDoc}
2102                             */
2103                            public void onStop(VideoEditor videoEditor) {
2104                                mMainHandler.post(new Runnable() {
2105                                    /*
2106                                     * {@inheritDoc}
2107                                     */
2108                                    public void run() {
2109                                        if (mPreviewState == PREVIEW_STATE_STARTED ||
2110                                                mPreviewState == PREVIEW_STATE_STOPPING) {
2111                                            previewStopped(false);
2112                                        }
2113                                    }
2114                                });
2115                            }
2116                        });
2117
2118                        mMainHandler.post(new Runnable() {
2119                            /*
2120                             * {@inheritDoc}
2121                             */
2122                            public void run() {
2123                                mPreviewState = PREVIEW_STATE_STARTED;
2124                            }
2125                        });
2126                    } catch (Exception ex) {
2127                        // This exception may occur when trying to play frames
2128                        // at the end of the timeline
2129                        // (e.g. when fromMs == clip duration)
2130                        if (Log.isLoggable(TAG, Log.DEBUG)) {
2131                            Log.d(TAG, "Cannot start preview at: " + fromMs, ex);
2132                        }
2133
2134                        mMainHandler.post(new Runnable() {
2135                            /*
2136                             * {@inheritDoc}
2137                             */
2138                            public void run() {
2139                                mPreviewState = PREVIEW_STATE_STARTED;
2140                                previewStopped(true);
2141                            }
2142                        });
2143                    }
2144                }
2145            });
2146
2147            if (mThreadHandler != null) {
2148                mThreadHandler.post(mProcessQueueRunnable);
2149            }
2150        }
2151
2152        /**
2153         * The preview started.
2154         * This method is always invoked from the UI thread.
2155         *
2156         * @param project The project
2157         */
2158        private void previewStarted(VideoEditorProject project) {
2159            // Change the button image back to a play icon
2160            mPreviewPlayButton.setImageResource(R.drawable.btn_playback_pause_selector);
2161
2162            mTimelineScroller.enableUserScrolling(false);
2163            mMediaLayout.setPlaybackInProgress(true);
2164            mOverlayLayout.setPlaybackInProgress(true);
2165            mAudioTrackLayout.setPlaybackInProgress(true);
2166
2167            mPreviewState = PREVIEW_STATE_STARTING;
2168        }
2169
2170        /**
2171         * Stop previewing
2172         */
2173        private void stopPreviewPlayback() {
2174            switch (mPreviewState) {
2175                case PREVIEW_STATE_STOPPED: {
2176                    if (Log.isLoggable(TAG, Log.DEBUG)) {
2177                        Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STOPPED");
2178                    }
2179                    return;
2180                }
2181
2182                case PREVIEW_STATE_STOPPING: {
2183                    if (Log.isLoggable(TAG, Log.DEBUG)) {
2184                        Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STOPPING");
2185                    }
2186                    return;
2187                }
2188
2189                case PREVIEW_STATE_STARTING: {
2190                    if (Log.isLoggable(TAG, Log.DEBUG)) {
2191                        Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STARTING " +
2192                                "now PREVIEW_STATE_STOPPING");
2193                    }
2194
2195                    mPreviewState = PREVIEW_STATE_STOPPING;
2196
2197                    // We need to wait until the preview starts
2198                    mMainHandler.postDelayed(new Runnable() {
2199                        /*
2200                         * {@inheritDoc}
2201                         */
2202                        public void run() {
2203                            if (isFinishing() || isChangingConfigurations()) {
2204                                // The activity is shutting down. Force stopping now.
2205                                if (Log.isLoggable(TAG, Log.DEBUG)) {
2206                                    Log.d(TAG, "stopPreviewPlayback: Activity is shutting down");
2207                                }
2208
2209                                mPreviewState = PREVIEW_STATE_STARTED;
2210                                previewStopped(true);
2211                            } else if (mPreviewState == PREVIEW_STATE_STARTED) {
2212                                if (Log.isLoggable(TAG, Log.DEBUG)) {
2213                                    Log.d(TAG, "stopPreviewPlayback: Now PREVIEW_STATE_STARTED");
2214                                }
2215
2216                                previewStopped(false);
2217                            } else if (mPreviewState == PREVIEW_STATE_STOPPING) {
2218                                // Keep waiting
2219                                mMainHandler.postDelayed(this, 100);
2220
2221                                if (Log.isLoggable(TAG, Log.DEBUG)) {
2222                                    Log.d(TAG, "stopPreviewPlayback: Waiting for PREVIEW_STATE_STARTED");
2223                                }
2224                            } else {
2225                                if (Log.isLoggable(TAG, Log.DEBUG)) {
2226                                    Log.d(TAG, "stopPreviewPlayback: PREVIEW_STATE_STOPPED while waiting");
2227                                }
2228                            }
2229                        }
2230                    }, 50);
2231
2232                    break;
2233                }
2234
2235                case PREVIEW_STATE_STARTED: {
2236                    if (Log.isLoggable(TAG, Log.DEBUG)) {
2237                        Log.d(TAG, "stopPreviewPlayback: State was PREVIEW_STATE_STARTED");
2238                    }
2239
2240                    // We need to stop
2241                    previewStopped(false);
2242                    return;
2243                }
2244
2245                default: {
2246                    throw new IllegalArgumentException("stopPreviewPlayback state: " +
2247                            mPreviewState);
2248                }
2249            }
2250        }
2251
2252        /**
2253         * The surface size has changed
2254         *
2255         * @param width The new surface width
2256         * @param heightThe new surface height
2257         */
2258        private void onSurfaceChanged(int width, int height) {
2259            if (mOverlayBitmap != null) {
2260                if (mOverlayBitmap.getWidth() == width && mOverlayBitmap.getHeight() == height) {
2261                    // The size has not changed
2262                    return;
2263                }
2264
2265                mOverlayView.setImageBitmap(null);
2266                mOverlayBitmap.recycle();
2267                mOverlayBitmap = null;
2268            }
2269
2270            // Create the overlay bitmap
2271            if (Log.isLoggable(TAG, Log.DEBUG)) {
2272                Log.d(TAG, "Overlay size: " + width + " x " + height);
2273            }
2274
2275            mOverlayBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
2276            mOverlayView.setImageBitmap(mOverlayBitmap);
2277        }
2278
2279        /**
2280         * Preview stopped. This method is always invoked from the UI thread.
2281         *
2282         * @param error true if the preview stopped due to an error
2283         *
2284         * @return The stop position
2285         */
2286        private void previewStopped(boolean error) {
2287            if (mProject == null) {
2288                Log.w(TAG, "previewStopped: project was deleted.");
2289                return;
2290            }
2291
2292            if (mPreviewState != PREVIEW_STATE_STARTED) {
2293                throw new IllegalStateException("previewStopped in state: " + mPreviewState);
2294            }
2295
2296            // Change the button image back to a play icon
2297            mPreviewPlayButton.setImageResource(R.drawable.btn_playback_play_selector);
2298
2299            if (error == false) {
2300                // Set the playhead position at the position where the playback stopped
2301                final long stopTimeMs = mProject.stopPreview();
2302                movePlayhead(stopTimeMs);
2303                if (Log.isLoggable(TAG, Log.DEBUG)) {
2304                    Log.d(TAG, "PREVIEW_STATE_STOPPED: " + stopTimeMs);
2305                }
2306            } else {
2307                if (Log.isLoggable(TAG, Log.DEBUG)) {
2308                    Log.d(TAG, "PREVIEW_STATE_STOPPED due to error");
2309                }
2310            }
2311
2312            mPreviewState = PREVIEW_STATE_STOPPED;
2313
2314            // The playback has stopped
2315            mTimelineScroller.enableUserScrolling(true);
2316            mMediaLayout.setPlaybackInProgress(false);
2317            mAudioTrackLayout.setPlaybackInProgress(false);
2318            mOverlayLayout.setPlaybackInProgress(false);
2319        }
2320
2321        /**
2322         * @return true if preview playback is in progress
2323         */
2324        private boolean isPlaying() {
2325            return mPreviewState == PREVIEW_STATE_STARTING ||
2326                mPreviewState == PREVIEW_STATE_STARTED;
2327        }
2328
2329        /**
2330         * @return true if the preview is stopped
2331         */
2332        private boolean isStopped() {
2333            return mPreviewState == PREVIEW_STATE_STOPPED;
2334        }
2335
2336        /*
2337         * {@inheritDoc}
2338         */
2339        @Override
2340        public void run() {
2341            setPriority(MAX_PRIORITY);
2342            Looper.prepare();
2343            mThreadHandler = new Handler();
2344
2345            // Ensure that the queued items are processed
2346            mMainHandler.post(new Runnable() {
2347                /*
2348                 * {@inheritDoc}
2349                 */
2350                public void run() {
2351                    // Start processing the queue of runnables
2352                    mThreadHandler.post(mProcessQueueRunnable);
2353                }
2354            });
2355
2356            // Run the loop
2357            Looper.loop();
2358        }
2359
2360        /**
2361         * Quit the thread
2362         */
2363        public void quit() {
2364            // Release the overlay bitmap
2365            if (mOverlayBitmap != null) {
2366                mOverlayView.setImageBitmap(null);
2367                mOverlayBitmap.recycle();
2368                mOverlayBitmap = null;
2369            }
2370
2371            if (mThreadHandler != null) {
2372                mThreadHandler.getLooper().quit();
2373                try {
2374                    // Wait for the thread to quit. An ANR waiting to happen.
2375                    mThreadHandler.getLooper().getThread().join();
2376                } catch (InterruptedException ex) {
2377                }
2378            }
2379
2380            mQueue.clear();
2381        }
2382    }
2383}
2384