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