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