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.view.accessibility;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.database.ContentObserver;
24import android.graphics.Color;
25import android.graphics.Typeface;
26import android.net.Uri;
27import android.os.Handler;
28import android.provider.Settings.Secure;
29import android.text.TextUtils;
30
31import java.util.ArrayList;
32import java.util.Locale;
33
34/**
35 * Contains methods for accessing and monitoring preferred video captioning state and visual
36 * properties.
37 * <p>
38 * To obtain a handle to the captioning manager, do the following:
39 * <p>
40 * <code>
41 * <pre>CaptioningManager captioningManager =
42 *        (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);</pre>
43 * </code>
44 */
45public class CaptioningManager {
46    /** Default captioning enabled value. */
47    private static final int DEFAULT_ENABLED = 0;
48
49    /** Default style preset as an index into {@link CaptionStyle#PRESETS}. */
50    private static final int DEFAULT_PRESET = 0;
51
52    /** Default scaling value for caption fonts. */
53    private static final float DEFAULT_FONT_SCALE = 1;
54
55    private final ArrayList<CaptioningChangeListener> mListeners = new ArrayList<>();
56    private final ContentResolver mContentResolver;
57    private final ContentObserver mContentObserver;
58
59    /**
60     * Creates a new captioning manager for the specified context.
61     *
62     * @hide
63     */
64    public CaptioningManager(Context context) {
65        mContentResolver = context.getContentResolver();
66
67        final Handler handler = new Handler(context.getMainLooper());
68        mContentObserver = new MyContentObserver(handler);
69    }
70
71    /**
72     * @return the user's preferred captioning enabled state
73     */
74    public final boolean isEnabled() {
75        return Secure.getInt(
76                mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_ENABLED, DEFAULT_ENABLED) == 1;
77    }
78
79    /**
80     * @return the raw locale string for the user's preferred captioning
81     *         language
82     * @hide
83     */
84    @Nullable
85    public final String getRawLocale() {
86        return Secure.getString(mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_LOCALE);
87    }
88
89    /**
90     * @return the locale for the user's preferred captioning language, or null
91     *         if not specified
92     */
93    @Nullable
94    public final Locale getLocale() {
95        final String rawLocale = getRawLocale();
96        if (!TextUtils.isEmpty(rawLocale)) {
97            final String[] splitLocale = rawLocale.split("_");
98            switch (splitLocale.length) {
99                case 3:
100                    return new Locale(splitLocale[0], splitLocale[1], splitLocale[2]);
101                case 2:
102                    return new Locale(splitLocale[0], splitLocale[1]);
103                case 1:
104                    return new Locale(splitLocale[0]);
105            }
106        }
107
108        return null;
109    }
110
111    /**
112     * @return the user's preferred font scaling factor for video captions, or 1 if not
113     *         specified
114     */
115    public final float getFontScale() {
116        return Secure.getFloat(
117                mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, DEFAULT_FONT_SCALE);
118    }
119
120    /**
121     * @return the raw preset number, or the first preset if not specified
122     * @hide
123     */
124    public int getRawUserStyle() {
125        return Secure.getInt(
126                mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_PRESET, DEFAULT_PRESET);
127    }
128
129    /**
130     * @return the user's preferred visual properties for captions as a
131     *         {@link CaptionStyle}, or the default style if not specified
132     */
133    @NonNull
134    public CaptionStyle getUserStyle() {
135        final int preset = getRawUserStyle();
136        if (preset == CaptionStyle.PRESET_CUSTOM) {
137            return CaptionStyle.getCustomStyle(mContentResolver);
138        }
139
140        return CaptionStyle.PRESETS[preset];
141    }
142
143    /**
144     * Adds a listener for changes in the user's preferred captioning enabled
145     * state and visual properties.
146     *
147     * @param listener the listener to add
148     */
149    public void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
150        synchronized (mListeners) {
151            if (mListeners.isEmpty()) {
152                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED);
153                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR);
154                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR);
155                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR);
156                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE);
157                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR);
158                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE);
159                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE);
160                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_LOCALE);
161                registerObserver(Secure.ACCESSIBILITY_CAPTIONING_PRESET);
162            }
163
164            mListeners.add(listener);
165        }
166    }
167
168    private void registerObserver(String key) {
169        mContentResolver.registerContentObserver(Secure.getUriFor(key), false, mContentObserver);
170    }
171
172    /**
173     * Removes a listener previously added using
174     * {@link #addCaptioningChangeListener}.
175     *
176     * @param listener the listener to remove
177     */
178    public void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
179        synchronized (mListeners) {
180            mListeners.remove(listener);
181
182            if (mListeners.isEmpty()) {
183                mContentResolver.unregisterContentObserver(mContentObserver);
184            }
185        }
186    }
187
188    private void notifyEnabledChanged() {
189        final boolean enabled = isEnabled();
190        synchronized (mListeners) {
191            for (CaptioningChangeListener listener : mListeners) {
192                listener.onEnabledChanged(enabled);
193            }
194        }
195    }
196
197    private void notifyUserStyleChanged() {
198        final CaptionStyle userStyle = getUserStyle();
199        synchronized (mListeners) {
200            for (CaptioningChangeListener listener : mListeners) {
201                listener.onUserStyleChanged(userStyle);
202            }
203        }
204    }
205
206    private void notifyLocaleChanged() {
207        final Locale locale = getLocale();
208        synchronized (mListeners) {
209            for (CaptioningChangeListener listener : mListeners) {
210                listener.onLocaleChanged(locale);
211            }
212        }
213    }
214
215    private void notifyFontScaleChanged() {
216        final float fontScale = getFontScale();
217        synchronized (mListeners) {
218            for (CaptioningChangeListener listener : mListeners) {
219                listener.onFontScaleChanged(fontScale);
220            }
221        }
222    }
223
224    private class MyContentObserver extends ContentObserver {
225        private final Handler mHandler;
226
227        public MyContentObserver(Handler handler) {
228            super(handler);
229
230            mHandler = handler;
231        }
232
233        @Override
234        public void onChange(boolean selfChange, Uri uri) {
235            final String uriPath = uri.getPath();
236            final String name = uriPath.substring(uriPath.lastIndexOf('/') + 1);
237            if (Secure.ACCESSIBILITY_CAPTIONING_ENABLED.equals(name)) {
238                notifyEnabledChanged();
239            } else if (Secure.ACCESSIBILITY_CAPTIONING_LOCALE.equals(name)) {
240                notifyLocaleChanged();
241            } else if (Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE.equals(name)) {
242                notifyFontScaleChanged();
243            } else {
244                // We only need a single callback when multiple style properties
245                // change in rapid succession.
246                mHandler.removeCallbacks(mStyleChangedRunnable);
247                mHandler.post(mStyleChangedRunnable);
248            }
249        }
250    };
251
252    /**
253     * Runnable posted when user style properties change. This is used to
254     * prevent unnecessary change notifications when multiple properties change
255     * in rapid succession.
256     */
257    private final Runnable mStyleChangedRunnable = new Runnable() {
258        @Override
259        public void run() {
260            notifyUserStyleChanged();
261        }
262    };
263
264    /**
265     * Specifies visual properties for video captions, including foreground and
266     * background colors, edge properties, and typeface.
267     */
268    public static final class CaptionStyle {
269        /**
270         * Packed value for a color of 'none' and a cached opacity of 100%.
271         *
272         * @hide
273         */
274        private static final int COLOR_NONE_OPAQUE = 0x000000FF;
275
276        /**
277         * Packed value for a color of 'default' and opacity of 100%.
278         *
279         * @hide
280         */
281        public static final int COLOR_UNSPECIFIED = 0x00FFFFFF;
282
283        private static final CaptionStyle WHITE_ON_BLACK;
284        private static final CaptionStyle BLACK_ON_WHITE;
285        private static final CaptionStyle YELLOW_ON_BLACK;
286        private static final CaptionStyle YELLOW_ON_BLUE;
287        private static final CaptionStyle DEFAULT_CUSTOM;
288        private static final CaptionStyle UNSPECIFIED;
289
290        /** The default caption style used to fill in unspecified values. @hide */
291        public static final CaptionStyle DEFAULT;
292
293        /** @hide */
294        public static final CaptionStyle[] PRESETS;
295
296        /** @hide */
297        public static final int PRESET_CUSTOM = -1;
298
299        /** Unspecified edge type value. */
300        public static final int EDGE_TYPE_UNSPECIFIED = -1;
301
302        /** Edge type value specifying no character edges. */
303        public static final int EDGE_TYPE_NONE = 0;
304
305        /** Edge type value specifying uniformly outlined character edges. */
306        public static final int EDGE_TYPE_OUTLINE = 1;
307
308        /** Edge type value specifying drop-shadowed character edges. */
309        public static final int EDGE_TYPE_DROP_SHADOW = 2;
310
311        /** Edge type value specifying raised bevel character edges. */
312        public static final int EDGE_TYPE_RAISED = 3;
313
314        /** Edge type value specifying depressed bevel character edges. */
315        public static final int EDGE_TYPE_DEPRESSED = 4;
316
317        /** The preferred foreground color for video captions. */
318        public final int foregroundColor;
319
320        /** The preferred background color for video captions. */
321        public final int backgroundColor;
322
323        /**
324         * The preferred edge type for video captions, one of:
325         * <ul>
326         * <li>{@link #EDGE_TYPE_UNSPECIFIED}
327         * <li>{@link #EDGE_TYPE_NONE}
328         * <li>{@link #EDGE_TYPE_OUTLINE}
329         * <li>{@link #EDGE_TYPE_DROP_SHADOW}
330         * <li>{@link #EDGE_TYPE_RAISED}
331         * <li>{@link #EDGE_TYPE_DEPRESSED}
332         * </ul>
333         */
334        public final int edgeType;
335
336        /**
337         * The preferred edge color for video captions, if using an edge type
338         * other than {@link #EDGE_TYPE_NONE}.
339         */
340        public final int edgeColor;
341
342        /** The preferred window color for video captions. */
343        public final int windowColor;
344
345        /**
346         * @hide
347         */
348        public final String mRawTypeface;
349
350        private final boolean mHasForegroundColor;
351        private final boolean mHasBackgroundColor;
352        private final boolean mHasEdgeType;
353        private final boolean mHasEdgeColor;
354        private final boolean mHasWindowColor;
355
356        /** Lazily-created typeface based on the raw typeface string. */
357        private Typeface mParsedTypeface;
358
359        private CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor,
360                int windowColor, String rawTypeface) {
361            mHasForegroundColor = hasColor(foregroundColor);
362            mHasBackgroundColor = hasColor(backgroundColor);
363            mHasEdgeType = edgeType != EDGE_TYPE_UNSPECIFIED;
364            mHasEdgeColor = hasColor(edgeColor);
365            mHasWindowColor = hasColor(windowColor);
366
367            // Always use valid colors, even when no override is specified, to
368            // ensure backwards compatibility with apps targeting KitKat MR2.
369            this.foregroundColor = mHasForegroundColor ? foregroundColor : Color.WHITE;
370            this.backgroundColor = mHasBackgroundColor ? backgroundColor : Color.BLACK;
371            this.edgeType = mHasEdgeType ? edgeType : EDGE_TYPE_NONE;
372            this.edgeColor = mHasEdgeColor ? edgeColor : Color.BLACK;
373            this.windowColor = mHasWindowColor ? windowColor : COLOR_NONE_OPAQUE;
374
375            mRawTypeface = rawTypeface;
376        }
377
378        /**
379         * Returns whether a packed color indicates a non-default value.
380         *
381         * @param packedColor the packed color value
382         * @return {@code true} if a non-default value is specified
383         * @hide
384         */
385        public static boolean hasColor(int packedColor) {
386            // Matches the color packing code from Settings. "Default" packed
387            // colors are indicated by zero alpha and non-zero red/blue. The
388            // cached alpha value used by Settings is stored in green.
389            return (packedColor >>> 24) != 0 || (packedColor & 0xFFFF00) == 0;
390        }
391
392        /**
393         * Applies a caption style, overriding any properties that are specified
394         * in the overlay caption.
395         *
396         * @param overlay The style to apply
397         * @return A caption style with the overlay style applied
398         * @hide
399         */
400        @NonNull
401        public CaptionStyle applyStyle(@NonNull CaptionStyle overlay) {
402            final int newForegroundColor = overlay.hasForegroundColor() ?
403                    overlay.foregroundColor : foregroundColor;
404            final int newBackgroundColor = overlay.hasBackgroundColor() ?
405                    overlay.backgroundColor : backgroundColor;
406            final int newEdgeType = overlay.hasEdgeType() ?
407                    overlay.edgeType : edgeType;
408            final int newEdgeColor = overlay.hasEdgeColor() ?
409                    overlay.edgeColor : edgeColor;
410            final int newWindowColor = overlay.hasWindowColor() ?
411                    overlay.windowColor : windowColor;
412            final String newRawTypeface = overlay.mRawTypeface != null ?
413                    overlay.mRawTypeface : mRawTypeface;
414            return new CaptionStyle(newForegroundColor, newBackgroundColor, newEdgeType,
415                    newEdgeColor, newWindowColor, newRawTypeface);
416        }
417
418        /**
419         * @return {@code true} if the user has specified a background color
420         *         that should override the application default, {@code false}
421         *         otherwise
422         */
423        public boolean hasBackgroundColor() {
424            return mHasBackgroundColor;
425        }
426
427        /**
428         * @return {@code true} if the user has specified a foreground color
429         *         that should override the application default, {@code false}
430         *         otherwise
431         */
432        public boolean hasForegroundColor() {
433            return mHasForegroundColor;
434        }
435
436        /**
437         * @return {@code true} if the user has specified an edge type that
438         *         should override the application default, {@code false}
439         *         otherwise
440         */
441        public boolean hasEdgeType() {
442            return mHasEdgeType;
443        }
444
445        /**
446         * @return {@code true} if the user has specified an edge color that
447         *         should override the application default, {@code false}
448         *         otherwise
449         */
450        public boolean hasEdgeColor() {
451            return mHasEdgeColor;
452        }
453
454        /**
455         * @return {@code true} if the user has specified a window color that
456         *         should override the application default, {@code false}
457         *         otherwise
458         */
459        public boolean hasWindowColor() {
460            return mHasWindowColor;
461        }
462
463        /**
464         * @return the preferred {@link Typeface} for video captions, or null if
465         *         not specified
466         */
467        @Nullable
468        public Typeface getTypeface() {
469            if (mParsedTypeface == null && !TextUtils.isEmpty(mRawTypeface)) {
470                mParsedTypeface = Typeface.create(mRawTypeface, Typeface.NORMAL);
471            }
472            return mParsedTypeface;
473        }
474
475        /**
476         * @hide
477         */
478        @NonNull
479        public static CaptionStyle getCustomStyle(ContentResolver cr) {
480            final CaptionStyle defStyle = CaptionStyle.DEFAULT_CUSTOM;
481            final int foregroundColor = Secure.getInt(
482                    cr, Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, defStyle.foregroundColor);
483            final int backgroundColor = Secure.getInt(
484                    cr, Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, defStyle.backgroundColor);
485            final int edgeType = Secure.getInt(
486                    cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, defStyle.edgeType);
487            final int edgeColor = Secure.getInt(
488                    cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, defStyle.edgeColor);
489            final int windowColor = Secure.getInt(
490                    cr, Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, defStyle.windowColor);
491
492            String rawTypeface = Secure.getString(cr, Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE);
493            if (rawTypeface == null) {
494                rawTypeface = defStyle.mRawTypeface;
495            }
496
497            return new CaptionStyle(foregroundColor, backgroundColor, edgeType, edgeColor,
498                    windowColor, rawTypeface);
499        }
500
501        static {
502            WHITE_ON_BLACK = new CaptionStyle(Color.WHITE, Color.BLACK, EDGE_TYPE_NONE,
503                    Color.BLACK, COLOR_NONE_OPAQUE, null);
504            BLACK_ON_WHITE = new CaptionStyle(Color.BLACK, Color.WHITE, EDGE_TYPE_NONE,
505                    Color.BLACK, COLOR_NONE_OPAQUE, null);
506            YELLOW_ON_BLACK = new CaptionStyle(Color.YELLOW, Color.BLACK, EDGE_TYPE_NONE,
507                    Color.BLACK, COLOR_NONE_OPAQUE, null);
508            YELLOW_ON_BLUE = new CaptionStyle(Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE,
509                    Color.BLACK, COLOR_NONE_OPAQUE, null);
510            UNSPECIFIED = new CaptionStyle(COLOR_UNSPECIFIED, COLOR_UNSPECIFIED,
511                    EDGE_TYPE_UNSPECIFIED, COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, null);
512
513            // The ordering of these cannot change since we store the index
514            // directly in preferences.
515            PRESETS = new CaptionStyle[] {
516                    WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE, UNSPECIFIED
517            };
518
519            DEFAULT_CUSTOM = WHITE_ON_BLACK;
520            DEFAULT = WHITE_ON_BLACK;
521        }
522    }
523
524    /**
525     * Listener for changes in captioning properties, including enabled state
526     * and user style preferences.
527     */
528    public static abstract class CaptioningChangeListener {
529        /**
530         * Called when the captioning enabled state changes.
531         *
532         * @param enabled the user's new preferred captioning enabled state
533         */
534        public void onEnabledChanged(boolean enabled) {}
535
536        /**
537         * Called when the captioning user style changes.
538         *
539         * @param userStyle the user's new preferred style
540         * @see CaptioningManager#getUserStyle()
541         */
542        public void onUserStyleChanged(@NonNull CaptionStyle userStyle) {}
543
544        /**
545         * Called when the captioning locale changes.
546         *
547         * @param locale the preferred captioning locale, or {@code null} if not specified
548         * @see CaptioningManager#getLocale()
549         */
550        public void onLocaleChanged(@Nullable Locale locale) {}
551
552        /**
553         * Called when the captioning font scaling factor changes.
554         *
555         * @param fontScale the preferred font scaling factor
556         * @see CaptioningManager#getFontScale()
557         */
558        public void onFontScaleChanged(float fontScale) {}
559    }
560}
561