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