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