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