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.Dialog;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.os.Bundle;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.ActionMode;
29import android.view.Display;
30import android.view.Menu;
31import android.view.MenuItem;
32import android.view.MotionEvent;
33import android.view.View;
34import android.widget.LinearLayout;
35import android.widget.RelativeLayout;
36import android.widget.SeekBar;
37import android.widget.TextView;
38
39import com.android.videoeditor.AlertDialogs;
40import com.android.videoeditor.VideoEditorActivity;
41import com.android.videoeditor.service.ApiService;
42import com.android.videoeditor.service.MovieAudioTrack;
43import com.android.videoeditor.service.VideoEditorProject;
44import com.android.videoeditor.util.FileUtils;
45import com.android.videoeditor.R;
46
47/**
48 * The LinearLayout which displays audio tracks
49 */
50public class AudioTrackLinearLayout extends LinearLayout {
51    // Logging
52    private static final String TAG = "AudioTrackLinearLayout";
53
54    // Dialog parameter ids
55    private static final String PARAM_DIALOG_AUDIO_TRACK_ID = "audio_track_id";
56
57    // Instance variables
58    private final ItemSimpleGestureListener mAudioTrackGestureListener;
59    private final int mAudioTrackHeight;
60    private final int mHalfParentWidth;
61    private final View mAddAudioTrackButtonView;
62    private final int mAddAudioTrackButtonWidth;
63    private AudioTracksLayoutListener mListener;
64    private ActionMode mAudioTrackActionMode;
65    private VideoEditorProject mProject;
66    private boolean mPlaybackInProgress;
67    private long mTimelineDurationMs;
68
69    /**
70     * Activity listener
71     */
72    public interface AudioTracksLayoutListener {
73        /**
74         * Add an audio track
75         */
76        public void onAddAudioTrack();
77    }
78
79    /**
80     * The audio track action mode handler
81     */
82    private class AudioTrackActionModeCallback implements ActionMode.Callback,
83            SeekBar.OnSeekBarChangeListener {
84        // Instance variables
85        private final MovieAudioTrack mAudioTrack;
86        private int mProgress;
87
88        /**
89         * Constructor
90         *
91         * @param audioTrack The audio track
92         */
93        public AudioTrackActionModeCallback(MovieAudioTrack audioTrack) {
94            mAudioTrack = audioTrack;
95        }
96
97        @Override
98        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
99            mAudioTrackActionMode = mode;
100
101            mode.getMenuInflater().inflate(R.menu.audio_mode_menu, menu);
102
103            final View titleBarView = inflate(getContext(), R.layout.audio_track_action_bar, null);
104
105            mode.setCustomView(titleBarView);
106
107            final TextView titleView = (TextView)titleBarView.findViewById(R.id.action_bar_title);
108            titleView.setText(FileUtils.getSimpleName(mAudioTrack.getFilename()));
109
110            final SeekBar seekBar =
111                ((SeekBar)titleBarView.findViewById(R.id.action_volume));
112            seekBar.setOnSeekBarChangeListener(this);
113            mProgress = mAudioTrack.getAppVolume();
114            seekBar.setProgress(mProgress);
115
116            return true;
117        }
118
119        @Override
120        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
121            MenuItem duckItem = menu.findItem(R.id.action_duck);
122            duckItem.setChecked(mAudioTrack.isAppDuckingEnabled());
123            return true;
124        }
125
126        @Override
127        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
128            switch (item.getItemId()) {
129                case R.id.action_duck: {
130                    final boolean duck = !mAudioTrack.isAppDuckingEnabled();
131                    mAudioTrack.enableAppDucking(duck);
132                    ApiService.setAudioTrackDuck(getContext(), mProject.getPath(),
133                            mAudioTrack.getId(), duck);
134                    item.setChecked(duck);
135                    break;
136                }
137
138                case R.id.action_remove_audio_track: {
139                    final Bundle bundle = new Bundle();
140                    bundle.putString(PARAM_DIALOG_AUDIO_TRACK_ID, mAudioTrack.getId());
141                    ((Activity)getContext()).showDialog(
142                            VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID, bundle);
143                    break;
144                }
145            }
146            return true;
147        }
148
149        @Override
150        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
151            if (fromUser) {
152                mProgress = progress;
153                mAudioTrack.setAppVolume(mProgress);
154            }
155        }
156
157        @Override
158        public void onStartTrackingTouch(SeekBar seekBar) {
159        }
160
161        @Override
162        public void onStopTrackingTouch(SeekBar seekBar) {
163            ApiService.setAudioTrackVolume(getContext(), mProject.getPath(),
164                    mAudioTrack.getId(), mProgress);
165        }
166
167        @Override
168        public void onDestroyActionMode(ActionMode mode) {
169            final View audioTrackView = getAudioTrackView(mAudioTrack.getId());
170            if (audioTrackView != null) {
171                selectView(audioTrackView, false);
172            }
173
174            mAudioTrackActionMode = null;
175        }
176    }
177
178    public AudioTrackLinearLayout(Context context, AttributeSet attrs, int defStyle) {
179        super(context, attrs, defStyle);
180
181        mAudioTrackGestureListener = new ItemSimpleGestureListener() {
182            @Override
183            public boolean onSingleTapConfirmed(View view, int area, MotionEvent e) {
184                if (mPlaybackInProgress) {
185                    return false;
186                }
187
188                if (!view.isSelected()) {
189                    selectView(view, true);
190                }
191
192                return true;
193            }
194
195            @Override
196            public void onLongPress(View view, MotionEvent e) {
197                if (mPlaybackInProgress) {
198                    return;
199                }
200
201                if (!view.isSelected()) {
202                    selectView(view, true);
203                }
204
205                if (mAudioTrackActionMode == null) {
206                    startActionMode(new AudioTrackActionModeCallback(
207                            (MovieAudioTrack)view.getTag()));
208                }
209            }
210        };
211
212        // Add the beginning timeline item
213        final View beginView = inflate(getContext(), R.layout.empty_timeline_item, null);
214        beginView.setOnClickListener(new View.OnClickListener() {
215            @Override
216            public void onClick(View view) {
217                unselectAllViews();
218            }
219        });
220        addView(beginView);
221
222        // Add the end timeline item
223        final View endView = inflate(context, R.layout.empty_timeline_item, null);
224        endView.setOnClickListener(new View.OnClickListener() {
225            @Override
226            public void onClick(View view) {
227                unselectAllViews();
228            }
229        });
230        addView(endView);
231
232        // Add the audio track button
233        mAddAudioTrackButtonView = inflate(getContext(), R.layout.add_audio_track_button, null);
234        addView(mAddAudioTrackButtonView, 1);
235        mAddAudioTrackButtonView.setOnClickListener(new View.OnClickListener() {
236            @Override
237            public void onClick(View view) {
238                if (mListener != null) {
239                    mListener.onAddAudioTrack();
240                }
241            }
242        });
243        mAddAudioTrackButtonWidth = (int)context.getResources().getDimension(
244                R.dimen.add_audio_track_button_width);
245
246        // Compute half the width of the screen (and therefore the parent view)
247        final Display display = ((Activity)context).getWindowManager().getDefaultDisplay();
248        mHalfParentWidth = display.getWidth() / 2;
249
250        // Get the layout height
251        mAudioTrackHeight = (int)context.getResources().getDimension(R.dimen.audio_layout_height);
252
253        setMotionEventSplittingEnabled(false);
254    }
255
256    public AudioTrackLinearLayout(Context context, AttributeSet attrs) {
257        this(context, attrs, 0);
258    }
259
260    public AudioTrackLinearLayout(Context context) {
261        this(context, null, 0);
262    }
263
264    /**
265     * The activity was resumed
266     */
267    public void onResume() {
268        final int childrenCount = getChildCount();
269        for (int i = 0; i < childrenCount; i++) {
270            final View childView = getChildAt(i);
271            final Object tag = childView.getTag();
272            if (tag != null) { // This view represents an audio track
273                final AudioTrackView audioTrackView = (AudioTrackView)childView;
274                if (audioTrackView.getWaveformData() == null) {
275                    final MovieAudioTrack audioTrack = (MovieAudioTrack)tag;
276                    if (audioTrack.getWaveformData() != null) {
277                        audioTrackView.setWaveformData(audioTrack.getWaveformData());
278                        audioTrackView.invalidate();
279                    }
280                }
281            }
282        }
283    }
284
285    /**
286     * @param listener The listener
287     */
288    public void setListener(AudioTracksLayoutListener listener) {
289        mListener = listener;
290    }
291
292    /**
293     * @param project The project
294     */
295    public void setProject(VideoEditorProject project) {
296        // Close the contextual action bar
297        if (mAudioTrackActionMode != null) {
298            mAudioTrackActionMode.finish();
299            mAudioTrackActionMode = null;
300        }
301
302        mProject = project;
303
304        updateAddAudioTrackButton();
305
306        removeAudioTrackViews();
307    }
308
309    /**
310     * @param inProgress true if playback is in progress
311     */
312    public void setPlaybackInProgress(boolean inProgress) {
313        mPlaybackInProgress = inProgress;
314
315        // Don't allow the user to interact with the audio tracks while playback
316        // is in progress
317        if (inProgress && mAudioTrackActionMode != null) {
318            mAudioTrackActionMode.finish();
319            mAudioTrackActionMode = null;
320        }
321    }
322
323    /**
324     * Add the audio tracks
325     *
326     * @param audioTracks The audio tracks
327     */
328    public void addAudioTracks(List<MovieAudioTrack> audioTracks) {
329        if (mAudioTrackActionMode != null) {
330            mAudioTrackActionMode.finish();
331            mAudioTrackActionMode = null;
332        }
333
334        updateAddAudioTrackButton();
335
336        removeAudioTrackViews();
337
338        mTimelineDurationMs = mProject.computeDuration();
339
340        for (MovieAudioTrack audioTrack : audioTracks) {
341            addAudioTrack(audioTrack);
342        }
343    }
344
345    /**
346     * Add a new audio track
347     *
348     * @param audioTrack The audio track
349     *
350     * @return The view that was added
351     */
352    public View addAudioTrack(MovieAudioTrack audioTrack) {
353        updateAddAudioTrackButton();
354
355        final AudioTrackView audioTrackView = (AudioTrackView)inflate(getContext(),
356                R.layout.audio_track_item, null);
357
358        audioTrackView.setTag(audioTrack);
359
360        audioTrackView.setGestureListener(mAudioTrackGestureListener);
361
362        audioTrackView.updateTimelineDuration(mTimelineDurationMs);
363
364        if (audioTrack.getWaveformData() != null) {
365            audioTrackView.setWaveformData(audioTrack.getWaveformData());
366        } else {
367            ApiService.extractAudioTrackAudioWaveform(getContext(), mProject.getPath(),
368                    audioTrack.getId());
369        }
370
371        final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
372                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.FILL_PARENT);
373        addView(audioTrackView, getChildCount() - 1, lp);
374
375        if (mAudioTrackActionMode != null) {
376            mAudioTrackActionMode.invalidate();
377        }
378
379        requestLayout();
380        return audioTrackView;
381    }
382
383    /**
384     * Remove an audio track
385     *
386     * @param audioTrackId The audio track id
387     * @return The view which was removed
388     */
389    public View removeAudioTrack(String audioTrackId) {
390        final int childrenCount = getChildCount();
391        for (int i = 0; i < childrenCount; i++) {
392            final View childView = getChildAt(i);
393            final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag();
394            if (audioTrack != null && audioTrack.getId().equals(audioTrackId)) {
395                removeViewAt(i);
396
397                updateAddAudioTrackButton();
398
399                requestLayout();
400                return childView;
401            }
402        }
403
404        return null;
405    }
406
407    /**
408     * Update the audio track item
409     *
410     * @param audioTrackId The audio track id
411     */
412    public void updateAudioTrack(String audioTrackId) {
413        final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId);
414        if (audioTrackView == null) {
415            Log.e(TAG, "updateAudioTrack: audio track view not found: " + audioTrackId);
416            return;
417        }
418
419        if (mAudioTrackActionMode != null) {
420            mAudioTrackActionMode.invalidate();
421        }
422
423        requestLayout();
424        invalidate();
425    }
426
427    /**
428     * An audio track is being decoded
429     *
430     * @param audioTrackId The audio track id
431     * @param action The action
432     * @param progress The progress
433     */
434    public void onGeneratePreviewProgress(String audioTrackId, int action, int progress) {
435        final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId);
436        if (audioTrackView == null) {
437            Log.e(TAG, "onGeneratePreviewProgress: audio track view not found: " + audioTrackId);
438            return;
439        }
440
441        audioTrackView.setProgress(progress);
442    }
443
444    /**
445     * Set the waveform progress
446     *
447     * @param audioTrackId The audio track id
448     * @param progress The progress
449     */
450    public void setWaveformExtractionProgress(String audioTrackId, int progress) {
451        final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId);
452        if (audioTrackView == null) {
453            Log.e(TAG, "setWaveformExtractionProgress: audio track view not found: "
454                    + audioTrackId);
455            return;
456        }
457
458        audioTrackView.setProgress(progress);
459    }
460
461    /**
462     * The waveform extraction is complete
463     *
464     * @param audioTrackId The audio track id
465     */
466    public void setWaveformExtractionComplete(String audioTrackId) {
467        final AudioTrackView audioTrackView = (AudioTrackView)getAudioTrackView(audioTrackId);
468        if (audioTrackView == null) {
469            Log.e(TAG, "setWaveformExtractionComplete: audio track view not found: "
470                    + audioTrackId);
471            return;
472        }
473
474        audioTrackView.setProgress(-1);
475
476        final MovieAudioTrack audioTrack = (MovieAudioTrack)audioTrackView.getTag();
477        if (audioTrack.getWaveformData() != null) {
478            audioTrackView.setWaveformData(audioTrack.getWaveformData());
479        }
480
481        requestLayout();
482        invalidate();
483    }
484
485    /**
486     * The timeline duration has changed. Refresh the view.
487     */
488    public void updateTimelineDuration() {
489        mTimelineDurationMs = mProject.computeDuration();
490
491        // Media items may had been added or removed
492        updateAddAudioTrackButton();
493
494        // Update the project duration for all views
495        final int childrenCount = getChildCount();
496        for (int i = 0; i < childrenCount; i++) {
497            final View childView = getChildAt(i);
498            final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag();
499            if (audioTrack != null) {
500                ((AudioTrackView)childView).updateTimelineDuration(mTimelineDurationMs);
501            }
502        }
503
504        requestLayout();
505        invalidate();
506    }
507
508    @Override
509    protected void onLayout(boolean changed, int l, int t, int r, int b) {
510        final int childrenCount = getChildCount();
511        if (mTimelineDurationMs == 0) {
512            int left = 0;
513            for (int i = 0; i < childrenCount; i++) {
514                final View childView = getChildAt(i);
515                final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag();
516                if (audioTrack != null) {
517                    // Audio tracks are not visible
518                    childView.layout(left, 0, left, mAudioTrackHeight);
519                } else { // Beginning and end views
520                    childView.layout(left, 0, left + mHalfParentWidth, mAudioTrackHeight);
521                    left += mHalfParentWidth;
522                }
523            }
524        } else {
525            final int viewWidth = getWidth() - (2 * mHalfParentWidth);
526            int left = 0;
527
528            final int leftViewWidth = (Integer)((View)getParent().getParent()).getTag(
529                    R.id.left_view_width);
530
531            for (int i = 0; i < childrenCount; i++) {
532                final View childView = getChildAt(i);
533                final int id = childView.getId();
534                final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag();
535                if (audioTrack != null) { // Audio track views
536                    final int width;
537                    if (audioTrack.isAppLooping()) {
538                        width = (int)((mTimelineDurationMs -
539                                audioTrack.getAppStartTime()) * viewWidth / mTimelineDurationMs);
540                    } else {
541                        if (audioTrack.getAppStartTime() + audioTrack.getTimelineDuration() >
542                                mTimelineDurationMs) {
543                            width = (int)((mTimelineDurationMs -
544                                audioTrack.getAppStartTime()) * viewWidth / mTimelineDurationMs);
545                        } else {
546                            width = (int)(audioTrack.getTimelineDuration() * viewWidth /
547                                    mTimelineDurationMs);
548                        }
549                    }
550
551                    final int trackLeft =
552                        (int)((audioTrack.getAppStartTime() * viewWidth) / mTimelineDurationMs) +
553                            leftViewWidth;
554                    childView.layout(trackLeft, 0, trackLeft + width, mAudioTrackHeight);
555                    left = trackLeft + width;
556                } else if (id == R.id.add_audio_track_button) {
557                    if (childView.getVisibility() == View.VISIBLE) {
558                        childView.layout(left, 0, left + mAddAudioTrackButtonWidth,
559                                mAudioTrackHeight);
560                        left += mAddAudioTrackButtonWidth;
561                    }
562                } else if (i == 0) { // Begin view
563                    childView.layout(left, 0, left + leftViewWidth, mAudioTrackHeight);
564                    left += leftViewWidth;
565                } else { // End view
566                    childView.layout(left, 0, getWidth(), mAudioTrackHeight);
567                }
568            }
569        }
570    }
571
572    /**
573     * Create a new dialog
574     *
575     * @param id The dialog id
576     * @param bundle The dialog bundle
577     *
578     * @return The dialog
579     */
580    public Dialog onCreateDialog(int id, final Bundle bundle) {
581        // If the project is not yet loaded do nothing.
582        if (mProject == null) {
583            return null;
584        }
585
586        switch (id) {
587            case VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID: {
588                final MovieAudioTrack audioTrack = mProject.getAudioTrack(
589                        bundle.getString(PARAM_DIALOG_AUDIO_TRACK_ID));
590                if (audioTrack == null) {
591                    return null;
592                }
593
594                final Activity activity = (Activity)getContext();
595                return AlertDialogs.createAlert(activity,
596                        FileUtils.getSimpleName(audioTrack.getFilename()), 0,
597                        activity.getString(R.string.editor_remove_audio_track_question),
598                        activity.getString(R.string.yes),
599                        new DialogInterface.OnClickListener() {
600                    @Override
601                    public void onClick(DialogInterface dialog, int which) {
602                        if (mAudioTrackActionMode != null) {
603                            mAudioTrackActionMode.finish();
604                            mAudioTrackActionMode = null;
605                        }
606                        activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID);
607
608                        ApiService.removeAudioTrack(activity, mProject.getPath(),
609                                audioTrack.getId());
610                    }
611                }, activity.getString(R.string.no), new DialogInterface.OnClickListener() {
612                    @Override
613                    public void onClick(DialogInterface dialog, int which) {
614                        activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID);
615                    }
616                }, new DialogInterface.OnCancelListener() {
617                    @Override
618                    public void onCancel(DialogInterface dialog) {
619                        activity.removeDialog(VideoEditorActivity.DIALOG_REMOVE_AUDIO_TRACK_ID);
620                    }
621                }, true);
622            }
623
624            default: {
625                return null;
626            }
627        }
628    }
629
630    /**
631     * Find the audio track view with the specified id
632     *
633     * @param audioTrackId The audio track id
634     * @return The audio track view
635     */
636    private View getAudioTrackView(String audioTrackId) {
637        final int childrenCount = getChildCount();
638        for (int i = 0; i < childrenCount; i++) {
639            final View childView = getChildAt(i);
640            final MovieAudioTrack audioTrack = (MovieAudioTrack)childView.getTag();
641            if (audioTrack != null && audioTrackId.equals(audioTrack.getId())) {
642                return childView;
643            }
644        }
645
646        return null;
647    }
648
649    /**
650     * Remove all audio track views (leave the beginning and end views)
651     */
652    private void removeAudioTrackViews() {
653        int index = 0;
654        while (index < getChildCount()) {
655            final Object tag = getChildAt(index).getTag();
656            if (tag != null) {
657                removeViewAt(index);
658            } else {
659                index++;
660            }
661        }
662
663        requestLayout();
664    }
665
666    /**
667     * Set the background of the begin view
668     */
669    private void updateAddAudioTrackButton() {
670        if (mProject == null) { // No project
671            mAddAudioTrackButtonView.setVisibility(View.GONE);
672        } else if (mProject.getMediaItemCount() > 0) {
673            if (mProject.getAudioTracks().size() > 0) {
674                mAddAudioTrackButtonView.setVisibility(View.GONE);
675            } else {
676                mAddAudioTrackButtonView.setVisibility(View.VISIBLE);
677            }
678        } else { // No media items
679            mAddAudioTrackButtonView.setVisibility(View.GONE);
680        }
681    }
682
683    @Override
684    public void setSelected(boolean selected) {
685        if (selected == false) {
686            // Close the contextual action bar
687            if (mAudioTrackActionMode != null) {
688                mAudioTrackActionMode.finish();
689                mAudioTrackActionMode = null;
690            }
691        }
692
693        final int childrenCount = getChildCount();
694        for (int i = 0; i < childrenCount; i++) {
695            final View childView = getChildAt(i);
696            childView.setSelected(false);
697        }
698    }
699
700    /**
701     * Select a view and unselect any view that is selected.
702     *
703     * @param selectedView The view to select
704     * @param selected true if selected
705     */
706    private void selectView(View selectedView, boolean selected) {
707        // Check if the selection has changed
708        if (selectedView.isSelected() == selected) {
709            return;
710        }
711
712        if (selected) {
713            unselectAllViews();
714        }
715
716        if (selected && mAudioTrackActionMode == null) {
717            startActionMode(new AudioTrackActionModeCallback(
718                    (MovieAudioTrack)selectedView.getTag()));
719        }
720
721        // Select the new view
722        selectedView.setSelected(selected);
723    }
724
725    /**
726     * Unselect all views
727     */
728    private void unselectAllViews() {
729        ((RelativeLayout)getParent()).setSelected(false);
730    }
731}
732