1/*
2 * Copyright (C) 2013 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.example.android.supportv4.media;
18
19import android.support.v4.media.TransportController;
20import android.support.v4.media.TransportMediator;
21import android.support.v4.media.TransportStateListener;
22import com.example.android.supportv4.R;
23
24import android.content.Context;
25import android.util.AttributeSet;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.accessibility.AccessibilityEvent;
29import android.view.accessibility.AccessibilityNodeInfo;
30import android.widget.FrameLayout;
31import android.widget.ImageButton;
32import android.widget.ProgressBar;
33import android.widget.SeekBar;
34import android.widget.TextView;
35
36import java.util.Formatter;
37import java.util.Locale;
38
39/**
40 * Helper for implementing media controls in an application.
41 * Use instead of the very useful android.widget.MediaController.
42 * This version is embedded inside of an application's layout.
43 */
44public class MediaController extends FrameLayout {
45
46    private TransportController mController;
47    private Context mContext;
48    private ProgressBar mProgress;
49    private TextView mEndTime, mCurrentTime;
50    private boolean mDragging;
51    private boolean mUseFastForward;
52    private boolean mListenersSet;
53    private boolean mShowNext, mShowPrev;
54    private View.OnClickListener mNextListener, mPrevListener;
55    StringBuilder mFormatBuilder;
56    Formatter mFormatter;
57    private ImageButton mPauseButton;
58    private ImageButton mFfwdButton;
59    private ImageButton mRewButton;
60    private ImageButton mNextButton;
61    private ImageButton mPrevButton;
62
63    private TransportStateListener mStateListener = new TransportStateListener() {
64        @Override
65        public void onPlayingChanged(TransportController controller) {
66            updatePausePlay();
67        }
68        @Override
69        public void onTransportControlsChanged(TransportController controller) {
70            updateButtons();
71        }
72    };
73
74    public MediaController(Context context, AttributeSet attrs) {
75        super(context, attrs);
76        mContext = context;
77        mUseFastForward = true;
78        LayoutInflater inflate = (LayoutInflater)
79                mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
80        inflate.inflate(R.layout.media_controller, this, true);
81        initControllerView();
82    }
83
84    public MediaController(Context context, boolean useFastForward) {
85        super(context);
86        mContext = context;
87        mUseFastForward = useFastForward;
88    }
89
90    public MediaController(Context context) {
91        this(context, true);
92    }
93
94    public void setMediaPlayer(TransportController controller) {
95        if (getWindowToken() != null) {
96            if (mController != null) {
97                mController.unregisterStateListener(mStateListener);
98            }
99            if (controller != null) {
100                controller.registerStateListener(mStateListener);
101            }
102        }
103        mController = controller;
104        updatePausePlay();
105    }
106
107    @Override
108    protected void onAttachedToWindow() {
109        super.onAttachedToWindow();
110        if (mController != null) {
111            mController.registerStateListener(mStateListener);
112        }
113    }
114
115    @Override
116    protected void onDetachedFromWindow() {
117        super.onDetachedFromWindow();
118        if (mController != null) {
119            mController.unregisterStateListener(mStateListener);
120        }
121    }
122
123    private void initControllerView() {
124        mPauseButton = (ImageButton) findViewById(R.id.pause);
125        if (mPauseButton != null) {
126            mPauseButton.requestFocus();
127            mPauseButton.setOnClickListener(mPauseListener);
128        }
129
130        mFfwdButton = (ImageButton) findViewById(R.id.ffwd);
131        if (mFfwdButton != null) {
132            mFfwdButton.setOnClickListener(mFfwdListener);
133            mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
134        }
135
136        mRewButton = (ImageButton) findViewById(R.id.rew);
137        if (mRewButton != null) {
138            mRewButton.setOnClickListener(mRewListener);
139            mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
140        }
141
142        // By default these are hidden. They will be enabled when setPrevNextListeners() is called
143        mNextButton = (ImageButton) findViewById(R.id.next);
144        if (mNextButton != null && !mListenersSet) {
145            mNextButton.setVisibility(View.GONE);
146        }
147        mPrevButton = (ImageButton) findViewById(R.id.prev);
148        if (mPrevButton != null && !mListenersSet) {
149            mPrevButton.setVisibility(View.GONE);
150        }
151
152        mProgress = (ProgressBar) findViewById(R.id.mediacontroller_progress);
153        if (mProgress != null) {
154            if (mProgress instanceof SeekBar) {
155                SeekBar seeker = (SeekBar) mProgress;
156                seeker.setOnSeekBarChangeListener(mSeekListener);
157            }
158            mProgress.setMax(1000);
159        }
160
161        mEndTime = (TextView) findViewById(R.id.time);
162        mCurrentTime = (TextView) findViewById(R.id.time_current);
163        mFormatBuilder = new StringBuilder();
164        mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
165
166        installPrevNextListeners();
167    }
168
169    /**
170     * Disable pause or seek buttons if the stream cannot be paused or seeked.
171     * This requires the control interface to be a MediaPlayerControlExt
172     */
173    void updateButtons() {
174        int flags = mController.getTransportControlFlags();
175        boolean enabled = isEnabled();
176        if (mPauseButton != null) {
177            mPauseButton.setEnabled(enabled && (flags&TransportMediator.FLAG_KEY_MEDIA_PAUSE) != 0);
178        }
179        if (mRewButton != null) {
180            mRewButton.setEnabled(enabled && (flags&TransportMediator.FLAG_KEY_MEDIA_REWIND) != 0);
181        }
182        if (mFfwdButton != null) {
183            mFfwdButton.setEnabled(enabled &&
184                    (flags&TransportMediator.FLAG_KEY_MEDIA_FAST_FORWARD) != 0);
185        }
186        if (mPrevButton != null) {
187            mShowPrev = (flags&TransportMediator.FLAG_KEY_MEDIA_PREVIOUS) != 0
188                    || mPrevListener != null;
189            mPrevButton.setEnabled(enabled && mShowPrev);
190        }
191        if (mNextButton != null) {
192            mShowNext = (flags&TransportMediator.FLAG_KEY_MEDIA_NEXT) != 0
193                    || mNextListener != null;
194            mNextButton.setEnabled(enabled && mShowNext);
195        }
196    }
197
198    public void refresh() {
199        updateProgress();
200        updateButtons();
201        updatePausePlay();
202    }
203
204    private String stringForTime(int timeMs) {
205        int totalSeconds = timeMs / 1000;
206
207        int seconds = totalSeconds % 60;
208        int minutes = (totalSeconds / 60) % 60;
209        int hours   = totalSeconds / 3600;
210
211        mFormatBuilder.setLength(0);
212        if (hours > 0) {
213            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
214        } else {
215            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
216        }
217    }
218
219    public long updateProgress() {
220        if (mController == null || mDragging) {
221            return 0;
222        }
223        long position = mController.getCurrentPosition();
224        long duration = mController.getDuration();
225        if (mProgress != null) {
226            if (duration > 0) {
227                // use long to avoid overflow
228                long pos = 1000L * position / duration;
229                mProgress.setProgress( (int) pos);
230            }
231            int percent = mController.getBufferPercentage();
232            mProgress.setSecondaryProgress(percent * 10);
233        }
234
235        if (mEndTime != null)
236            mEndTime.setText(stringForTime((int)duration));
237        if (mCurrentTime != null)
238            mCurrentTime.setText(stringForTime((int)position));
239
240        return position;
241    }
242
243    private View.OnClickListener mPauseListener = new View.OnClickListener() {
244        public void onClick(View v) {
245            doPauseResume();
246        }
247    };
248
249    private void updatePausePlay() {
250        if (mPauseButton == null)
251            return;
252
253        if (mController.isPlaying()) {
254            mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
255        } else {
256            mPauseButton.setImageResource(android.R.drawable.ic_media_play);
257        }
258    }
259
260    private void doPauseResume() {
261        if (mController.isPlaying()) {
262            mController.pausePlaying();
263        } else {
264            mController.startPlaying();
265        }
266        updatePausePlay();
267    }
268
269    // There are two scenarios that can trigger the seekbar listener to trigger:
270    //
271    // The first is the user using the touchpad to adjust the posititon of the
272    // seekbar's thumb. In this case onStartTrackingTouch is called followed by
273    // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
274    // We're setting the field "mDragging" to true for the duration of the dragging
275    // session to avoid jumps in the position in case of ongoing playback.
276    //
277    // The second scenario involves the user operating the scroll ball, in this
278    // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
279    // we will simply apply the updated position without suspending regular updates.
280    private SeekBar.OnSeekBarChangeListener mSeekListener = new SeekBar.OnSeekBarChangeListener() {
281        public void onStartTrackingTouch(SeekBar bar) {
282            mDragging = true;
283        }
284
285        public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
286            if (!fromuser) {
287                // We're not interested in programmatically generated changes to
288                // the progress bar's position.
289                return;
290            }
291
292            long duration = mController.getDuration();
293            long newposition = (duration * progress) / 1000L;
294            mController.seekTo((int) newposition);
295            if (mCurrentTime != null)
296                mCurrentTime.setText(stringForTime( (int) newposition));
297        }
298
299        public void onStopTrackingTouch(SeekBar bar) {
300            mDragging = false;
301            updateProgress();
302            updatePausePlay();
303        }
304    };
305
306    @Override
307    public void setEnabled(boolean enabled) {
308        super.setEnabled(enabled);
309        updateButtons();
310    }
311
312    @Override
313    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
314        super.onInitializeAccessibilityEvent(event);
315        event.setClassName(MediaController.class.getName());
316    }
317
318    @Override
319    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
320        super.onInitializeAccessibilityNodeInfo(info);
321        info.setClassName(MediaController.class.getName());
322    }
323
324    private View.OnClickListener mRewListener = new View.OnClickListener() {
325        public void onClick(View v) {
326            long pos = mController.getCurrentPosition();
327            pos -= 5000; // milliseconds
328            mController.seekTo(pos);
329            updateProgress();
330        }
331    };
332
333    private View.OnClickListener mFfwdListener = new View.OnClickListener() {
334        public void onClick(View v) {
335            long pos = mController.getCurrentPosition();
336            pos += 15000; // milliseconds
337            mController.seekTo(pos);
338            updateProgress();
339        }
340    };
341
342    private void installPrevNextListeners() {
343        if (mNextButton != null) {
344            mNextButton.setOnClickListener(mNextListener);
345            mNextButton.setEnabled(mShowNext);
346        }
347
348        if (mPrevButton != null) {
349            mPrevButton.setOnClickListener(mPrevListener);
350            mPrevButton.setEnabled(mShowPrev);
351        }
352    }
353
354    public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
355        mNextListener = next;
356        mPrevListener = prev;
357        mListenersSet = true;
358
359        installPrevNextListeners();
360
361        if (mNextButton != null) {
362            mNextButton.setVisibility(View.VISIBLE);
363            mShowNext = true;
364        }
365        if (mPrevButton != null) {
366            mPrevButton.setVisibility(View.VISIBLE);
367            mShowPrev = true;
368        }
369    }
370}
371