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