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