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