Home | History | Annotate | Download | only in widgets
      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.widgets;
     18 
     19 import java.util.List;
     20 
     21 import android.app.Activity;
     22 import android.app.AlertDialog;
     23 import android.app.Dialog;
     24 import android.content.ClipData;
     25 import android.content.Context;
     26 import android.content.DialogInterface;
     27 import android.content.Intent;
     28 import android.graphics.Bitmap;
     29 import android.graphics.Rect;
     30 import android.media.videoeditor.EffectColor;
     31 import android.media.videoeditor.MediaItem;
     32 import android.media.videoeditor.Transition;
     33 import android.media.videoeditor.TransitionSliding;
     34 import android.os.Bundle;
     35 import android.os.Handler;
     36 import android.util.AttributeSet;
     37 import android.util.Log;
     38 import android.view.ActionMode;
     39 import android.view.Display;
     40 import android.view.DragEvent;
     41 import android.view.Menu;
     42 import android.view.MenuItem;
     43 import android.view.MotionEvent;
     44 import android.view.View;
     45 import android.widget.ImageButton;
     46 import android.widget.LinearLayout;
     47 import android.widget.RelativeLayout;
     48 import android.widget.Toast;
     49 
     50 import com.android.videoeditor.AlertDialogs;
     51 import com.android.videoeditor.EffectType;
     52 import com.android.videoeditor.KenBurnsActivity;
     53 import com.android.videoeditor.OverlayTitleEditor;
     54 import com.android.videoeditor.TransitionType;
     55 import com.android.videoeditor.TransitionsActivity;
     56 import com.android.videoeditor.VideoEditorActivity;
     57 import com.android.videoeditor.service.ApiService;
     58 import com.android.videoeditor.service.MovieEffect;
     59 import com.android.videoeditor.service.MovieMediaItem;
     60 import com.android.videoeditor.service.MovieOverlay;
     61 import com.android.videoeditor.service.MovieTransition;
     62 import com.android.videoeditor.service.VideoEditorProject;
     63 import com.android.videoeditor.util.FileUtils;
     64 import com.android.videoeditor.util.MediaItemUtils;
     65 import com.android.videoeditor.R;
     66 
     67 /**
     68  * LinearLayout which holds media items and transitions.
     69  */
     70 public class MediaLinearLayout extends LinearLayout {
     71     // Logging
     72     private static final String TAG = "MediaLinearLayout";
     73 
     74     // Dialog parameter ids
     75     private static final String PARAM_DIALOG_MEDIA_ITEM_ID = "media_item_id";
     76     private static final String PARAM_DIALOG_CURRENT_RENDERING_MODE = "rendering_mode";
     77     private static final String PARAM_DIALOG_TRANSITION_ID = "transition_id";
     78 
     79     // Transition duration limits
     80     private static final long MAXIMUM_IMAGE_DURATION = 6000;
     81     private static final long MAXIMUM_TRANSITION_DURATION = 3000;
     82     private static final long MINIMUM_TRANSITION_DURATION = 250;
     83 
     84     private static final long TIME_TOLERANCE = 30;
     85 
     86     // Instance variables
     87     private final ItemSimpleGestureListener mMediaItemGestureListener;
     88     private final ItemSimpleGestureListener mTransitionGestureListener;
     89     private final Handler mHandler;
     90     private final int mHalfParentWidth;
     91     private final int mHandleWidth;
     92     private final int mTransitionVerticalInset;
     93     private final ImageButton mLeftAddClipButton, mRightAddClipButton;
     94     private MediaLinearLayoutListener mListener;
     95     private ActionMode mMediaItemActionMode;
     96     private ActionMode mTransitionActionMode;
     97     private VideoEditorProject mProject;
     98     private boolean mPlaybackInProgress;
     99     private HandleView mLeftHandle, mRightHandle;
    100     private boolean mIsTrimming;  // Indicates if some media item is being trimmed.
    101     private boolean mMoveLayoutPending;
    102     private View mScrollView;  // Convenient handle to the parent scroll view.
    103     private View mSelectedView;
    104     private String mDragMediaItemId;
    105     private float mPrevDragPosition;
    106     private long mPrevDragScrollTime;
    107     private MovieMediaItem mDropAfterMediaItem;
    108     private int mDropIndex;
    109     private boolean mFirstEntered;
    110 
    111     /**
    112      * The media item action mode handler.
    113      */
    114     private class MediaItemActionModeCallback implements ActionMode.Callback {
    115         // Media item associated with this callback.
    116         private final MovieMediaItem mMediaItem;
    117 
    118         public MediaItemActionModeCallback(MovieMediaItem mediaItem) {
    119             mMediaItem = mediaItem;
    120         }
    121 
    122         @Override
    123         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    124             mMediaItemActionMode = mode;
    125 
    126             final Activity activity = (Activity) getContext();
    127             activity.getMenuInflater().inflate(R.menu.media_item_mode_menu, menu);
    128 
    129             return true;
    130         }
    131 
    132         @Override
    133         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    134             final boolean enable = !ApiService.isProjectBeingEdited(mProject.getPath()) &&
    135                 !mPlaybackInProgress;
    136 
    137             // Pan zoom effect is only for images. Hide it from video clips.
    138             MenuItem item;
    139             if (!mMediaItem.isImage()) {
    140                 item = menu.findItem(R.id.action_pan_zoom_effect);
    141                 item.setVisible(false);
    142                 item.setEnabled(false);
    143             }
    144 
    145             // If the selected media item already has an effect applied on it, check the
    146             // corresponding effect menu item.
    147             MovieEffect effect = mMediaItem.getEffect();
    148             if (effect != null) {
    149                 switch (mMediaItem.getEffect().getType()) {
    150                     case EffectType.EFFECT_KEN_BURNS:
    151                         item = menu.findItem(R.id.action_pan_zoom_effect);
    152                         break;
    153                     case EffectType.EFFECT_COLOR_GRADIENT:
    154                         item = menu.findItem(R.id.action_gradient_effect);
    155                         break;
    156                     case EffectType.EFFECT_COLOR_SEPIA:
    157                         item = menu.findItem(R.id.action_sepia_effect);
    158                         break;
    159                     case EffectType.EFFECT_COLOR_NEGATIVE:
    160                         item = menu.findItem(R.id.action_negative_effect);
    161                         break;
    162                     default:
    163                         item = menu.findItem(R.id.action_no_effect);
    164                         break;
    165                 }
    166             } else {
    167                 item = menu.findItem(R.id.action_no_effect);
    168             }
    169             item.setChecked(true);
    170             menu.findItem(R.id.media_item_effect_menu).setEnabled(enable);
    171 
    172             // Menu item for adding a new overlay. It is also used to edit
    173             // existing overlay. We change the displayed text accordingly.
    174             final MenuItem aomi = menu.findItem(R.id.action_add_overlay);
    175             aomi.setTitle((mMediaItem.getOverlay() == null) ?
    176                     R.string.editor_add_overlay : R.string.editor_edit_overlay);
    177             aomi.setEnabled(enable);
    178 
    179             final MenuItem romi = menu.findItem(R.id.action_remove_overlay);
    180             romi.setVisible(mMediaItem.getOverlay() != null);
    181             romi.setEnabled(enable && mMediaItem.getOverlay() != null);
    182 
    183             final MenuItem btmi = menu.findItem(R.id.action_add_begin_transition);
    184             btmi.setVisible(mMediaItem.getBeginTransition() == null);
    185             btmi.setEnabled(enable && mMediaItem.getBeginTransition() == null);
    186 
    187             final MenuItem etmi = menu.findItem(R.id.action_add_end_transition);
    188             etmi.setVisible(mMediaItem.getEndTransition() == null);
    189             etmi.setEnabled(enable && mMediaItem.getEndTransition() == null);
    190 
    191             final MenuItem rmmi = menu.findItem(R.id.action_rendering_mode);
    192             rmmi.setVisible(mProject.hasMultipleAspectRatios());
    193             rmmi.setEnabled(enable && mProject.hasMultipleAspectRatios());
    194 
    195             return true;
    196         }
    197 
    198         @Override
    199         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    200             switch (item.getItemId()) {
    201                 case R.id.action_add_overlay: {
    202                     editOverlay(mMediaItem);
    203                     break;
    204                 }
    205 
    206                 case R.id.action_remove_overlay: {
    207                     removeOverlay(mMediaItem);
    208                     break;
    209                 }
    210 
    211                 case R.id.action_add_begin_transition: {
    212                     final MovieMediaItem prevMediaItem = mProject.getPreviousMediaItem(
    213                             mMediaItem.getId());
    214                     pickTransition(prevMediaItem);
    215                     break;
    216                 }
    217 
    218                 case R.id.action_add_end_transition: {
    219                     pickTransition(mMediaItem);
    220                     break;
    221                 }
    222 
    223                 case R.id.action_gradient_effect:
    224                 case R.id.action_sepia_effect:
    225                 case R.id.action_negative_effect:
    226                 case R.id.action_pan_zoom_effect: {
    227                     applyEffect(item);
    228                     break;
    229                 }
    230 
    231                 case R.id.action_no_effect: {
    232                     if (!item.isChecked()) {
    233                         final Bundle bundle = new Bundle();
    234                         bundle.putString(PARAM_DIALOG_MEDIA_ITEM_ID, mMediaItem.getId());
    235                         ((Activity) getContext()).showDialog(
    236                                 VideoEditorActivity.DIALOG_REMOVE_EFFECT_ID, bundle);
    237                     }
    238                     break;
    239                 }
    240 
    241                 case R.id.action_rendering_mode: {
    242                     final Bundle bundle = new Bundle();
    243                     bundle.putString(PARAM_DIALOG_MEDIA_ITEM_ID, mMediaItem.getId());
    244                     bundle.putInt(PARAM_DIALOG_CURRENT_RENDERING_MODE,
    245                             mMediaItem.getAppRenderingMode());
    246                     ((Activity) getContext()).showDialog(
    247                             VideoEditorActivity.DIALOG_CHANGE_RENDERING_MODE_ID, bundle);
    248                     break;
    249                 }
    250 
    251                 case R.id.action_delete_media_item: {
    252                     final Bundle bundle = new Bundle();
    253                     bundle.putString(PARAM_DIALOG_MEDIA_ITEM_ID, mMediaItem.getId());
    254                     ((Activity) getContext()).showDialog(
    255                             VideoEditorActivity.DIALOG_REMOVE_MEDIA_ITEM_ID, bundle);
    256                     break;
    257                 }
    258 
    259                 default: {
    260                     break;
    261                 }
    262             }
    263 
    264             return true;
    265         }
    266 
    267         @Override
    268         public void onDestroyActionMode(ActionMode mode) {
    269             final View mediaItemView = getMediaItemView(mMediaItem.getId());
    270             if (mSelectedView != null) {
    271                 mLeftHandle.endMove();
    272                 mRightHandle.endMove();
    273             }
    274             unSelect(mediaItemView);
    275             showAddMediaItemButtons(true);
    276             mMediaItemActionMode = null;
    277         }
    278 
    279         private void applyEffect(MenuItem clickedItem) {
    280             if (!clickedItem.isChecked()) {
    281                 switch(clickedItem.getItemId()) {
    282                     case R.id.action_gradient_effect:
    283                         addEffect(EffectType.EFFECT_COLOR_GRADIENT,
    284                                 mMediaItem.getId(), null, null);
    285                         clickedItem.setChecked(true);
    286                         break;
    287                     case R.id.action_sepia_effect:
    288                         addEffect(EffectType.EFFECT_COLOR_SEPIA,
    289                                 mMediaItem.getId(), null, null);
    290                         clickedItem.setChecked(true);
    291                         break;
    292                     case R.id.action_negative_effect:
    293                         addEffect(EffectType.EFFECT_COLOR_NEGATIVE,
    294                                 mMediaItem.getId(), null, null);
    295                         clickedItem.setChecked(true);
    296                         break;
    297                     case R.id.action_pan_zoom_effect: {
    298                         // Note that we don't check the pan zoom checkbox here.
    299                         // Because pan zoom effect will start a new activity and users
    300                         // could cancel applying the effect. Once pan zoom effect has
    301                         // really been applied. The action mode will be invalidated in
    302                         // onActivityResult() method and the checkbox is then checked.
    303                         final Intent intent = new Intent(getContext(), KenBurnsActivity.class);
    304                         intent.putExtra(KenBurnsActivity.PARAM_MEDIA_ITEM_ID, mMediaItem.getId());
    305                         intent.putExtra(KenBurnsActivity.PARAM_FILENAME, mMediaItem.getFilename());
    306                         intent.putExtra(KenBurnsActivity.PARAM_WIDTH, mMediaItem.getWidth());
    307                         intent.putExtra(KenBurnsActivity.PARAM_HEIGHT, mMediaItem.getHeight());
    308                         ((Activity) getContext()).startActivityForResult(intent,
    309                                 VideoEditorActivity.REQUEST_CODE_KEN_BURNS);
    310                         break;
    311                     }
    312                     default:
    313                         break;
    314                 }
    315             }
    316         }
    317     }
    318 
    319     /**
    320      * The transition action mode handler.
    321      */
    322     private class TransitionActionModeCallback implements ActionMode.Callback {
    323         private final MovieTransition mTransition;
    324 
    325         public TransitionActionModeCallback(MovieTransition transition) {
    326             mTransition = transition;
    327         }
    328 
    329         @Override
    330         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    331             mTransitionActionMode = mode;
    332 
    333             final Activity activity = (Activity) getContext();
    334             activity.getMenuInflater().inflate(R.menu.transition_mode_menu, menu);
    335             mode.setTitle(activity.getString(R.string.editor_transition_title));
    336 
    337             return true;
    338         }
    339 
    340         @Override
    341         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    342             final boolean enable = !ApiService.isProjectBeingEdited(mProject.getPath()) &&
    343                 !mPlaybackInProgress;
    344 
    345             final MenuItem etmi = menu.findItem(R.id.action_change_transition);
    346             etmi.setEnabled(enable);
    347 
    348             final MenuItem rtmi = menu.findItem(R.id.action_remove_transition);
    349             rtmi.setEnabled(enable);
    350 
    351             return true;
    352         }
    353 
    354         @Override
    355         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    356             switch (item.getItemId()) {
    357                 case R.id.action_remove_transition: {
    358                     final Bundle bundle = new Bundle();
    359                     bundle.putString(PARAM_DIALOG_TRANSITION_ID, mTransition.getId());
    360                     ((Activity) getContext()).showDialog(
    361                             VideoEditorActivity.DIALOG_REMOVE_TRANSITION_ID, bundle);
    362                     break;
    363                 }
    364 
    365                 case R.id.action_change_transition: {
    366                     editTransition(mTransition);
    367                     break;
    368                 }
    369 
    370                 default: {
    371                     break;
    372                 }
    373             }
    374 
    375             return true;
    376         }
    377 
    378         @Override
    379         public void onDestroyActionMode(ActionMode mode) {
    380             final View transitionView = getTransitionView(mTransition.getId());
    381             unSelect(transitionView);
    382             showAddMediaItemButtons(true);
    383             mTransitionActionMode = null;
    384         }
    385     }
    386 
    387     public MediaLinearLayout(Context context, AttributeSet attrs, int defStyle) {
    388         super(context, attrs, defStyle);
    389 
    390         mMediaItemGestureListener = new ItemSimpleGestureListener() {
    391             @Override
    392             public boolean onSingleTapConfirmed(View view, int area, MotionEvent e) {
    393                 if (mPlaybackInProgress) {
    394                     return false;
    395                 }
    396 
    397                 switch (area) {
    398                     case ItemSimpleGestureListener.LEFT_AREA: {
    399                         if (view.isSelected()) {
    400                             final MovieMediaItem mediaItem = (MovieMediaItem) view.getTag();
    401                             final MovieMediaItem prevMediaItem = mProject.getPreviousMediaItem(
    402                                     mediaItem.getId());
    403                             pickTransition(prevMediaItem);
    404                         }
    405                         break;
    406                     }
    407 
    408                     case ItemSimpleGestureListener.CENTER_AREA: {
    409                         break;
    410                     }
    411 
    412                     case ItemSimpleGestureListener.RIGHT_AREA: {
    413                         if (view.isSelected()) {
    414                             pickTransition((MovieMediaItem) view.getTag());
    415                         }
    416                         break;
    417                     }
    418                 }
    419                 select(view);
    420 
    421                 return true;
    422             }
    423 
    424             @Override
    425             public void onLongPress(View view, MotionEvent e) {
    426                 if (mPlaybackInProgress) {
    427                     return;
    428                 }
    429 
    430                 final MovieMediaItem mediaItem = (MovieMediaItem)view.getTag();
    431                 if (mProject.getMediaItemCount() > 1) {
    432                     view.startDrag(ClipData.newPlainText("File", mediaItem.getFilename()),
    433                             ((MediaItemView)view).getShadowBuilder(), mediaItem.getId(), 0);
    434                 }
    435 
    436                 select(view);
    437 
    438                 if (mMediaItemActionMode == null) {
    439                     startActionMode(new MediaItemActionModeCallback(mediaItem));
    440                 }
    441             }
    442         };
    443 
    444         mTransitionGestureListener = new ItemSimpleGestureListener() {
    445             @Override
    446             public boolean onSingleTapConfirmed(View view, int area, MotionEvent e) {
    447                 if (mPlaybackInProgress) {
    448                     return false;
    449                 }
    450                 select(view);
    451                 return true;
    452             }
    453 
    454             @Override
    455             public void onLongPress(View view, MotionEvent e) {
    456                 if (mPlaybackInProgress) {
    457                     return;
    458                 }
    459 
    460                 select(view);
    461 
    462                 if (mTransitionActionMode == null) {
    463                     startActionMode(new TransitionActionModeCallback(
    464                             (MovieTransition) view.getTag()));
    465                 }
    466             }
    467         };
    468 
    469         // Add the beginning timeline item
    470         final View beginView = inflate(getContext(), R.layout.empty_left_timeline_item, null);
    471         beginView.setOnClickListener(new View.OnClickListener() {
    472             @Override
    473             public void onClick(View view) {
    474                 unselectAllTimelineViews();
    475             }
    476         });
    477 
    478         mLeftAddClipButton = (ImageButton) beginView.findViewById(
    479                 R.id.add_left_media_item_button);
    480         mLeftAddClipButton.setVisibility(View.GONE);
    481         mLeftAddClipButton.setOnClickListener(new View.OnClickListener() {
    482             @Override
    483             public void onClick(View view) {
    484                 if (mProject != null && mProject.getMediaItemCount() > 0) {
    485                     unselectAllTimelineViews();
    486                     // Add a clip at the beginning of the movie.
    487                     mListener.onAddMediaItem(null);
    488                 }
    489             }
    490         });
    491         addView(beginView);
    492 
    493         // Add the end timeline item
    494         final View endView = inflate(getContext(), R.layout.empty_right_timeline_item, null);
    495         endView.setOnClickListener(new View.OnClickListener() {
    496             @Override
    497             public void onClick(View view) {
    498                 unselectAllTimelineViews();
    499             }
    500         });
    501 
    502         mRightAddClipButton = (ImageButton) endView.findViewById(
    503                 R.id.add_right_media_item_button);
    504         mRightAddClipButton.setOnClickListener(new View.OnClickListener() {
    505             @Override
    506             public void onClick(View view) {
    507                 if (mProject != null) {
    508                     unselectAllTimelineViews();
    509                     // Add a clip at the end of the movie.
    510                     final MovieMediaItem lastMediaItem = mProject.getLastMediaItem();
    511                     if (lastMediaItem != null) {
    512                         mListener.onAddMediaItem(lastMediaItem.getId());
    513                     } else {
    514                         mListener.onAddMediaItem(null);
    515                     }
    516                 }
    517             }
    518         });
    519         addView(endView);
    520 
    521         mLeftHandle = (HandleView)inflate(getContext(), R.layout.left_handle_view, null);
    522         addView(mLeftHandle);
    523 
    524         mRightHandle = (HandleView)inflate(getContext(), R.layout.right_handle_view, null);
    525         addView(mRightHandle);
    526 
    527         mHandleWidth = (int) context.getResources().getDimension(R.dimen.handle_width);
    528 
    529         mTransitionVerticalInset = (int) context.getResources().getDimension(
    530                 R.dimen.timelime_transition_vertical_inset);
    531 
    532         // Compute half the width of the screen (and therefore the parent view).
    533         final Display display = ((Activity) context).getWindowManager().getDefaultDisplay();
    534         mHalfParentWidth = display.getWidth() / 2;
    535 
    536         mHandler = new Handler();
    537 
    538         setMotionEventSplittingEnabled(false);
    539     }
    540 
    541     public MediaLinearLayout(Context context, AttributeSet attrs) {
    542         this(context, attrs, 0);
    543     }
    544 
    545     public MediaLinearLayout(Context context) {
    546         this(context, null, 0);
    547     }
    548 
    549     public void setParentTimelineScrollView(View scrollView) {
    550         mScrollView = scrollView;
    551     }
    552 
    553     /**
    554      * Called when the containing activity is resumed.
    555      */
    556     public void onResume() {
    557         // Invalidate all progress in case the transition generation or
    558         // Ken Burns effect completed while the activity was being paused.
    559         final int childrenCount = getChildCount();
    560         for (int i = 0; i < childrenCount; i++) {
    561             final View childView = getChildAt(i);
    562             final Object item = childView.getTag();
    563             if (item != null) {
    564                 if (item instanceof MovieMediaItem) {
    565                     ((MediaItemView) childView).resetGeneratingEffectProgress();
    566                 } else if (item instanceof MovieTransition) {
    567                     ((TransitionView) childView).resetGeneratingTransitionProgress();
    568                 }
    569             }
    570         }
    571     }
    572 
    573     public void setListener(MediaLinearLayoutListener listener) {
    574         mListener = listener;
    575     }
    576 
    577     public void setProject(VideoEditorProject project) {
    578         closeActionBars();
    579         clearAndHideTrimHandles();
    580         removeAllMediaItemAndTransitionViews();
    581 
    582         mProject = project;
    583     }
    584 
    585     /**
    586      * @param inProgress {@code true} if playback is in progress, false otherwise
    587      */
    588     public void setPlaybackInProgress(boolean inProgress) {
    589         mPlaybackInProgress = inProgress;
    590         setPlaybackState(inProgress);
    591         // Don't allow the user to interact with media items or
    592         // transitions while the playback is in progress.
    593         closeActionBars();
    594     }
    595 
    596     /**
    597      * Returns selected view's position on the timeline; -1 if none.
    598      */
    599     public int getSelectedViewPos() {
    600         return indexOfChild(mSelectedView);
    601     }
    602 
    603     /**
    604      * Selects the view at the specified position; null if it does not exist.
    605      */
    606     public void setSelectedView(int pos) {
    607         if (pos < 0) {
    608             return;
    609         }
    610         mSelectedView = getChildAt(pos);
    611         if (mSelectedView != null) {
    612             select(mSelectedView);
    613         }
    614     }
    615 
    616     /**
    617      * Clears existing media or transition items and adds all given media items.
    618      *
    619      * @param mediaItems The list of media items
    620      */
    621     public void addMediaItems(List<MovieMediaItem> mediaItems) {
    622         closeActionBars();
    623         removeAllMediaItemAndTransitionViews();
    624 
    625         for (MovieMediaItem mediaItem : mediaItems) {
    626             addMediaItem(mediaItem);
    627         }
    628     }
    629 
    630     /**
    631      * Adds a new media item at the end of the timeline.
    632      *
    633      * @param mediaItem The media item
    634      */
    635     private void addMediaItem(MovieMediaItem mediaItem) {
    636         final View mediaItemView = inflate(getContext(), R.layout.media_item, null);
    637         ((MediaItemView) mediaItemView).setGestureListener(mMediaItemGestureListener);
    638         ((MediaItemView) mediaItemView).setProjectPath(mProject.getPath());
    639         mediaItemView.setTag(mediaItem);
    640 
    641         // Add the new view
    642         final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
    643                 LinearLayout.LayoutParams.WRAP_CONTENT,
    644                 LinearLayout.LayoutParams.FILL_PARENT);
    645         // Add the view before the end view, left handle and right handle views.
    646         addView(mediaItemView, getChildCount() - 3, lp);
    647 
    648         // If the new media item has beginning and end transitions, add them.
    649         final MovieTransition beginTransition = mediaItem.getBeginTransition();
    650         if (beginTransition != null) {
    651             final int cc = getChildCount();
    652             // Account for the beginning and end views and the trim handles
    653             if (cc > 5) { // There is a previous view (transition or media item)
    654                 final View view = getChildAt(cc - 5);
    655                 final Object tag = view.getTag();
    656                 // Do not add transition if it already exists
    657                 if (tag != null && tag instanceof MovieMediaItem) {
    658                     final MovieMediaItem prevMediaItem = (MovieMediaItem)tag;
    659                     addTransition(beginTransition, prevMediaItem.getId());
    660                 }
    661             } else { // This is the first media item
    662                 addTransition(beginTransition, null);
    663             }
    664         }
    665 
    666         final MovieTransition endTransition = mediaItem.getEndTransition();
    667         if (endTransition != null) {
    668             addTransition(endTransition, mediaItem.getId());
    669         }
    670 
    671         requestLayout();
    672 
    673         if (mMediaItemActionMode != null) {
    674             mMediaItemActionMode.invalidate();
    675         }
    676 
    677         // Now we can add clips by tapping the beginning view
    678         mLeftAddClipButton.setVisibility(View.VISIBLE);
    679     }
    680 
    681     /**
    682      * Inserts a new media item after the specified media item id.
    683      *
    684      * @param mediaItem The media item
    685      * @param afterMediaItemId The id of the media item preceding the media item
    686      */
    687     public void insertMediaItem(MovieMediaItem mediaItem, String afterMediaItemId) {
    688         final View mediaItemView = inflate(getContext(), R.layout.media_item, null);
    689         ((MediaItemView)mediaItemView).setGestureListener(mMediaItemGestureListener);
    690         ((MediaItemView)mediaItemView).setProjectPath(mProject.getPath());
    691 
    692         mediaItemView.setTag(mediaItem);
    693 
    694         int insertViewIndex;
    695         if (afterMediaItemId != null) {
    696             if ((insertViewIndex = getMediaItemViewIndex(afterMediaItemId)) == -1) {
    697                 Log.e(TAG, "Media item not found: " + afterMediaItemId);
    698                 return;
    699             }
    700 
    701             insertViewIndex++;
    702 
    703             if (insertViewIndex < getChildCount()) {
    704                 final Object tag = getChildAt(insertViewIndex).getTag();
    705                 if (tag != null && tag instanceof MovieTransition) {
    706                     // Remove the transition following the media item
    707                     removeViewAt(insertViewIndex);
    708                 }
    709             }
    710         } else { // Insert at the beginning
    711             // If we have a transition at the beginning remove it
    712             final Object tag = getChildAt(1).getTag();
    713             if (tag != null && tag instanceof MovieTransition) {
    714                 removeViewAt(1);
    715             }
    716 
    717             insertViewIndex = 1;
    718         }
    719 
    720         // Add the new view
    721         final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
    722                 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.FILL_PARENT);
    723         addView(mediaItemView, insertViewIndex, lp);
    724 
    725         // If the new media item has beginning and end transitions add them
    726         final MovieTransition beginTransition = mediaItem.getBeginTransition();
    727         if (beginTransition != null) {
    728             if (insertViewIndex > 1) { // There is a previous view (transition or media item)
    729                 final View view = getChildAt(insertViewIndex - 1);
    730                 final Object tag = view.getTag();
    731                 // Do not add transition if it already exists
    732                 if (tag != null && tag instanceof MovieMediaItem) {
    733                     final MovieMediaItem prevMediaItem = (MovieMediaItem)tag;
    734                     addTransition(beginTransition, prevMediaItem.getId());
    735                 }
    736             } else { // This is the first media item
    737                 addTransition(beginTransition, null);
    738             }
    739         }
    740 
    741         final MovieTransition endTransition = mediaItem.getEndTransition();
    742         if (endTransition != null) {
    743             addTransition(endTransition, mediaItem.getId());
    744         }
    745 
    746         requestLayout();
    747 
    748         if (mMediaItemActionMode != null) {
    749             mMediaItemActionMode.invalidate();
    750         }
    751 
    752         // Now we can add clips by tapping the beginning view
    753         mLeftAddClipButton.setVisibility(View.VISIBLE);
    754     }
    755 
    756     /**
    757      * Updates the specified media item.
    758      *
    759      * @param mediaItem The media item to be updated
    760      */
    761     public void updateMediaItem(MovieMediaItem mediaItem) {
    762         final String mediaItemId = mediaItem.getId();
    763         final int childrenCount = getChildCount();
    764         for (int i = 0; i < childrenCount; i++) {
    765             final View childView = getChildAt(i);
    766             final Object tag = childView.getTag();
    767             if (tag != null && tag instanceof MovieMediaItem) {
    768                 final MovieMediaItem mi = (MovieMediaItem) tag;
    769                 if (mediaItemId.equals(mi.getId())) {
    770                     if (mediaItem != mi) {
    771                         // The media item is a new instance of the media item
    772                         childView.setTag(mediaItem);
    773                         if (mediaItem.getBeginTransition() != null) {
    774                             if (i > 0) {
    775                                 final View tView = getChildAt(i - 1);
    776                                 final Object tagT = tView.getTag();
    777                                 if (tagT != null && tagT instanceof MovieTransition) {
    778                                     tView.setTag(mediaItem.getBeginTransition());
    779                                 }
    780                             }
    781                         }
    782 
    783                         if (mediaItem.getEndTransition() != null) {
    784                             if (i < childrenCount - 1) {
    785                                 final View tView = getChildAt(i + 1);
    786                                 final Object tagT = tView.getTag();
    787                                 if (tagT != null && tagT instanceof MovieTransition) {
    788                                     tView.setTag(mediaItem.getEndTransition());
    789                                 }
    790                             }
    791                         }
    792                     }
    793 
    794                     if (childView.isSelected()) {
    795                         mLeftHandle.setEnabled(true);
    796                         mRightHandle.setEnabled(true);
    797                     }
    798 
    799                     break;
    800                 }
    801             }
    802         }
    803 
    804         requestLayout();
    805 
    806         if (mMediaItemActionMode != null) {
    807             mMediaItemActionMode.invalidate();
    808         }
    809     }
    810 
    811     /**
    812      * Removes a media item view.
    813      *
    814      * @param mediaItemId The media item id
    815      * @param transition The transition inserted at the removal position
    816      *          if a theme is in use.
    817      *
    818      * @return The view which was removed
    819      */
    820     public View removeMediaItem(String mediaItemId, MovieTransition transition) {
    821         final int childrenCount = getChildCount();
    822         MovieMediaItem prevMediaItem = null;
    823         for (int i = 0; i < childrenCount; i++) {
    824             final View childView = getChildAt(i);
    825             final Object tag = childView.getTag();
    826             if (tag != null && tag instanceof MovieMediaItem) {
    827                 final MovieMediaItem mi = (MovieMediaItem)tag;
    828                 if (mediaItemId.equals(mi.getId())) {
    829                     int mediaItemViewIndex = i;
    830 
    831                     // Remove the before transition
    832                     if (mediaItemViewIndex > 0) {
    833                         final Object beforeTag = getChildAt(mediaItemViewIndex - 1).getTag();
    834                         if (beforeTag != null && beforeTag instanceof MovieTransition) {
    835                             // Remove the transition view
    836                             removeViewAt(mediaItemViewIndex - 1);
    837                             mediaItemViewIndex--;
    838                         }
    839                     }
    840 
    841                     // Remove the after transition view
    842                     if (mediaItemViewIndex < getChildCount() - 1) {
    843                         final Object afterTag = getChildAt(mediaItemViewIndex + 1).getTag();
    844                         if (afterTag != null && afterTag instanceof MovieTransition) {
    845                             // Remove the transition view
    846                             removeViewAt(mediaItemViewIndex + 1);
    847                         }
    848                     }
    849 
    850                     // Remove the media item view
    851                     removeViewAt(mediaItemViewIndex);
    852 
    853                     if (transition != null) {
    854                         addTransition(transition,
    855                                 prevMediaItem != null ? prevMediaItem.getId() : null);
    856                     }
    857 
    858                     if (mMediaItemActionMode != null) {
    859                         mMediaItemActionMode.invalidate();
    860                     }
    861 
    862                     if (mProject.getMediaItemCount() == 0) {
    863                         // We cannot add clips by tapping the beginning view
    864                         mLeftAddClipButton.setVisibility(View.GONE);
    865                     }
    866                     return childView;
    867                 }
    868 
    869                 prevMediaItem = mi;
    870             }
    871         }
    872 
    873         return null;
    874     }
    875 
    876     /**
    877      * Creates a new transition.
    878      *
    879      * @param afterMediaItemId Insert the transition after this media item id
    880      * @param transitionType The transition type
    881      * @param transitionDurationMs The transition duration in ms
    882      */
    883     public void addTransition(String afterMediaItemId, int transitionType,
    884             long transitionDurationMs) {
    885         unselectAllTimelineViews();
    886 
    887         final MovieMediaItem afterMediaItem;
    888         if (afterMediaItemId != null) {
    889             afterMediaItem = mProject.getMediaItem(afterMediaItemId);
    890             if (afterMediaItem == null) {
    891                 return;
    892             }
    893         } else {
    894             afterMediaItem = null;
    895         }
    896 
    897         final String id = ApiService.generateId();
    898         switch (transitionType) {
    899             case TransitionType.TRANSITION_TYPE_ALPHA_CONTOUR: {
    900                 ApiService.insertAlphaTransition(getContext(), mProject.getPath(),
    901                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR,
    902                         R.raw.mask_contour, 50, false);
    903                 break;
    904             }
    905 
    906             case TransitionType.TRANSITION_TYPE_ALPHA_DIAGONAL: {
    907                 ApiService.insertAlphaTransition(getContext(), mProject.getPath(),
    908                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR,
    909                         R.raw.mask_diagonal, 50, false);
    910                 break;
    911             }
    912 
    913             case TransitionType.TRANSITION_TYPE_CROSSFADE: {
    914                 ApiService.insertCrossfadeTransition(getContext(), mProject.getPath(),
    915                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR);
    916                 break;
    917             }
    918 
    919             case TransitionType.TRANSITION_TYPE_FADE_BLACK: {
    920                 ApiService.insertFadeBlackTransition(getContext(), mProject.getPath(),
    921                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR);
    922                 break;
    923             }
    924 
    925             case TransitionType.TRANSITION_TYPE_SLIDING_RIGHT_OUT_LEFT_IN: {
    926                 ApiService.insertSlidingTransition(getContext(), mProject.getPath(),
    927                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR,
    928                         TransitionSliding.DIRECTION_RIGHT_OUT_LEFT_IN);
    929                 break;
    930             }
    931 
    932             case TransitionType.TRANSITION_TYPE_SLIDING_LEFT_OUT_RIGHT_IN: {
    933                 ApiService.insertSlidingTransition(getContext(), mProject.getPath(),
    934                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR,
    935                         TransitionSliding.DIRECTION_LEFT_OUT_RIGHT_IN);
    936                 break;
    937             }
    938 
    939             case TransitionType.TRANSITION_TYPE_SLIDING_TOP_OUT_BOTTOM_IN: {
    940                 ApiService.insertSlidingTransition(getContext(), mProject.getPath(),
    941                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR,
    942                         TransitionSliding.DIRECTION_TOP_OUT_BOTTOM_IN);
    943                 break;
    944             }
    945 
    946             case TransitionType.TRANSITION_TYPE_SLIDING_BOTTOM_OUT_TOP_IN: {
    947                 ApiService.insertSlidingTransition(getContext(), mProject.getPath(),
    948                         afterMediaItemId, id, transitionDurationMs, Transition.BEHAVIOR_LINEAR,
    949                         TransitionSliding.DIRECTION_BOTTOM_OUT_TOP_IN);
    950                 break;
    951             }
    952 
    953             default: {
    954                 break;
    955             }
    956         }
    957 
    958         if (mMediaItemActionMode != null) {
    959             mMediaItemActionMode.invalidate();
    960         }
    961     }
    962 
    963     /**
    964      * Edits a transition.
    965      *
    966      * @param afterMediaItemId Insert the transition after this media item id
    967      * @param transitionId The transition id
    968      * @param transitionType The transition type
    969      * @param transitionDurationMs The transition duration in ms
    970      */
    971     public void editTransition(String afterMediaItemId, String transitionId, int transitionType,
    972             long transitionDurationMs) {
    973         final MovieTransition transition = mProject.getTransition(transitionId);
    974         if (transition == null) {
    975             return;
    976         }
    977 
    978         // Check if the type or duration had changed
    979         if (transition.getType() != transitionType) {
    980             // Remove the transition and add it again
    981             ApiService.removeTransition(getContext(), mProject.getPath(), transitionId);
    982             addTransition(afterMediaItemId, transitionType, transitionDurationMs);
    983         } else if (transition.getAppDuration() != transitionDurationMs) {
    984             transition.setAppDuration(transitionDurationMs);
    985             ApiService.setTransitionDuration(getContext(), mProject.getPath(), transitionId,
    986                     transitionDurationMs);
    987         }
    988 
    989         if (mMediaItemActionMode != null) {
    990             mMediaItemActionMode.invalidate();
    991         }
    992     }
    993 
    994     /**
    995      * Adds a new transition after the specified media id. This method assumes that a
    996      * transition does not exist at the insertion point.
    997      *
    998      * @param transition The transition to be added
    999      * @param afterMediaItemId After the specified media item id
   1000      *
   1001      * @return The transition view that was added, {@code null} upon errors.
   1002      */
   1003     public View addTransition(MovieTransition transition, String afterMediaItemId) {
   1004         // Determine the insert position
   1005         int index;
   1006         if (afterMediaItemId != null) {
   1007             index = -1;
   1008             final int childrenCount = getChildCount();
   1009             for (int i = 0; i < childrenCount; i++) {
   1010                 final Object tag = getChildAt(i).getTag();
   1011                 if (tag != null && tag instanceof MovieMediaItem) {
   1012                     final MovieMediaItem mi = (MovieMediaItem) tag;
   1013                     if (afterMediaItemId.equals(mi.getId())) {
   1014                         index = i + 1;
   1015                         break;
   1016                     }
   1017                 }
   1018             }
   1019 
   1020             if (index < 0) {
   1021                 Log.e(TAG, "addTransition media item not found: " + afterMediaItemId);
   1022                 return null;
   1023             }
   1024         } else {
   1025             index = 1;
   1026         }
   1027 
   1028         final View transitionView = inflate(getContext(), R.layout.transition_view, null);
   1029         ((TransitionView) transitionView).setGestureListener(mTransitionGestureListener);
   1030         ((TransitionView) transitionView).setProjectPath(mProject.getPath());
   1031         transitionView.setTag(transition);
   1032 
   1033         final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
   1034                 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.FILL_PARENT);
   1035         addView(transitionView, index, lp);
   1036 
   1037         // Adjust the size of all the views
   1038         requestLayout();
   1039 
   1040         // If this transition was added by the user invalidate the menu item
   1041         if (mMediaItemActionMode != null) {
   1042             mMediaItemActionMode.invalidate();
   1043         }
   1044 
   1045         return transitionView;
   1046     }
   1047 
   1048     /**
   1049      * Updates a transition.
   1050      *
   1051      * @param transitionId The transition id
   1052      */
   1053     public void updateTransition(String transitionId) {
   1054         requestLayout();
   1055         invalidate();
   1056     }
   1057 
   1058     /**
   1059      * Removes a transition with the specified id.
   1060      *
   1061      * @param transitionId The transition id
   1062      */
   1063     public void removeTransition(String transitionId) {
   1064         final int childrenCount = getChildCount();
   1065         for (int i = 0; i < childrenCount; i++) {
   1066             final Object tag = getChildAt(i).getTag();
   1067             if (tag != null && tag instanceof MovieTransition) {
   1068                 final MovieTransition transition = (MovieTransition)tag;
   1069                 if (transitionId.equals(transition.getId())) {
   1070                     // Remove the view
   1071                     removeViewAt(i);
   1072 
   1073                     // Adjust the size of all the views
   1074                     requestLayout();
   1075 
   1076                     // If this transition was removed by the user invalidate the menu item
   1077                     if (mMediaItemActionMode != null) {
   1078                         mMediaItemActionMode.invalidate();
   1079                     }
   1080 
   1081                     return;
   1082                 }
   1083             }
   1084         }
   1085     }
   1086 
   1087     /**
   1088      * Invalidates the available action modes. Used to refresh menu contents.
   1089      */
   1090     public void invalidateActionBar() {
   1091         if (mMediaItemActionMode != null) {
   1092             mMediaItemActionMode.invalidate();
   1093         }
   1094         if (mTransitionActionMode != null) {
   1095             mTransitionActionMode.invalidate();
   1096         }
   1097     }
   1098 
   1099     /**
   1100      * A Ken Burns movie is encoded for an MediaImageItem.
   1101      *
   1102      * @param mediaItemId The media item id
   1103      * @param action The action
   1104      * @param progress Progress value (between 0..100)
   1105      */
   1106     public void onGeneratePreviewMediaItemProgress(String mediaItemId, int action, int progress) {
   1107         // Display the progress while generating the Ken Burns video clip
   1108         final MediaItemView view = (MediaItemView) getMediaItemView(mediaItemId);
   1109         if (view != null) {
   1110             view.setGeneratingEffectProgress(progress);
   1111 
   1112             if (view.isSelected()) {
   1113                 if (progress == 0) {
   1114                     mLeftHandle.setEnabled(false);
   1115                     mRightHandle.setEnabled(false);
   1116                 } else if (progress == 100) {
   1117                     mLeftHandle.setEnabled(true);
   1118                     mRightHandle.setEnabled(true);
   1119                 }
   1120             }
   1121         }
   1122     }
   1123 
   1124     /**
   1125      * A transition is being encoded.
   1126      *
   1127      * @param transitionId The transition id
   1128      * @param action The action
   1129      * @param progress The progress
   1130      */
   1131     public void onGeneratePreviewTransitionProgress(String transitionId, int action,
   1132             int progress) {
   1133         // Display the progress while generating the transition
   1134         final TransitionView view = (TransitionView) getTransitionView(transitionId);
   1135         if (view != null) {
   1136             view.setGeneratingTransitionProgress(progress);
   1137 
   1138             if (view.isSelected()) {
   1139                 if (progress == 0) {
   1140                     mLeftHandle.setEnabled(false);
   1141                     mRightHandle.setEnabled(false);
   1142                 } else if (progress == 100) {
   1143                     mLeftHandle.setEnabled(true);
   1144                     mRightHandle.setEnabled(true);
   1145                 }
   1146             }
   1147         }
   1148     }
   1149 
   1150     /**
   1151      * Creates a new effect on the specified media item.
   1152      *
   1153      * @param effectType The effect type
   1154      * @param mediaItemId Add the effect for this media item id
   1155      * @param startRect The start rectangle
   1156      * @param endRect The end rectangle
   1157      */
   1158     public void addEffect(int effectType, String mediaItemId, Rect startRect, Rect endRect) {
   1159         final MovieMediaItem mediaItem = mProject.getMediaItem(mediaItemId);
   1160         if (mediaItem == null) {
   1161             Log.e(TAG, "addEffect media item not found: " + mediaItemId);
   1162             return;
   1163         }
   1164 
   1165         final String id = ApiService.generateId();
   1166         switch (effectType) {
   1167             case EffectType.EFFECT_KEN_BURNS: {
   1168                 ApiService.addEffectKenBurns(getContext(), mProject.getPath(), mediaItemId,
   1169                         id, 0, mediaItem.getDuration(), startRect, endRect);
   1170                 break;
   1171             }
   1172 
   1173             case EffectType.EFFECT_COLOR_GRADIENT: {
   1174                 ApiService.addEffectColor(getContext(), mProject.getPath(), mediaItemId, id, 0,
   1175                         mediaItem.getDuration(), EffectColor.TYPE_GRADIENT,
   1176                         EffectColor.GRAY);
   1177                 break;
   1178             }
   1179 
   1180             case EffectType.EFFECT_COLOR_SEPIA: {
   1181                 ApiService.addEffectColor(getContext(), mProject.getPath(), mediaItemId, id, 0,
   1182                         mediaItem.getDuration(), EffectColor.TYPE_SEPIA, 0);
   1183                 break;
   1184             }
   1185 
   1186             case EffectType.EFFECT_COLOR_NEGATIVE: {
   1187                 ApiService.addEffectColor(getContext(), mProject.getPath(), mediaItemId, id, 0,
   1188                         mediaItem.getDuration(), EffectColor.TYPE_NEGATIVE, 0);
   1189                 break;
   1190             }
   1191 
   1192             default: {
   1193                 break;
   1194             }
   1195         }
   1196 
   1197         if (mMediaItemActionMode != null) {
   1198             mMediaItemActionMode.invalidate();
   1199         }
   1200     }
   1201 
   1202     /**
   1203      * Set the media item thumbnail.
   1204      *
   1205      * @param mediaItemId The media item id
   1206      * @param bitmap The bitmap
   1207      * @param index The index of the bitmap
   1208      * @param token The token given in the original request
   1209      *
   1210      * @return true if the bitmap is used
   1211      */
   1212     public boolean setMediaItemThumbnail(
   1213             String mediaItemId, Bitmap bitmap, int index, int token) {
   1214         final int childrenCount = getChildCount();
   1215         for (int i = 0; i < childrenCount; i++) {
   1216             final Object tag = getChildAt(i).getTag();
   1217             if (tag != null && tag instanceof MovieMediaItem) {
   1218                 final MovieMediaItem mi = (MovieMediaItem)tag;
   1219                 if (mediaItemId.equals(mi.getId())) {
   1220                     return ((MediaItemView)getChildAt(i)).setBitmap(
   1221                             bitmap, index, token);
   1222                 }
   1223             }
   1224         }
   1225 
   1226         return false;
   1227     }
   1228 
   1229     /**
   1230      * Sets the transition thumbnails.
   1231      *
   1232      * @param transitionId The transition id
   1233      * @param bitmaps The bitmaps array
   1234      *
   1235      * @return true if the bitmaps were used
   1236      */
   1237     public boolean setTransitionThumbnails(String transitionId, Bitmap[] bitmaps) {
   1238         final int childrenCount = getChildCount();
   1239         for (int i = 0; i < childrenCount; i++) {
   1240             final Object tag = getChildAt(i).getTag();
   1241             if (tag != null && tag instanceof MovieTransition) {
   1242                 final MovieTransition transition = (MovieTransition)tag;
   1243                 if (transitionId.equals(transition.getId())) {
   1244                     return ((TransitionView)getChildAt(i)).setBitmaps(bitmaps);
   1245                 }
   1246             }
   1247         }
   1248 
   1249         return false;
   1250     }
   1251 
   1252     @Override
   1253     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1254         // Compute the total duration of the project.
   1255         final long totalDurationMs = mProject.computeDuration();
   1256 
   1257         // Total available width for putting media items and transitions.
   1258         // We subtract 2 half screen widths from the width because we put
   1259         // 2 empty view at the beginning and end of the timeline, each with
   1260         // half screen width. We then layout each child view into the
   1261         // available width.
   1262         final int viewWidth = getWidth() - (2 * mHalfParentWidth);
   1263 
   1264         // If we are in trimming mode, the left view width might be different
   1265         // due to trimming; otherwise it equals half of screen width.
   1266         final int leftViewWidth = (mSelectedView != null) ?
   1267                 (Integer) mScrollView.getTag(R.id.left_view_width) : mHalfParentWidth;
   1268 
   1269         // Top and bottom position are fixed for media item views. For transition views,
   1270         // there is additional inset which makes them smaller. See below.
   1271         final int top = getPaddingTop();
   1272         final int bottom = b - t;
   1273 
   1274         long startMs = 0;
   1275         int left = 0;
   1276 
   1277         final int childrenCount = getChildCount();
   1278         for (int i = 0; i < childrenCount; i++) {
   1279             final View view = getChildAt(i);
   1280             final Object tag = view.getTag();
   1281             if (tag != null) {
   1282                 final long durationMs = computeViewDuration(view);
   1283 
   1284                 final int right = (int)((float)((startMs + durationMs) * viewWidth) /
   1285                         (float)totalDurationMs) + leftViewWidth;
   1286 
   1287                 if (tag instanceof MovieMediaItem) {
   1288                     if (left != view.getLeft() || right != view.getRight()) {
   1289                         final int oldLeft = view.getLeft();
   1290                         final int oldRight = view.getRight();
   1291                         view.layout(left, top, right, bottom);
   1292                         ((MediaItemView) view).onLayoutPerformed(oldLeft, oldRight);
   1293                     } else {
   1294                         view.layout(left, top, right, bottom);
   1295                     }
   1296                 } else {  // Transition view.
   1297                     // Note that we set additional inset so it looks smaller
   1298                     // than media item views on the timeline.
   1299                     view.layout(left,
   1300                             top + mTransitionVerticalInset,
   1301                             right,
   1302                             bottom - mTransitionVerticalInset);
   1303                 }
   1304 
   1305                 startMs += durationMs;
   1306                 left = right;
   1307             } else if (view == mLeftHandle && mSelectedView != null) {
   1308                 // We are in trimming mode, the left handle must be shown.
   1309                 view.layout(mSelectedView.getLeft() - mHandleWidth,
   1310                         top + mSelectedView.getPaddingTop(),
   1311                         mSelectedView.getLeft(),
   1312                         bottom - mSelectedView.getPaddingBottom());
   1313             } else if (view == mRightHandle && mSelectedView != null) {
   1314                 // We are in trimming mode, the right handle must be shown.
   1315                 view.layout(mSelectedView.getRight(),
   1316                         top + mSelectedView.getPaddingTop(),
   1317                         mSelectedView.getRight() + mHandleWidth,
   1318                         bottom - mSelectedView.getPaddingBottom());
   1319             } else if (i == 0) {  // Begin view
   1320                 view.layout(0, top, leftViewWidth, bottom);
   1321                 left += leftViewWidth;
   1322             } else {  // End view
   1323                 view.layout(left, top, getWidth(), bottom);
   1324             }
   1325         }
   1326         mMoveLayoutPending = false;
   1327     }
   1328 
   1329     /**
   1330      * Computes the duration of the specified view.
   1331      *
   1332      * @param view The specified view
   1333      *
   1334      * @return The duration in milliseconds, 0 if the specified view is not a media item view
   1335      *         or a transition view
   1336      */
   1337     private long computeViewDuration(View view) {
   1338         long durationMs;
   1339         final Object tag = view.getTag();
   1340         if (tag != null) {
   1341             if (tag instanceof MovieMediaItem) {
   1342                 final MovieMediaItem mediaItem = (MovieMediaItem) view.getTag();
   1343                 durationMs = mediaItem.getAppTimelineDuration();
   1344                 if (mediaItem.getBeginTransition() != null) {
   1345                     durationMs -= mediaItem.getBeginTransition().getAppDuration();
   1346                 }
   1347 
   1348                 if (mediaItem.getEndTransition() != null) {
   1349                     durationMs -= mediaItem.getEndTransition().getAppDuration();
   1350                 }
   1351             } else {  // Transition
   1352                 final MovieTransition transition = (MovieTransition) tag;
   1353                 durationMs = transition.getAppDuration();
   1354             }
   1355         } else {
   1356             durationMs = 0;
   1357         }
   1358 
   1359         return durationMs;
   1360     }
   1361 
   1362     /**
   1363      * Creates a new dialog.
   1364      *
   1365      * @param id The dialog id
   1366      * @param bundle The dialog bundle
   1367      *
   1368      * @return The dialog
   1369      */
   1370     public Dialog onCreateDialog(int id, final Bundle bundle) {
   1371         // If the project is not yet loaded do nothing.
   1372         if (mProject == null) {
   1373             return null;
   1374         }
   1375 
   1376         switch (id) {
   1377             case VideoEditorActivity.DIALOG_REMOVE_MEDIA_ITEM_ID: {
   1378                 final MovieMediaItem mediaItem = mProject.getMediaItem(
   1379                         bundle.getString(PARAM_DIALOG_MEDIA_ITEM_ID));
   1380                 if (mediaItem == null) {
   1381                     return null;
   1382                 }
   1383 
   1384                 final Activity activity = (Activity) getContext();
   1385                 return AlertDialogs.createAlert(activity,
   1386                         FileUtils.getSimpleName(mediaItem.getFilename()),
   1387                         0, mediaItem.isVideoClip() ?
   1388                                 activity.getString(R.string.editor_remove_video_question) :
   1389                                     activity.getString(R.string.editor_remove_image_question),
   1390                         activity.getString(R.string.yes),
   1391                         new DialogInterface.OnClickListener() {
   1392                     @Override
   1393                     public void onClick(DialogInterface dialog, int which) {
   1394                         if (mMediaItemActionMode != null) {
   1395                             mMediaItemActionMode.finish();
   1396                             mMediaItemActionMode = null;
   1397                         }
   1398                         unselectAllTimelineViews();
   1399 
   1400                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_MEDIA_ITEM_ID);
   1401 
   1402                         ApiService.removeMediaItem(activity, mProject.getPath(), mediaItem.getId(),
   1403                                 mProject.getTheme());
   1404                     }
   1405                 }, activity.getString(R.string.no), new DialogInterface.OnClickListener() {
   1406                     @Override
   1407                     public void onClick(DialogInterface dialog, int which) {
   1408                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_MEDIA_ITEM_ID);
   1409                     }
   1410                 }, new DialogInterface.OnCancelListener() {
   1411                     @Override
   1412                     public void onCancel(DialogInterface dialog) {
   1413                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_MEDIA_ITEM_ID);
   1414                     }
   1415                 }, true);
   1416             }
   1417 
   1418             case VideoEditorActivity.DIALOG_CHANGE_RENDERING_MODE_ID: {
   1419                 final MovieMediaItem mediaItem = mProject.getMediaItem(
   1420                         bundle.getString(PARAM_DIALOG_MEDIA_ITEM_ID));
   1421                 if (mediaItem == null) {
   1422                     return null;
   1423                 }
   1424 
   1425                 final Activity activity = (Activity)getContext();
   1426                 final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
   1427                 builder.setTitle(activity.getString(R.string.editor_change_rendering_mode));
   1428                 final CharSequence[] renderingModeStrings = new CharSequence[3];
   1429                 renderingModeStrings[0] = getContext().getString(R.string.rendering_mode_black_borders);
   1430                 renderingModeStrings[1] = getContext().getString(R.string.rendering_mode_stretch);
   1431                 renderingModeStrings[2] = getContext().getString(R.string.rendering_mode_crop);
   1432 
   1433                 final int currentRenderingMode = bundle.getInt(PARAM_DIALOG_CURRENT_RENDERING_MODE);
   1434                 final int currentRenderingModeIndex;
   1435                 switch (currentRenderingMode) {
   1436                     case MediaItem.RENDERING_MODE_CROPPING: {
   1437                         currentRenderingModeIndex = 2;
   1438                         break;
   1439                     }
   1440 
   1441                     case MediaItem.RENDERING_MODE_STRETCH: {
   1442                         currentRenderingModeIndex = 1;
   1443                         break;
   1444                     }
   1445 
   1446                     case MediaItem.RENDERING_MODE_BLACK_BORDER:
   1447                     default: {
   1448                         currentRenderingModeIndex = 0;
   1449                         break;
   1450                     }
   1451                 }
   1452 
   1453                 builder.setSingleChoiceItems(renderingModeStrings, currentRenderingModeIndex,
   1454                         new DialogInterface.OnClickListener() {
   1455                     @Override
   1456                     public void onClick(DialogInterface dialog, int which) {
   1457                         switch (which) {
   1458                             case 0: {
   1459                                 mediaItem.setAppRenderingMode(MediaItem.RENDERING_MODE_BLACK_BORDER);
   1460                                 ApiService.setMediaItemRenderingMode(getContext(),
   1461                                         mProject.getPath(), mediaItem.getId(),
   1462                                         MediaItem.RENDERING_MODE_BLACK_BORDER);
   1463                                 break;
   1464                             }
   1465 
   1466                             case 1: {
   1467                                 mediaItem.setAppRenderingMode(MediaItem.RENDERING_MODE_STRETCH);
   1468                                 ApiService.setMediaItemRenderingMode(getContext(),
   1469                                         mProject.getPath(),
   1470                                         mediaItem.getId(), MediaItem.RENDERING_MODE_STRETCH);
   1471                                 break;
   1472                             }
   1473 
   1474                             case 2: {
   1475                                 mediaItem.setAppRenderingMode(MediaItem.RENDERING_MODE_CROPPING);
   1476                                 ApiService.setMediaItemRenderingMode(getContext(),
   1477                                         mProject.getPath(),
   1478                                         mediaItem.getId(), MediaItem.RENDERING_MODE_CROPPING);
   1479                                 break;
   1480                             }
   1481 
   1482                             default: {
   1483                                 break;
   1484                             }
   1485                         }
   1486                         activity.removeDialog(VideoEditorActivity.DIALOG_CHANGE_RENDERING_MODE_ID);
   1487                     }
   1488                 });
   1489                 builder.setCancelable(true);
   1490                 builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
   1491                     @Override
   1492                     public void onCancel(DialogInterface dialog) {
   1493                         activity.removeDialog(VideoEditorActivity.DIALOG_CHANGE_RENDERING_MODE_ID);
   1494                     }
   1495                 });
   1496                 return builder.create();
   1497             }
   1498 
   1499             case VideoEditorActivity.DIALOG_REMOVE_TRANSITION_ID: {
   1500                 final MovieTransition transition = mProject.getTransition(
   1501                         bundle.getString(PARAM_DIALOG_TRANSITION_ID));
   1502                 if (transition == null) {
   1503                     return null;
   1504                 }
   1505 
   1506                 final Activity activity = (Activity) getContext();
   1507                 return AlertDialogs.createAlert(activity,
   1508                         activity.getString(R.string.remove),
   1509                         0, activity.getString(R.string.editor_remove_transition_question),
   1510                         activity.getString(R.string.yes),
   1511                         new DialogInterface.OnClickListener() {
   1512                     @Override
   1513                     public void onClick(DialogInterface dialog, int which) {
   1514                         if (mTransitionActionMode != null) {
   1515                             mTransitionActionMode.finish();
   1516                             mTransitionActionMode = null;
   1517                         }
   1518                         unselectAllTimelineViews();
   1519                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_TRANSITION_ID);
   1520 
   1521                         ApiService.removeTransition(activity, mProject.getPath(),
   1522                                 transition.getId());
   1523                     }
   1524                 }, activity.getString(R.string.no), new DialogInterface.OnClickListener() {
   1525                     @Override
   1526                     public void onClick(DialogInterface dialog, int which) {
   1527                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_TRANSITION_ID);
   1528                     }
   1529                 }, new DialogInterface.OnCancelListener() {
   1530                     @Override
   1531                     public void onCancel(DialogInterface dialog) {
   1532                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_TRANSITION_ID);
   1533                     }
   1534                 }, true);
   1535             }
   1536 
   1537             case VideoEditorActivity.DIALOG_REMOVE_EFFECT_ID: {
   1538                 final MovieMediaItem mediaItem = mProject.getMediaItem(
   1539                         bundle.getString(PARAM_DIALOG_MEDIA_ITEM_ID));
   1540                 if (mediaItem == null) {
   1541                     return null;
   1542                 }
   1543 
   1544                 final Activity activity = (Activity) getContext();
   1545                 return AlertDialogs.createAlert(activity,
   1546                         FileUtils.getSimpleName(mediaItem.getFilename()),
   1547                         0, activity.getString(R.string.editor_remove_effect_question),
   1548                         activity.getString(R.string.yes),
   1549                         new DialogInterface.OnClickListener() {
   1550                     @Override
   1551                     public void onClick(DialogInterface dialog, int which) {
   1552                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_EFFECT_ID);
   1553 
   1554                         ApiService.removeEffect(activity, mProject.getPath(),
   1555                                 mediaItem.getId(), mediaItem.getEffect().getId());
   1556 
   1557                         if (mMediaItemActionMode != null) {
   1558                             mMediaItemActionMode.invalidate();
   1559                         }
   1560                     }
   1561                 }, activity.getString(R.string.no), new DialogInterface.OnClickListener() {
   1562                     @Override
   1563                     public void onClick(DialogInterface dialog, int which) {
   1564                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_EFFECT_ID);
   1565                     }
   1566                 }, new DialogInterface.OnCancelListener() {
   1567                     @Override
   1568                     public void onCancel(DialogInterface dialog) {
   1569                         activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_EFFECT_ID);
   1570                     }
   1571                 }, true);
   1572             }
   1573 
   1574             default: {
   1575                 return null;
   1576             }
   1577         }
   1578     }
   1579 
   1580     @Override
   1581     public boolean onDragEvent(DragEvent event) {
   1582         boolean result = false;
   1583         switch (event.getAction()) {
   1584             case DragEvent.ACTION_DRAG_STARTED: {
   1585                 // Claim to accept any dragged content
   1586                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1587                     Log.v(TAG, "ACTION_DRAG_STARTED: " + event);
   1588                 }
   1589 
   1590                 mDragMediaItemId = (String)event.getLocalState();
   1591 
   1592                 // Hide the handles while dragging
   1593                 mLeftHandle.setVisibility(View.GONE);
   1594                 mRightHandle.setVisibility(View.GONE);
   1595 
   1596                 mDropAfterMediaItem = null;
   1597                 mDropIndex = -1;
   1598 
   1599                 mFirstEntered = true;
   1600                 // This view accepts drag
   1601                 result = true;
   1602                 break;
   1603             }
   1604 
   1605             case DragEvent.ACTION_DRAG_ENTERED: {
   1606                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1607                     Log.v(TAG, "ACTION_DRAG_ENTERED: " + event);
   1608                 }
   1609 
   1610                 if (!mFirstEntered && mDropIndex >= 0) {
   1611                     mScrollView.setTag(R.id.playhead_type,
   1612                             TimelineHorizontalScrollView.PLAYHEAD_MOVE_OK);
   1613                 } else {
   1614                     mScrollView.setTag(R.id.playhead_type,
   1615                             TimelineHorizontalScrollView.PLAYHEAD_MOVE_NOT_OK);
   1616                 }
   1617                 mScrollView.invalidate();
   1618 
   1619                 mFirstEntered = false;
   1620                 break;
   1621             }
   1622 
   1623             case DragEvent.ACTION_DRAG_EXITED: {
   1624                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1625                     Log.v(TAG, "ACTION_DRAG_EXITED: " + event);
   1626                 }
   1627 
   1628                 // Redraw the "normal playhead"
   1629                 mScrollView.setTag(R.id.playhead_type, TimelineHorizontalScrollView.PLAYHEAD_NORMAL);
   1630                 mScrollView.invalidate();
   1631                 break;
   1632             }
   1633 
   1634             case DragEvent.ACTION_DRAG_ENDED: {
   1635                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1636                     Log.v(TAG, "ACTION_DRAG_ENDED: " + event);
   1637                 }
   1638 
   1639                 mDragMediaItemId = null;
   1640                 mDropIndex = -1;
   1641 
   1642                 // Hide the handles while dragging
   1643                 mLeftHandle.setVisibility(View.VISIBLE);
   1644                 mRightHandle.setVisibility(View.VISIBLE);
   1645 
   1646                 // Redraw the "normal playhead"
   1647                 mScrollView.setTag(R.id.playhead_type, TimelineHorizontalScrollView.PLAYHEAD_NORMAL);
   1648                 mScrollView.invalidate();
   1649 
   1650                 requestLayout();
   1651                 break;
   1652             }
   1653 
   1654             case DragEvent.ACTION_DRAG_LOCATION: {
   1655                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1656                     Log.v(TAG, "ACTION_DRAG_LOCATION: " + event);
   1657                 }
   1658 
   1659                 moveToPosition(event.getX());
   1660                 // We returned true to DRAG_STARTED, so return true here
   1661                 result = true;
   1662                 break;
   1663             }
   1664 
   1665             case DragEvent.ACTION_DROP: {
   1666                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1667                     Log.v(TAG, "ACTION_DROP: " + event);
   1668                 }
   1669 
   1670                 if (mDropIndex >= 0) {
   1671                     final String afterMediaItemId =
   1672                         mDropAfterMediaItem != null ? mDropAfterMediaItem.getId() : null;
   1673                     if (Log.isLoggable(TAG, Log.DEBUG)) {
   1674                         Log.d(TAG, "ACTION_DROP: Index: " + mDropIndex + " | " + afterMediaItemId);
   1675                     }
   1676                     ApiService.moveMediaItem(getContext(), mProject.getPath(), mDragMediaItemId,
   1677                             afterMediaItemId, null);
   1678                 }
   1679                 result = true;
   1680                 break;
   1681             }
   1682 
   1683 
   1684             default: {
   1685                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1686                     Log.v(TAG, "Other drag event: " + event);
   1687                 }
   1688                 result = true;
   1689                 break;
   1690             }
   1691         }
   1692 
   1693         return result;
   1694     }
   1695 
   1696     /**
   1697      * Move the playhead during a move operation
   1698      *
   1699      * @param eventX The event horizontal position
   1700      */
   1701     private void moveToPosition(float eventX) {
   1702         final int x = (int)eventX - mScrollView.getScrollX();
   1703         final long now = System.currentTimeMillis();
   1704         if (now - mPrevDragScrollTime > 300) {
   1705             if (x < mPrevDragPosition - 42) { // Backwards
   1706                 final long positionMs = getLeftDropPosition();
   1707                 if (mDropIndex >= 0) {
   1708                     // Redraw the "move ok playhead"
   1709                     mScrollView.setTag(R.id.playhead_type,
   1710                             TimelineHorizontalScrollView.PLAYHEAD_MOVE_OK);
   1711                 } else {
   1712                     // Redraw the "move not ok playhead"
   1713                     mScrollView.setTag(R.id.playhead_type,
   1714                             TimelineHorizontalScrollView.PLAYHEAD_MOVE_NOT_OK);
   1715                 }
   1716 
   1717                 mListener.onRequestMovePlayhead(positionMs, true);
   1718                 mScrollView.invalidate();
   1719 
   1720                 mPrevDragPosition = x;
   1721                 mPrevDragScrollTime = now;
   1722             } else if (x > mPrevDragPosition + 42) { // Forward
   1723                 final long positionMs = getRightDropPosition();
   1724                 if (mDropIndex >= 0) {
   1725                     // Redraw the "move ok playhead"
   1726                     mScrollView.setTag(R.id.playhead_type,
   1727                             TimelineHorizontalScrollView.PLAYHEAD_MOVE_OK);
   1728                 } else {
   1729                     // Redraw the "move not ok playhead"
   1730                     mScrollView.setTag(R.id.playhead_type,
   1731                             TimelineHorizontalScrollView.PLAYHEAD_MOVE_NOT_OK);
   1732                 }
   1733 
   1734                 mListener.onRequestMovePlayhead(positionMs, true);
   1735                 mScrollView.invalidate();
   1736 
   1737                 mPrevDragPosition = x;
   1738                 mPrevDragScrollTime = now;
   1739             }
   1740         } else {
   1741             mPrevDragPosition = x;
   1742         }
   1743     }
   1744 
   1745     // Returns the begin time of a media item (exclude transition).
   1746     private long getBeginTime(MovieMediaItem item) {
   1747         final List<MovieMediaItem> mediaItems = mProject.getMediaItems();
   1748         long beginMs = 0;
   1749         final int mediaItemsCount = mediaItems.size();
   1750         for (int i = 0; i < mediaItemsCount; i++) {
   1751             final MovieMediaItem mediaItem = mediaItems.get(i);
   1752             final MovieTransition beginTransition = mediaItem.getBeginTransition();
   1753             final MovieTransition endTransition = mediaItem.getEndTransition();
   1754 
   1755             if (item.getId().equals(mediaItem.getId())) {
   1756                 if (beginTransition != null) {
   1757                     beginMs += beginTransition.getAppDuration();
   1758                 }
   1759                 return beginMs;
   1760             }
   1761 
   1762             beginMs += mediaItem.getAppTimelineDuration();
   1763 
   1764             if (endTransition != null) {
   1765                 beginMs -= endTransition.getAppDuration();
   1766             }
   1767         }
   1768 
   1769         return 0;
   1770     }
   1771 
   1772     // Returns the end time of a media item (exclude transition)
   1773     private long getEndTime(MovieMediaItem item) {
   1774         final List<MovieMediaItem> mediaItems = mProject.getMediaItems();
   1775         long endMs = 0;
   1776         final int mediaItemsCount = mediaItems.size();
   1777         for (int i = 0; i < mediaItemsCount; i++) {
   1778             final MovieMediaItem mediaItem = mediaItems.get(i);
   1779             final MovieTransition beginTransition = mediaItem.getBeginTransition();
   1780             final MovieTransition endTransition = mediaItem.getEndTransition();
   1781 
   1782             endMs += mediaItem.getAppTimelineDuration();
   1783 
   1784             if (endTransition != null) {
   1785                 endMs -= endTransition.getAppDuration();
   1786             }
   1787 
   1788             if (item.getId().equals(mediaItem.getId())) {
   1789                 return endMs;
   1790             }
   1791 
   1792         }
   1793 
   1794         return 0;
   1795     }
   1796 
   1797     /**
   1798      * @return The valid time location of the drop (-1 if none)
   1799      */
   1800     private long getLeftDropPosition() {
   1801         final List<MovieMediaItem> mediaItems = mProject.getMediaItems();
   1802         long beginMs = 0;
   1803         long endMs = 0;
   1804         long timeMs = mProject.getPlayheadPos();
   1805 
   1806         final int mediaItemsCount = mediaItems.size();
   1807         for (int i = 0; i < mediaItemsCount; i++) {
   1808             final MovieMediaItem mediaItem = mediaItems.get(i);
   1809 
   1810             endMs = beginMs + mediaItem.getAppTimelineDuration();
   1811 
   1812             if (mediaItem.getEndTransition() != null) {
   1813                 if (i < mediaItemsCount - 1) {
   1814                     endMs -= mediaItem.getEndTransition().getAppDuration();
   1815                 }
   1816             }
   1817 
   1818             if (timeMs > beginMs && timeMs <= endMs) {
   1819                 if (mediaItem.getBeginTransition() != null) {
   1820                     beginMs += mediaItem.getBeginTransition().getAppDuration();
   1821                 }
   1822 
   1823                 if (!mDragMediaItemId.equals(mediaItem.getId())) {
   1824                     if (i > 0) {
   1825                         // Check if the previous item is the drag item
   1826                         final MovieMediaItem prevMediaItem = mediaItems.get(i - 1);
   1827                         if (!mDragMediaItemId.equals(prevMediaItem.getId())) {
   1828                             mDropAfterMediaItem = prevMediaItem;
   1829                             mDropIndex = i;
   1830                             return beginMs;
   1831                         } else {
   1832                             mDropAfterMediaItem = null;
   1833                             mDropIndex = -1;
   1834                             return beginMs;
   1835                         }
   1836                     } else {
   1837                         mDropAfterMediaItem = null;
   1838                         mDropIndex = 0;
   1839                         return 0;
   1840                     }
   1841                 } else {
   1842                     mDropAfterMediaItem = null;
   1843                     mDropIndex = -1;
   1844                     return beginMs;
   1845                 }
   1846             }
   1847 
   1848             beginMs = endMs;
   1849         }
   1850 
   1851         return timeMs;
   1852     }
   1853 
   1854     /**
   1855      * @return The valid time location of the drop (-1 if none)
   1856      */
   1857     private long getRightDropPosition() {
   1858         final List<MovieMediaItem> mediaItems = mProject.getMediaItems();
   1859         long beginMs = 0;
   1860         long endMs = 0;
   1861         long timeMs = mProject.getPlayheadPos();
   1862 
   1863         final int mediaItemsCount = mediaItems.size();
   1864         for (int i = 0; i < mediaItemsCount; i++) {
   1865             final MovieMediaItem mediaItem = mediaItems.get(i);
   1866 
   1867             endMs = beginMs + mediaItem.getAppTimelineDuration();
   1868 
   1869             if (mediaItem.getEndTransition() != null) {
   1870                 if (i < mediaItemsCount - 1) {
   1871                     endMs -= mediaItem.getEndTransition().getAppDuration();
   1872                 }
   1873             }
   1874 
   1875             if (timeMs >= beginMs && timeMs < endMs) {
   1876                 if (!mDragMediaItemId.equals(mediaItem.getId())) {
   1877                     if (i < mediaItemsCount - 1) {
   1878                         // Check if the next item is the drag item
   1879                         final MovieMediaItem nextMediaItem = mediaItems.get(i + 1);
   1880                         if (!mDragMediaItemId.equals(nextMediaItem.getId())) {
   1881                             mDropAfterMediaItem = mediaItem;
   1882                             mDropIndex = i;
   1883                             return endMs;
   1884                         } else {
   1885                             mDropAfterMediaItem = null;
   1886                             mDropIndex = -1;
   1887                             return endMs;
   1888                         }
   1889                     } else {
   1890                         mDropAfterMediaItem = mediaItem;
   1891                         mDropIndex = i;
   1892                         return endMs;
   1893                     }
   1894                 } else {
   1895                     mDropAfterMediaItem = null;
   1896                     mDropIndex = -1;
   1897                     return endMs;
   1898                 }
   1899             }
   1900 
   1901             beginMs = endMs;
   1902         }
   1903 
   1904         return timeMs;
   1905     }
   1906 
   1907 
   1908     /**
   1909      * Adds/edits title overlay of the specified media item.
   1910      */
   1911     private void editOverlay(MovieMediaItem mediaItem) {
   1912         final Intent intent = new Intent(getContext(), OverlayTitleEditor.class);
   1913         intent.putExtra(OverlayTitleEditor.PARAM_MEDIA_ITEM_ID, mediaItem.getId());
   1914 
   1915         // Determine if user wants to edit an existing title overlay or add a new one.
   1916         // Add overlay id and attributes bundle to the extra if the overlay already exists.
   1917         final MovieOverlay overlay = mediaItem.getOverlay();
   1918         if (overlay != null) {
   1919             final String overlayId = mediaItem.getOverlay().getId();
   1920             intent.putExtra(OverlayTitleEditor.PARAM_OVERLAY_ID, overlayId);
   1921             final Bundle attributes = MovieOverlay.buildUserAttributes(
   1922                     overlay.getType(), overlay.getTitle(), overlay.getSubtitle());
   1923             intent.putExtra(OverlayTitleEditor.PARAM_OVERLAY_ATTRIBUTES,
   1924                     attributes);
   1925         }
   1926         ((Activity) getContext()).startActivityForResult(intent,
   1927                 VideoEditorActivity.REQUEST_CODE_PICK_OVERLAY);
   1928     }
   1929 
   1930     /**
   1931      * Removes the overlay of the specified media item.
   1932      */
   1933     private void removeOverlay(MovieMediaItem mediaItem) {
   1934         final Bundle bundle = new Bundle();
   1935         bundle.putString(PARAM_DIALOG_MEDIA_ITEM_ID, mediaItem.getId());
   1936         ((Activity) getContext()).showDialog(
   1937                 VideoEditorActivity.DIALOG_REMOVE_OVERLAY_ID, bundle);
   1938     }
   1939 
   1940     /**
   1941      * Picks a transition.
   1942      *
   1943      * @param afterMediaItem After the media item
   1944      *
   1945      * @return true if the transition can be inserted
   1946      */
   1947     private boolean pickTransition(MovieMediaItem afterMediaItem) {
   1948         // Check if the transition would be too short
   1949         final long transitionDurationMs = getTransitionDuration(afterMediaItem);
   1950         if (transitionDurationMs < MINIMUM_TRANSITION_DURATION) {
   1951             Toast.makeText(getContext(),
   1952                     getContext().getString(R.string.editor_transition_too_short),
   1953                     Toast.LENGTH_SHORT).show();
   1954             return false;
   1955         }
   1956 
   1957         final String afterMediaId = afterMediaItem != null ? afterMediaItem.getId() : null;
   1958         final Intent intent = new Intent(getContext(), TransitionsActivity.class);
   1959         intent.putExtra(TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID, afterMediaId);
   1960         intent.putExtra(TransitionsActivity.PARAM_MINIMUM_DURATION, MINIMUM_TRANSITION_DURATION);
   1961         intent.putExtra(TransitionsActivity.PARAM_DEFAULT_DURATION, transitionDurationMs);
   1962         intent.putExtra(TransitionsActivity.PARAM_MAXIMUM_DURATION,
   1963                 getMaxTransitionDuration(afterMediaItem));
   1964         ((Activity) getContext()).startActivityForResult(intent,
   1965                 VideoEditorActivity.REQUEST_CODE_PICK_TRANSITION);
   1966         return true;
   1967     }
   1968 
   1969     /**
   1970      * Edits a transition.
   1971      *
   1972      * @param transition The transition
   1973      */
   1974     private void editTransition(MovieTransition transition) {
   1975         final MovieMediaItem afterMediaItem = mProject.getPreviousMediaItem(transition);
   1976         final String afterMediaItemId = afterMediaItem != null ? afterMediaItem.getId() : null;
   1977 
   1978         final Intent intent = new Intent(getContext(), TransitionsActivity.class);
   1979         intent.putExtra(TransitionsActivity.PARAM_AFTER_MEDIA_ITEM_ID, afterMediaItemId);
   1980         intent.putExtra(TransitionsActivity.PARAM_TRANSITION_ID, transition.getId());
   1981         intent.putExtra(TransitionsActivity.PARAM_TRANSITION_TYPE, transition.getType());
   1982         intent.putExtra(TransitionsActivity.PARAM_MINIMUM_DURATION, MINIMUM_TRANSITION_DURATION);
   1983         intent.putExtra(TransitionsActivity.PARAM_DEFAULT_DURATION, transition.getAppDuration());
   1984         intent.putExtra(TransitionsActivity.PARAM_MAXIMUM_DURATION,
   1985                 getMaxTransitionDuration(afterMediaItem));
   1986         ((Activity)getContext()).startActivityForResult(intent,
   1987                 VideoEditorActivity.REQUEST_CODE_EDIT_TRANSITION);
   1988     }
   1989 
   1990     /**
   1991      * Finds the media item view with the specified id.
   1992      *
   1993      * @param mediaItemId The media item id
   1994      * @return The found media item view; null if not found
   1995      */
   1996     private View getMediaItemView(String mediaItemId) {
   1997         final int childrenCount = getChildCount();
   1998         for (int i = 0; i < childrenCount; i++) {
   1999             final View childView = getChildAt(i);
   2000             final Object tag = childView.getTag();
   2001             if (tag != null && tag instanceof MovieMediaItem) {
   2002                 final MovieMediaItem mediaItem = (MovieMediaItem)tag;
   2003                 if (mediaItemId.equals(mediaItem.getId())) {
   2004                     return childView;
   2005                 }
   2006             }
   2007         }
   2008 
   2009         return null;
   2010     }
   2011 
   2012     /**
   2013      * Finds the media item view index with the specified id.
   2014      *
   2015      * @param mediaItemId The media item id
   2016      * @return The media item view index; -1 if not found
   2017      */
   2018     private int getMediaItemViewIndex(String mediaItemId) {
   2019         final int childrenCount = getChildCount();
   2020         for (int i = 0; i < childrenCount; i++) {
   2021             final View childView = getChildAt(i);
   2022             final Object tag = childView.getTag();
   2023             if (tag != null && tag instanceof MovieMediaItem) {
   2024                 final MovieMediaItem mediaItem = (MovieMediaItem)tag;
   2025                 if (mediaItemId.equals(mediaItem.getId())) {
   2026                     return i;
   2027                 }
   2028             }
   2029         }
   2030 
   2031         return -1;
   2032     }
   2033 
   2034     /**
   2035      * Finds the transition view with the specified id.
   2036      *
   2037      * @param transitionId The transition id
   2038      *
   2039      * @return The found transition view; null if not found
   2040      */
   2041     private View getTransitionView(String transitionId) {
   2042         final int childrenCount = getChildCount();
   2043         for (int i = 0; i < childrenCount; i++) {
   2044             final View childView = getChildAt(i);
   2045             final Object tag = childView.getTag();
   2046             if (tag != null && tag instanceof MovieTransition) {
   2047                 final MovieTransition transition = (MovieTransition)tag;
   2048                 if (transitionId.equals(transition.getId())) {
   2049                     return childView;
   2050                 }
   2051             }
   2052         }
   2053 
   2054         return null;
   2055     }
   2056 
   2057     /**
   2058      * Removes a transition.
   2059      *
   2060      * @param transitionId The id of the transition to be removed
   2061      */
   2062     public void removeTransitionView(String transitionId) {
   2063         final int childrenCount = getChildCount();
   2064         for (int i = 0; i < childrenCount; i++) {
   2065             final Object tag = getChildAt(i).getTag();
   2066             if (tag != null && tag instanceof MovieTransition) {
   2067                 final MovieTransition transition = (MovieTransition)tag;
   2068                 if (transitionId.equals(transition.getId())) {
   2069                     // Remove the view
   2070                     removeViewAt(i);
   2071 
   2072                     // Adjust the size of all the views
   2073                     requestLayout();
   2074 
   2075                     // If this transition was removed by the user invalidate the menu item
   2076                     if (mMediaItemActionMode != null) {
   2077                         mMediaItemActionMode.invalidate();
   2078                     }
   2079                     return;
   2080                 }
   2081             }
   2082         }
   2083     }
   2084 
   2085     /**
   2086      * Removes all media item and transition views but leave the beginning, end views, and handles.
   2087      */
   2088     private void removeAllMediaItemAndTransitionViews() {
   2089         int index = 0;
   2090         while (index < getChildCount()) {
   2091             final Object tag = getChildAt(index).getTag();
   2092             // Media item view or transition view is associated with a media item or transition
   2093             // attached as a tag. We can thus check the nullity of the tag to determine if it is
   2094             // media item view or transition view.
   2095             if (tag != null) {
   2096                 removeViewAt(index);
   2097             } else {
   2098                 index++;
   2099             }
   2100         }
   2101         requestLayout();
   2102 
   2103         // We cannot add clips by tapping the beginning view.
   2104         mLeftAddClipButton.setVisibility(View.GONE);
   2105     }
   2106 
   2107     /**
   2108      * Computes the transition duration.
   2109      *
   2110      * @param afterMediaItem The position of the transition
   2111      *
   2112      * @return The transition duration
   2113      */
   2114     private long getTransitionDuration(MovieMediaItem afterMediaItem) {
   2115         if (afterMediaItem == null) {
   2116             final MovieMediaItem firstMediaItem = mProject.getFirstMediaItem();
   2117             return Math.min(MAXIMUM_TRANSITION_DURATION / 2,
   2118                     firstMediaItem.getAppTimelineDuration() / 4);
   2119         } else if (mProject.isLastMediaItem(afterMediaItem.getId())) {
   2120             return Math.min(MAXIMUM_TRANSITION_DURATION / 2,
   2121                     afterMediaItem.getAppTimelineDuration() / 4);
   2122         } else {
   2123             final MovieMediaItem beforeMediaItem =
   2124                 mProject.getNextMediaItem(afterMediaItem.getId());
   2125             final long minDurationMs = Math.min(afterMediaItem.getAppTimelineDuration(),
   2126                     beforeMediaItem.getAppTimelineDuration());
   2127             return Math.min(MAXIMUM_TRANSITION_DURATION / 2, minDurationMs / 4);
   2128         }
   2129     }
   2130 
   2131     /**
   2132      * Computes the maximum transition duration.
   2133      *
   2134      * @param afterMediaItem The position of the transition
   2135      *
   2136      * @return The transition duration
   2137      */
   2138     private long getMaxTransitionDuration(MovieMediaItem afterMediaItem) {
   2139         if (afterMediaItem == null) {
   2140             final MovieMediaItem firstMediaItem = mProject.getFirstMediaItem();
   2141             return Math.min(MAXIMUM_TRANSITION_DURATION,
   2142                     firstMediaItem.getAppTimelineDuration() / 4);
   2143         } else if (mProject.isLastMediaItem(afterMediaItem.getId())) {
   2144             return Math.min(MAXIMUM_TRANSITION_DURATION,
   2145                     afterMediaItem.getAppTimelineDuration() / 4);
   2146         } else {
   2147             final MovieMediaItem beforeMediaItem =
   2148                 mProject.getNextMediaItem(afterMediaItem.getId());
   2149             final long minDurationMs = Math.min(afterMediaItem.getAppTimelineDuration(),
   2150                     beforeMediaItem.getAppTimelineDuration());
   2151             return Math.min(MAXIMUM_TRANSITION_DURATION, minDurationMs / 4);
   2152         }
   2153     }
   2154 
   2155     @Override
   2156     public void setSelected(boolean selected) {
   2157         // We only care about when this layout is unselected, which means all children are
   2158         // unselected. Clients should never call setSelected(true) since it is no-op here.
   2159         if (selected == false) {
   2160             closeActionBars();
   2161             clearAndHideTrimHandles();
   2162             mSelectedView = null;
   2163             showAddMediaItemButtons(true);
   2164         }
   2165         dispatchSetSelected(false);
   2166     }
   2167 
   2168     /**
   2169      * Returns true if some view item on the timeline is selected.
   2170      */
   2171     public boolean hasItemSelected() {
   2172         return (mSelectedView != null);
   2173     }
   2174 
   2175     /**
   2176      * Returns true if some media item is being trimmed by user.
   2177      */
   2178     public boolean isTrimming() {
   2179         return mIsTrimming;
   2180     }
   2181 
   2182     /**
   2183      * Closes all contextual action bars.
   2184      */
   2185     private void closeActionBars() {
   2186         if (mMediaItemActionMode != null) {
   2187             mMediaItemActionMode.finish();
   2188             mMediaItemActionMode = null;
   2189         }
   2190 
   2191         if (mTransitionActionMode != null) {
   2192             mTransitionActionMode.finish();
   2193             mTransitionActionMode = null;
   2194         }
   2195     }
   2196 
   2197     /**
   2198      * Hides left and right trim handles and unregisters their listeners.
   2199      */
   2200     private void clearAndHideTrimHandles() {
   2201         mLeftHandle.setVisibility(View.GONE);
   2202         mLeftHandle.setListener(null);
   2203         mRightHandle.setVisibility(View.GONE);
   2204         mRightHandle.setListener(null);
   2205     }
   2206 
   2207     /**
   2208      * Unselects the specified view. No-op if the specified view is already unselected.
   2209      */
   2210     private void unSelect(View view) {
   2211         // Return early if the specified view is already unselected or null.
   2212         if (view == null || !view.isSelected()) {
   2213             return;
   2214         }
   2215 
   2216         mSelectedView = null;
   2217         view.setSelected(false);
   2218         // Need to redraw other children as well because they had dimmed themselves.
   2219         invalidateAllChildren();
   2220         clearAndHideTrimHandles();
   2221     }
   2222 
   2223     /**
   2224      * Selects the specified view and un-selects all others.
   2225      * No-op if the specified view is already selected.
   2226      * The selected view will stand out and all other views on the
   2227      * timeline are dimmed.
   2228      */
   2229     private void select(View selectedView) {
   2230         // Return early if the view is already selected.
   2231         if (selectedView.isSelected()) {
   2232             return;
   2233         }
   2234 
   2235         unselectAllTimelineViews();
   2236         mSelectedView = selectedView;
   2237         mSelectedView.setSelected(true);
   2238         showAddMediaItemButtons(false);
   2239 
   2240         final Object tag = mSelectedView.getTag();
   2241         if (tag instanceof MovieMediaItem) {
   2242             final MediaItemView mediaItemView = (MediaItemView) mSelectedView;
   2243             if (mediaItemView.isGeneratingEffect()) {
   2244                 mLeftHandle.setEnabled(false);
   2245                 mRightHandle.setEnabled(false);
   2246             } else {
   2247                 mLeftHandle.setEnabled(true);
   2248                 mRightHandle.setEnabled(true);
   2249             }
   2250 
   2251             final MovieMediaItem mi = (MovieMediaItem) tag;
   2252             if (mMediaItemActionMode == null) {
   2253                 startActionMode(new MediaItemActionModeCallback(mi));
   2254             }
   2255 
   2256             final boolean videoClip = mi.isVideoClip();
   2257             if (videoClip) {
   2258                 mLeftHandle.setVisibility(View.VISIBLE);
   2259                 mLeftHandle.bringToFront();
   2260                 mLeftHandle.setLimitReached(mi.getAppBoundaryBeginTime() <= 0,
   2261                         mi.getAppTimelineDuration() <=
   2262                             MediaItemUtils.getMinimumVideoItemDuration());
   2263                 mLeftHandle.setListener(new HandleView.MoveListener() {
   2264                     private View mTrimmedView;
   2265                     private MovieMediaItem mMediaItem;
   2266                     private long mTransitionsDurationMs;
   2267                     private long mOriginalBeginMs, mOriginalEndMs;
   2268                     private long mMinimumDurationMs;
   2269                     private int mOriginalWidth;
   2270                     private int mMovePosition;
   2271 
   2272                     @Override
   2273                     public void onMoveBegin(HandleView view) {
   2274                         mMediaItem = (MovieMediaItem)mediaItemView.getTag();
   2275                         mTransitionsDurationMs = (mMediaItem.getBeginTransition() != null ?
   2276                                 mMediaItem.getBeginTransition().getAppDuration() : 0)
   2277                                 + (mMediaItem.getEndTransition() != null ?
   2278                                         mMediaItem.getEndTransition().getAppDuration() : 0);
   2279                         mOriginalBeginMs = mMediaItem.getAppBoundaryBeginTime();
   2280                         mOriginalEndMs = mMediaItem.getAppBoundaryEndTime();
   2281                         mOriginalWidth = mediaItemView.getWidth();
   2282                         mMinimumDurationMs = MediaItemUtils.getMinimumVideoItemDuration();
   2283                         setIsTrimming(true);
   2284                         invalidateAllChildren();
   2285                         mTrimmedView = mediaItemView;
   2286 
   2287                         mListener.onTrimMediaItemBegin(mMediaItem);
   2288                         if (videoClip) { // Video clip
   2289                             mListener.onTrimMediaItem(mMediaItem,
   2290                                     mMediaItem.getAppBoundaryBeginTime());
   2291                         } else {
   2292                             mListener.onTrimMediaItem(mMediaItem, 0);
   2293                         }
   2294                         // Move the playhead
   2295                         mScrollView.setTag(R.id.playhead_offset, view.getRight());
   2296                         mScrollView.invalidate();
   2297                     }
   2298 
   2299                     @Override
   2300                     public boolean onMove(HandleView view, int left, int delta) {
   2301                         if (mMoveLayoutPending) {
   2302                             return false;
   2303                         }
   2304 
   2305                         int position = left + delta;
   2306                         mMovePosition = position;
   2307                         // Compute what will become the width of the view
   2308                         int newWidth = mTrimmedView.getRight() - position;
   2309                         if (newWidth == mTrimmedView.getWidth()) {
   2310                             return false;
   2311                         }
   2312 
   2313                         // Compute the new duration
   2314                         long newDurationMs = mTransitionsDurationMs +
   2315                                 (newWidth * mProject.computeDuration()) /
   2316                                 (getWidth() - (2 * mHalfParentWidth));
   2317                         if (Math.abs(mMediaItem.getAppTimelineDuration() - newDurationMs) <
   2318                                 TIME_TOLERANCE) {
   2319                             return false;
   2320                         } else if (newDurationMs < Math.max(2 * mTransitionsDurationMs,
   2321                                 mMinimumDurationMs)) {
   2322                             newDurationMs = Math.max(2 * mTransitionsDurationMs,
   2323                                     mMinimumDurationMs);
   2324                             newWidth = (int)(((newDurationMs - mTransitionsDurationMs) *
   2325                                     (getWidth() - (2 * mHalfParentWidth)) /
   2326                                     mProject.computeDuration()));
   2327                             position = mTrimmedView.getRight() - newWidth;
   2328                         } else if (mMediaItem.getAppBoundaryEndTime() - newDurationMs < 0) {
   2329                             newDurationMs = mMediaItem.getAppBoundaryEndTime();
   2330                             newWidth = (int)(((newDurationMs - mTransitionsDurationMs) *
   2331                                     (getWidth() - (2 * mHalfParentWidth)) /
   2332                                     mProject.computeDuration()));
   2333                             position = mTrimmedView.getRight() - newWidth;
   2334                         }
   2335 
   2336                         // Return early if the new duration has not changed. We don't have to
   2337                         // adjust the layout.
   2338                         if (newDurationMs == mMediaItem.getAppTimelineDuration()) {
   2339                             return false;
   2340                         }
   2341 
   2342                         mMediaItem.setAppExtractBoundaries(
   2343                                 mMediaItem.getAppBoundaryEndTime() - newDurationMs,
   2344                                 mMediaItem.getAppBoundaryEndTime());
   2345 
   2346                         mLeftHandle.setLimitReached(mMediaItem.getAppBoundaryBeginTime() <= 0,
   2347                                 mMediaItem.getAppTimelineDuration() <= mMinimumDurationMs);
   2348                         mMoveLayoutPending = true;
   2349                         mScrollView.setTag(R.id.left_view_width,
   2350                                 mHalfParentWidth - (newWidth - mOriginalWidth));
   2351                         mScrollView.setTag(R.id.playhead_offset, position);
   2352                         requestLayout();
   2353 
   2354                         mListener.onTrimMediaItem(mMediaItem,
   2355                                 mMediaItem.getAppBoundaryBeginTime());
   2356                         return true;
   2357                     }
   2358 
   2359                     @Override
   2360                     public void onMoveEnd(final HandleView view, final int left, final int delta) {
   2361                         final int position = left + delta;
   2362                         if (mMoveLayoutPending || (position != mMovePosition)) {
   2363                             mHandler.post(new Runnable() {
   2364                                 @Override
   2365                                 public void run() {
   2366                                     if (mMoveLayoutPending) {
   2367                                         mHandler.post(this);
   2368                                     } else if (position != mMovePosition) {
   2369                                         if (onMove(view, left, delta)) {
   2370                                             mHandler.post(this);
   2371                                         } else {
   2372                                             moveDone();
   2373                                         }
   2374                                     } else {
   2375                                         moveDone();
   2376                                     }
   2377                                 }
   2378                             });
   2379                         } else {
   2380                             moveDone();
   2381                         }
   2382                     }
   2383 
   2384                     /**
   2385                      * The move is complete
   2386                      */
   2387                     private void moveDone() {
   2388                         mScrollView.setTag(R.id.left_view_width, mHalfParentWidth);
   2389                         mScrollView.setTag(R.id.playhead_offset, -1);
   2390 
   2391                         mListener.onTrimMediaItemEnd(mMediaItem,
   2392                                 mMediaItem.getAppBoundaryBeginTime());
   2393                         mListener.onRequestMovePlayhead(getBeginTime(mMediaItem), false);
   2394 
   2395                         if (Math.abs(mOriginalBeginMs - mMediaItem.getAppBoundaryBeginTime()) >
   2396                                     TIME_TOLERANCE
   2397                                 || Math.abs(mOriginalEndMs - mMediaItem.getAppBoundaryEndTime()) >
   2398                                     TIME_TOLERANCE) {
   2399 
   2400                             if (videoClip) { // Video clip
   2401                                 ApiService.setMediaItemBoundaries(getContext(), mProject.getPath(),
   2402                                         mMediaItem.getId(), mMediaItem.getAppBoundaryBeginTime(),
   2403                                         mMediaItem.getAppBoundaryEndTime());
   2404                             } else { // Image
   2405                                 ApiService.setMediaItemDuration(getContext(), mProject.getPath(),
   2406                                         mMediaItem.getId(), mMediaItem.getAppTimelineDuration());
   2407                             }
   2408 
   2409                             final long durationMs = mMediaItem.getAppTimelineDuration();
   2410                             mRightHandle.setLimitReached(durationMs <=
   2411                                 MediaItemUtils.getMinimumMediaItemDuration(mMediaItem),
   2412                                     videoClip ? (mMediaItem.getAppBoundaryEndTime() >=
   2413                                         mMediaItem.getDuration()) : durationMs >=
   2414                                             MAXIMUM_IMAGE_DURATION);
   2415 
   2416                             mLeftHandle.setEnabled(false);
   2417                             mRightHandle.setEnabled(false);
   2418                         }
   2419                         setIsTrimming(false);
   2420                         mScrollView.invalidate();
   2421                         invalidateAllChildren();
   2422                     }
   2423                 });
   2424             }
   2425 
   2426             mRightHandle.setVisibility(View.VISIBLE);
   2427             mRightHandle.bringToFront();
   2428             final long durationMs = mi.getAppTimelineDuration();
   2429             mRightHandle.setLimitReached(
   2430                     durationMs <= MediaItemUtils.getMinimumMediaItemDuration(mi),
   2431                     videoClip ? (mi.getAppBoundaryEndTime() >= mi.getDuration()) :
   2432                         durationMs >= MAXIMUM_IMAGE_DURATION);
   2433             mRightHandle.setListener(new HandleView.MoveListener() {
   2434                 private View mTrimmedView;
   2435                 private MovieMediaItem mMediaItem;
   2436                 private long mTransitionsDurationMs;
   2437                 private long mOriginalBeginMs, mOriginalEndMs;
   2438                 private long mMinimumItemDurationMs;
   2439                 private int mMovePosition;
   2440 
   2441                 @Override
   2442                 public void onMoveBegin(HandleView view) {
   2443                     mMediaItem = (MovieMediaItem)mediaItemView.getTag();
   2444                     mTransitionsDurationMs = (mMediaItem.getBeginTransition() != null ?
   2445                             mMediaItem.getBeginTransition().getAppDuration() : 0)
   2446                             + (mMediaItem.getEndTransition() != null ?
   2447                                     mMediaItem.getEndTransition().getAppDuration() : 0);
   2448                     mOriginalBeginMs = mMediaItem.getAppBoundaryBeginTime();
   2449                     mOriginalEndMs = mMediaItem.getAppBoundaryEndTime();
   2450                     mMinimumItemDurationMs = MediaItemUtils.getMinimumMediaItemDuration(mMediaItem);
   2451                     setIsTrimming(true);
   2452                     invalidateAllChildren();
   2453                     mTrimmedView = mediaItemView;
   2454 
   2455                     mListener.onTrimMediaItemBegin(mMediaItem);
   2456                     if (videoClip) {  // Video clip
   2457                         mListener.onTrimMediaItem(mMediaItem, mMediaItem.getAppBoundaryEndTime());
   2458                     } else {
   2459                         mListener.onTrimMediaItem(mMediaItem, 0);
   2460                     }
   2461 
   2462                     // Move the playhead
   2463                     mScrollView.setTag(R.id.playhead_offset, view.getLeft());
   2464                     mScrollView.invalidate();
   2465                 }
   2466 
   2467                 @Override
   2468                 public boolean onMove(HandleView view, int left, int delta) {
   2469                     if (mMoveLayoutPending) {
   2470                         return false;
   2471                     }
   2472 
   2473                     int position = left + delta;
   2474                     mMovePosition = position;
   2475 
   2476                     long newDurationMs;
   2477                     // Compute what will become the width of the view
   2478                     int newWidth = position - mTrimmedView.getLeft();
   2479                     if (newWidth == mTrimmedView.getWidth()) {
   2480                         return false;
   2481                     }
   2482 
   2483                     // Compute the new duration
   2484                     newDurationMs = mTransitionsDurationMs +
   2485                             (newWidth * mProject.computeDuration()) /
   2486                             (getWidth() - (2 * mHalfParentWidth));
   2487                     if (Math.abs(mMediaItem.getAppTimelineDuration() - newDurationMs) <
   2488                             TIME_TOLERANCE) {
   2489                         return false;
   2490                     }
   2491 
   2492                     if (videoClip) { // Video clip
   2493                         if (newDurationMs < Math.max(2 * mTransitionsDurationMs,
   2494                                 mMinimumItemDurationMs)) {
   2495                             newDurationMs = Math.max(2 * mTransitionsDurationMs,
   2496                                     mMinimumItemDurationMs);
   2497                             newWidth = (int)(((newDurationMs - mTransitionsDurationMs) *
   2498                                     (getWidth() - (2 * mHalfParentWidth)) /
   2499                                     mProject.computeDuration()));
   2500                             position = newWidth + mTrimmedView.getLeft();
   2501                         } else if (mMediaItem.getAppBoundaryBeginTime() + newDurationMs >
   2502                                 mMediaItem.getDuration()) {
   2503                             newDurationMs = mMediaItem.getDuration() -
   2504                                 mMediaItem.getAppBoundaryBeginTime();
   2505                             newWidth = (int)(((newDurationMs - mTransitionsDurationMs) *
   2506                                     (getWidth() - (2 * mHalfParentWidth)) /
   2507                                     mProject.computeDuration()));
   2508                             position = newWidth + mTrimmedView.getLeft();
   2509                         }
   2510 
   2511                         if (newDurationMs == mMediaItem.getAppTimelineDuration()) {
   2512                             return false;
   2513                         }
   2514 
   2515                         mMediaItem.setAppExtractBoundaries(mMediaItem.getAppBoundaryBeginTime(),
   2516                                 mMediaItem.getAppBoundaryBeginTime() + newDurationMs);
   2517                         mListener.onTrimMediaItem(mMediaItem, mMediaItem.getAppBoundaryEndTime());
   2518                     } else { // Image
   2519                         if (newDurationMs < Math.max(mMinimumItemDurationMs,
   2520                                 2 * mTransitionsDurationMs)) {
   2521                             newDurationMs = Math.max(mMinimumItemDurationMs,
   2522                                     2 * mTransitionsDurationMs);
   2523                             newWidth = (int)(((newDurationMs - mTransitionsDurationMs) *
   2524                                     (getWidth() - (2 * mHalfParentWidth)) /
   2525                                     mProject.computeDuration()));
   2526                             position = newWidth + mTrimmedView.getLeft();
   2527                         } else if (newDurationMs > MAXIMUM_IMAGE_DURATION) {
   2528                             newDurationMs = MAXIMUM_IMAGE_DURATION;
   2529                             newWidth = (int)(((newDurationMs - mTransitionsDurationMs) *
   2530                                     (getWidth() - (2 * mHalfParentWidth)) /
   2531                                     mProject.computeDuration()));
   2532                             position = newWidth + mTrimmedView.getLeft();
   2533                         }
   2534 
   2535                         // Check if the duration would change
   2536                         if (newDurationMs == mMediaItem.getAppTimelineDuration()) {
   2537                             return false;
   2538                         }
   2539 
   2540                         mMediaItem.setAppExtractBoundaries(0, newDurationMs);
   2541                         mListener.onTrimMediaItem(mMediaItem, 0);
   2542                     }
   2543 
   2544                     mScrollView.setTag(R.id.playhead_offset, position);
   2545                     mRightHandle.setLimitReached(
   2546                             newDurationMs <= mMinimumItemDurationMs,
   2547                             videoClip ? (mMediaItem.getAppBoundaryEndTime() >=
   2548                                 mMediaItem.getDuration()) : newDurationMs >=
   2549                                     MAXIMUM_IMAGE_DURATION);
   2550 
   2551                     mMoveLayoutPending = true;
   2552                     requestLayout();
   2553 
   2554                     return true;
   2555                 }
   2556 
   2557                 @Override
   2558                 public void onMoveEnd(final HandleView view, final int left, final int delta) {
   2559                     final int position = left + delta;
   2560                     if (mMoveLayoutPending || (position != mMovePosition)) {
   2561                         mHandler.post(new Runnable() {
   2562                             @Override
   2563                             public void run() {
   2564                                 if (mMoveLayoutPending) {
   2565                                     mHandler.post(this);
   2566                                 } else if (position != mMovePosition) {
   2567                                     if (onMove(view, left, delta)) {
   2568                                         mHandler.post(this);
   2569                                     } else {
   2570                                         moveDone();
   2571                                     }
   2572                                 } else {
   2573                                     moveDone();
   2574                                 }
   2575                             }
   2576                         });
   2577                     } else {
   2578                         moveDone();
   2579                     }
   2580                 }
   2581 
   2582                 /**
   2583                  * The move is complete
   2584                  */
   2585                 private void moveDone() {
   2586                     mScrollView.setTag(R.id.playhead_offset, -1);
   2587 
   2588                     mListener.onTrimMediaItemEnd(mMediaItem,
   2589                             mMediaItem.getAppBoundaryEndTime());
   2590                     mListener.onRequestMovePlayhead(getEndTime(mMediaItem), false);
   2591 
   2592                     if (Math.abs(mOriginalBeginMs - mMediaItem.getAppBoundaryBeginTime()) >
   2593                             TIME_TOLERANCE ||
   2594                             Math.abs(mOriginalEndMs - mMediaItem.getAppBoundaryEndTime()) >
   2595                             TIME_TOLERANCE) {
   2596                         if (videoClip) { // Video clip
   2597                             ApiService.setMediaItemBoundaries(getContext(), mProject.getPath(),
   2598                                     mMediaItem.getId(), mMediaItem.getAppBoundaryBeginTime(),
   2599                                     mMediaItem.getAppBoundaryEndTime());
   2600                         } else { // Image
   2601                             ApiService.setMediaItemDuration(getContext(), mProject.getPath(),
   2602                                     mMediaItem.getId(), mMediaItem.getAppTimelineDuration());
   2603                         }
   2604 
   2605                         if (videoClip) {
   2606                             mLeftHandle.setLimitReached(mMediaItem.getAppBoundaryBeginTime() <= 0,
   2607                                     mMediaItem.getAppTimelineDuration() <= mMinimumItemDurationMs);
   2608                         }
   2609 
   2610                         mLeftHandle.setEnabled(false);
   2611                         mRightHandle.setEnabled(false);
   2612                     }
   2613                     setIsTrimming(false);
   2614                     mScrollView.invalidate();
   2615                     invalidateAllChildren();
   2616                 }
   2617             });
   2618         } else if (tag instanceof MovieTransition) {
   2619             if (mTransitionActionMode == null) {
   2620                 startActionMode(new TransitionActionModeCallback((MovieTransition) tag));
   2621             }
   2622         }
   2623     }
   2624 
   2625     /**
   2626      * Indicates if any media item is being trimmed or no.
   2627      */
   2628     private void setIsTrimming(boolean isTrimming) {
   2629         mIsTrimming = isTrimming;
   2630     }
   2631 
   2632     /**
   2633      * Sets the playback state for all media item views.
   2634      *
   2635      * @param playback indicates if the playback is ongoing
   2636      */
   2637     private void setPlaybackState(boolean playback) {
   2638         final int childrenCount = getChildCount();
   2639         for (int i = 0; i < childrenCount; i++) {
   2640             final View childView = getChildAt(i);
   2641             final Object tag = childView.getTag();
   2642             if (tag != null) {
   2643                 if (tag instanceof MovieMediaItem) {
   2644                     ((MediaItemView) childView).setPlaybackMode(playback);
   2645                 } else if (tag instanceof MovieTransition) {
   2646                     ((TransitionView) childView).setPlaybackMode(playback);
   2647                 }
   2648             }
   2649         }
   2650     }
   2651 
   2652     /**
   2653      * Un-selects all views in the timeline relative layout, including playhead view and
   2654      * the ones in audio track layout and transition layout.
   2655      */
   2656     private void unselectAllTimelineViews() {
   2657         ((RelativeLayout) getParent()).setSelected(false);
   2658         invalidateAllChildren();
   2659     }
   2660 
   2661     /**
   2662      * Invalidates all children. Note that invalidating the parent does not invalidate its children.
   2663      */
   2664     private void invalidateAllChildren() {
   2665         final int childrenCount = getChildCount();
   2666         for (int i = 0; i < childrenCount; i++) {
   2667             final View childView = getChildAt(i);
   2668             childView.invalidate();
   2669         }
   2670     }
   2671 
   2672     /**
   2673      * Shows or hides "add media buttons" on both sides of the timeline.
   2674      *
   2675      * @param show {@code true} to show the "Add media item" buttons, {@code false} to hide them
   2676      */
   2677     private void showAddMediaItemButtons(boolean show) {
   2678         if (show) {
   2679             // Shows left add button iff there is at least one media item on the timeline.
   2680             if (mProject.getMediaItemCount() > 0) {
   2681                 mLeftAddClipButton.setVisibility(View.VISIBLE);
   2682             }
   2683             mRightAddClipButton.setVisibility(View.VISIBLE);
   2684         } else {
   2685             mLeftAddClipButton.setVisibility(View.GONE);
   2686             mRightAddClipButton.setVisibility(View.GONE);
   2687         }
   2688     }
   2689 }
   2690