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
17package com.android.videoeditor.widgets;
18
19import java.util.List;
20
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.app.Dialog;
24import android.content.ClipData;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.graphics.Bitmap;
29import android.graphics.Rect;
30import android.media.videoeditor.EffectColor;
31import android.media.videoeditor.MediaItem;
32import android.media.videoeditor.Transition;
33import android.media.videoeditor.TransitionSliding;
34import android.os.Bundle;
35import android.os.Handler;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.ActionMode;
39import android.view.Display;
40import android.view.DragEvent;
41import android.view.Menu;
42import android.view.MenuItem;
43import android.view.MotionEvent;
44import android.view.View;
45import android.widget.ImageButton;
46import android.widget.LinearLayout;
47import android.widget.RelativeLayout;
48import android.widget.Toast;
49
50import com.android.videoeditor.AlertDialogs;
51import com.android.videoeditor.EffectType;
52import com.android.videoeditor.KenBurnsActivity;
53import com.android.videoeditor.OverlayTitleEditor;
54import com.android.videoeditor.TransitionType;
55import com.android.videoeditor.TransitionsActivity;
56import com.android.videoeditor.VideoEditorActivity;
57import com.android.videoeditor.service.ApiService;
58import com.android.videoeditor.service.MovieEffect;
59import com.android.videoeditor.service.MovieMediaItem;
60import com.android.videoeditor.service.MovieOverlay;
61import com.android.videoeditor.service.MovieTransition;
62import com.android.videoeditor.service.VideoEditorProject;
63import com.android.videoeditor.util.FileUtils;
64import com.android.videoeditor.util.MediaItemUtils;
65import com.android.videoeditor.R;
66
67/**
68 * LinearLayout which holds media items and transitions.
69 */
70public 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);
182
183            final MenuItem btmi = menu.findItem(R.id.action_add_begin_transition);
184            btmi.setVisible(mMediaItem.getBeginTransition() == null);
185            btmi.setEnabled(enable);
186
187            final MenuItem etmi = menu.findItem(R.id.action_add_end_transition);
188            etmi.setVisible(mMediaItem.getEndTransition() == null);
189            etmi.setEnabled(enable);
190
191            final MenuItem rmmi = menu.findItem(R.id.action_rendering_mode);
192            rmmi.setVisible(mProject.hasMultipleAspectRatios());
193            rmmi.setEnabled(enable);
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