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