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 android.media;
18
19import java.util.Locale;
20import java.util.Vector;
21
22import android.content.Context;
23import android.media.SubtitleTrack.RenderingWidget;
24import android.os.Handler;
25import android.os.Looper;
26import android.os.Message;
27import android.view.accessibility.CaptioningManager;
28
29/**
30 * The subtitle controller provides the architecture to display subtitles for a
31 * media source.  It allows specifying which tracks to display, on which anchor
32 * to display them, and also allows adding external, out-of-band subtitle tracks.
33 *
34 * @hide
35 */
36public class SubtitleController {
37    private MediaTimeProvider mTimeProvider;
38    private Vector<Renderer> mRenderers;
39    private Vector<SubtitleTrack> mTracks;
40    private SubtitleTrack mSelectedTrack;
41    private boolean mShowing;
42    private CaptioningManager mCaptioningManager;
43    private Handler mHandler;
44
45    private static final int WHAT_SHOW = 1;
46    private static final int WHAT_HIDE = 2;
47    private static final int WHAT_SELECT_TRACK = 3;
48    private static final int WHAT_SELECT_DEFAULT_TRACK = 4;
49
50    private final Handler.Callback mCallback = new Handler.Callback() {
51        @Override
52        public boolean handleMessage(Message msg) {
53            switch (msg.what) {
54            case WHAT_SHOW:
55                doShow();
56                return true;
57            case WHAT_HIDE:
58                doHide();
59                return true;
60            case WHAT_SELECT_TRACK:
61                doSelectTrack((SubtitleTrack)msg.obj);
62                return true;
63            case WHAT_SELECT_DEFAULT_TRACK:
64                doSelectDefaultTrack();
65                return true;
66            default:
67                return false;
68            }
69        }
70    };
71
72    private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
73        new CaptioningManager.CaptioningChangeListener() {
74            /** @hide */
75            @Override
76            public void onEnabledChanged(boolean enabled) {
77                selectDefaultTrack();
78            }
79
80            /** @hide */
81            @Override
82            public void onLocaleChanged(Locale locale) {
83                selectDefaultTrack();
84            }
85        };
86
87    /**
88     * Creates a subtitle controller for a media playback object that implements
89     * the MediaTimeProvider interface.
90     *
91     * @param timeProvider
92     */
93    public SubtitleController(
94            Context context,
95            MediaTimeProvider timeProvider,
96            Listener listener) {
97        mTimeProvider = timeProvider;
98        mListener = listener;
99
100        mRenderers = new Vector<Renderer>();
101        mShowing = false;
102        mTracks = new Vector<SubtitleTrack>();
103        mCaptioningManager =
104            (CaptioningManager)context.getSystemService(Context.CAPTIONING_SERVICE);
105    }
106
107    @Override
108    protected void finalize() throws Throwable {
109        mCaptioningManager.removeCaptioningChangeListener(
110                mCaptioningChangeListener);
111        super.finalize();
112    }
113
114    /**
115     * @return the available subtitle tracks for this media. These include
116     * the tracks found by {@link MediaPlayer} as well as any tracks added
117     * manually via {@link #addTrack}.
118     */
119    public SubtitleTrack[] getTracks() {
120        synchronized(mTracks) {
121            SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
122            mTracks.toArray(tracks);
123            return tracks;
124        }
125    }
126
127    /**
128     * @return the currently selected subtitle track
129     */
130    public SubtitleTrack getSelectedTrack() {
131        return mSelectedTrack;
132    }
133
134    private RenderingWidget getRenderingWidget() {
135        if (mSelectedTrack == null) {
136            return null;
137        }
138        return mSelectedTrack.getRenderingWidget();
139    }
140
141    /**
142     * Selects a subtitle track.  As a result, this track will receive
143     * in-band data from the {@link MediaPlayer}.  However, this does
144     * not change the subtitle visibility.
145     *
146     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
147     *
148     * @param track The subtitle track to select.  This must be one of the
149     *              tracks in {@link #getTracks}.
150     * @return true if the track was successfully selected.
151     */
152    public boolean selectTrack(SubtitleTrack track) {
153        if (track != null && !mTracks.contains(track)) {
154            return false;
155        }
156
157        processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_TRACK, track));
158        return true;
159    }
160
161    private void doSelectTrack(SubtitleTrack track) {
162        mTrackIsExplicit = true;
163        if (mSelectedTrack == track) {
164            return;
165        }
166
167        if (mSelectedTrack != null) {
168            mSelectedTrack.hide();
169            mSelectedTrack.setTimeProvider(null);
170        }
171
172        mSelectedTrack = track;
173        if (mAnchor != null) {
174            mAnchor.setSubtitleWidget(getRenderingWidget());
175        }
176
177        if (mSelectedTrack != null) {
178            mSelectedTrack.setTimeProvider(mTimeProvider);
179            mSelectedTrack.show();
180        }
181
182        if (mListener != null) {
183            mListener.onSubtitleTrackSelected(track);
184        }
185    }
186
187    /**
188     * @return the default subtitle track based on system preferences, or null,
189     * if no such track exists in this manager.
190     *
191     * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
192     *
193     * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
194     * consider all tracks, but prefer non-FORCED ones.
195     * 2. If user selected "Default" caption language:
196     *   a. If there is a considered track with DEFAULT=yes, returns that track
197     *      (favor the first one in the current language if there are more than
198     *      one default tracks, or the first in general if none of them are in
199     *      the current language).
200     *   b. Otherwise, if there is a track with AUTOSELECT=yes in the current
201     *      language, return that one.
202     *   c. If there are no default tracks, and no autoselectable tracks in the
203     *      current language, return null.
204     * 3. If there is a track with the caption language, select that one.  Prefer
205     * the one with AUTOSELECT=no.
206     *
207     * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
208     * and FORCED=no.
209     */
210    public SubtitleTrack getDefaultTrack() {
211        SubtitleTrack bestTrack = null;
212        int bestScore = -1;
213
214        Locale selectedLocale = mCaptioningManager.getLocale();
215        Locale locale = selectedLocale;
216        if (locale == null) {
217            locale = Locale.getDefault();
218        }
219        boolean selectForced = !mCaptioningManager.isEnabled();
220
221        synchronized(mTracks) {
222            for (SubtitleTrack track: mTracks) {
223                MediaFormat format = track.getFormat();
224                String language = format.getString(MediaFormat.KEY_LANGUAGE);
225                boolean forced =
226                    format.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
227                boolean autoselect =
228                    format.getInteger(MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
229                boolean is_default =
230                    format.getInteger(MediaFormat.KEY_IS_DEFAULT, 0) != 0;
231
232                boolean languageMatches =
233                    (locale == null ||
234                    locale.getLanguage().equals("") ||
235                    locale.getISO3Language().equals(language) ||
236                    locale.getLanguage().equals(language));
237                // is_default is meaningless unless caption language is 'default'
238                int score = (forced ? 0 : 8) +
239                    (((selectedLocale == null) && is_default) ? 4 : 0) +
240                    (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
241
242                if (selectForced && !forced) {
243                    continue;
244                }
245
246                // we treat null locale/language as matching any language
247                if ((selectedLocale == null && is_default) ||
248                    (languageMatches &&
249                     (autoselect || forced || selectedLocale != null))) {
250                    if (score > bestScore) {
251                        bestScore = score;
252                        bestTrack = track;
253                    }
254                }
255            }
256        }
257        return bestTrack;
258    }
259
260    private boolean mTrackIsExplicit = false;
261    private boolean mVisibilityIsExplicit = false;
262
263    /** @hide - should be called from anchor thread */
264    public void selectDefaultTrack() {
265        processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
266    }
267
268    private void doSelectDefaultTrack() {
269        if (mTrackIsExplicit) {
270            // If track selection is explicit, but visibility
271            // is not, it falls back to the captioning setting
272            if (!mVisibilityIsExplicit) {
273                if (mCaptioningManager.isEnabled() ||
274                    (mSelectedTrack != null &&
275                     mSelectedTrack.getFormat().getInteger(
276                            MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
277                    show();
278                } else if (mSelectedTrack != null && !mSelectedTrack.isTimedText()) {
279                    hide();
280                }
281                mVisibilityIsExplicit = false;
282            }
283            return;
284        }
285
286        // We can have a default (forced) track even if captioning
287        // is not enabled.  This is handled by getDefaultTrack().
288        // Show this track unless subtitles were explicitly hidden.
289        SubtitleTrack track = getDefaultTrack();
290        if (track != null) {
291            selectTrack(track);
292            mTrackIsExplicit = false;
293            if (!mVisibilityIsExplicit) {
294                show();
295                mVisibilityIsExplicit = false;
296            }
297        }
298    }
299
300    /** @hide - must be called from anchor thread */
301    public void reset() {
302        checkAnchorLooper();
303        hide();
304        selectTrack(null);
305        mTracks.clear();
306        mTrackIsExplicit = false;
307        mVisibilityIsExplicit = false;
308        mCaptioningManager.removeCaptioningChangeListener(
309                mCaptioningChangeListener);
310    }
311
312    /**
313     * Adds a new, external subtitle track to the manager.
314     *
315     * @param format the format of the track that will include at least
316     *               the MIME type {@link MediaFormat@KEY_MIME}.
317     * @return the created {@link SubtitleTrack} object
318     */
319    public SubtitleTrack addTrack(MediaFormat format) {
320        synchronized(mRenderers) {
321            for (Renderer renderer: mRenderers) {
322                if (renderer.supports(format)) {
323                    SubtitleTrack track = renderer.createTrack(format);
324                    if (track != null) {
325                        synchronized(mTracks) {
326                            if (mTracks.size() == 0) {
327                                mCaptioningManager.addCaptioningChangeListener(
328                                        mCaptioningChangeListener);
329                            }
330                            mTracks.add(track);
331                        }
332                        return track;
333                    }
334                }
335            }
336        }
337        return null;
338    }
339
340    /**
341     * Show the selected (or default) subtitle track.
342     *
343     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
344     */
345    public void show() {
346        processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
347    }
348
349    private void doShow() {
350        mShowing = true;
351        mVisibilityIsExplicit = true;
352        if (mSelectedTrack != null) {
353            mSelectedTrack.show();
354        }
355    }
356
357    /**
358     * Hide the selected (or default) subtitle track.
359     *
360     * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
361     */
362    public void hide() {
363        processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
364    }
365
366    private void doHide() {
367        mVisibilityIsExplicit = true;
368        if (mSelectedTrack != null) {
369            mSelectedTrack.hide();
370        }
371        mShowing = false;
372    }
373
374    /**
375     * Interface for supporting a single or multiple subtitle types in {@link
376     * MediaPlayer}.
377     */
378    public abstract static class Renderer {
379        /**
380         * Called by {@link MediaPlayer}'s {@link SubtitleController} when a new
381         * subtitle track is detected, to see if it should use this object to
382         * parse and display this subtitle track.
383         *
384         * @param format the format of the track that will include at least
385         *               the MIME type {@link MediaFormat@KEY_MIME}.
386         *
387         * @return true if and only if the track format is supported by this
388         * renderer
389         */
390        public abstract boolean supports(MediaFormat format);
391
392        /**
393         * Called by {@link MediaPlayer}'s {@link SubtitleController} for each
394         * subtitle track that was detected and is supported by this object to
395         * create a {@link SubtitleTrack} object.  This object will be created
396         * for each track that was found.  If the track is selected for display,
397         * this object will be used to parse and display the track data.
398         *
399         * @param format the format of the track that will include at least
400         *               the MIME type {@link MediaFormat@KEY_MIME}.
401         * @return a {@link SubtitleTrack} object that will be used to parse
402         * and render the subtitle track.
403         */
404        public abstract SubtitleTrack createTrack(MediaFormat format);
405    }
406
407    /**
408     * Add support for a subtitle format in {@link MediaPlayer}.
409     *
410     * @param renderer a {@link SubtitleController.Renderer} object that adds
411     *                 support for a subtitle format.
412     */
413    public void registerRenderer(Renderer renderer) {
414        synchronized(mRenderers) {
415            // TODO how to get available renderers in the system
416            if (!mRenderers.contains(renderer)) {
417                // TODO should added renderers override existing ones (to allow replacing?)
418                mRenderers.add(renderer);
419            }
420        }
421    }
422
423    /** @hide */
424    public boolean hasRendererFor(MediaFormat format) {
425        synchronized(mRenderers) {
426            // TODO how to get available renderers in the system
427            for (Renderer renderer: mRenderers) {
428                if (renderer.supports(format)) {
429                    return true;
430                }
431            }
432            return false;
433        }
434    }
435
436    /**
437     * Subtitle anchor, an object that is able to display a subtitle renderer,
438     * e.g. a VideoView.
439     */
440    public interface Anchor {
441        /**
442         * Anchor should use the supplied subtitle rendering widget, or
443         * none if it is null.
444         * @hide
445         */
446        public void setSubtitleWidget(RenderingWidget subtitleWidget);
447
448        /**
449         * Anchors provide the looper on which all track visibility changes
450         * (track.show/hide, setSubtitleWidget) will take place.
451         * @hide
452         */
453        public Looper getSubtitleLooper();
454    }
455
456    private Anchor mAnchor;
457
458    /**
459     *  @hide - called from anchor's looper (if any, both when unsetting and
460     *  setting)
461     */
462    public void setAnchor(Anchor anchor) {
463        if (mAnchor == anchor) {
464            return;
465        }
466
467        if (mAnchor != null) {
468            checkAnchorLooper();
469            mAnchor.setSubtitleWidget(null);
470        }
471        mAnchor = anchor;
472        mHandler = null;
473        if (mAnchor != null) {
474            mHandler = new Handler(mAnchor.getSubtitleLooper(), mCallback);
475            checkAnchorLooper();
476            mAnchor.setSubtitleWidget(getRenderingWidget());
477        }
478    }
479
480    private void checkAnchorLooper() {
481        assert mHandler != null : "Should have a looper already";
482        assert Looper.myLooper() == mHandler.getLooper() : "Must be called from the anchor's looper";
483    }
484
485    private void processOnAnchor(Message m) {
486        assert mHandler != null : "Should have a looper already";
487        if (Looper.myLooper() == mHandler.getLooper()) {
488            mHandler.dispatchMessage(m);
489        } else {
490            mHandler.sendMessage(m);
491        }
492    }
493
494    public interface Listener {
495        /**
496         * Called when a subtitle track has been selected.
497         *
498         * @param track selected subtitle track or null
499         * @hide
500         */
501        public void onSubtitleTrackSelected(SubtitleTrack track);
502    }
503
504    private Listener mListener;
505}
506