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