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