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