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 
17 package android.view.accessibility;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.database.ContentObserver;
24 import android.graphics.Color;
25 import android.graphics.Typeface;
26 import android.net.Uri;
27 import android.os.Handler;
28 import android.provider.Settings.Secure;
29 import android.text.TextUtils;
30 
31 import java.util.ArrayList;
32 import 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  */
45 public 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      */
CaptioningManager(Context context)66     public CaptioningManager(Context context) {
67         mContentResolver = context.getContentResolver();
68     }
69 
70     /**
71      * @return the user's preferred captioning enabled state
72      */
isEnabled()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
getRawLocale()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
getLocale()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      */
getFontScale()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      */
getRawUserStyle()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
getUserStyle()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      */
addCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 
registerObserver(String key)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      */
removeCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 
notifyEnabledChanged()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 
notifyUserStyleChanged()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 
notifyLocaleChanged()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 
notifyFontScaleChanged()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 
CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor, int windowColor, String rawTypeface)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
applyStyle(@onNull CaptionStyle overlay)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          */
hasBackgroundColor()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          */
hasForegroundColor()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          */
hasEdgeType()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          */
hasEdgeColor()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          */
hasWindowColor()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
getTypeface()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
getCustomStyle(ContentResolver cr)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          */
onEnabledChanged(boolean enabled)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          */
onUserStyleChanged(@onNull CaptionStyle userStyle)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          */
onLocaleChanged(@ullable Locale locale)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          */
onFontScaleChanged(float fontScale)527         public void onFontScaleChanged(float fontScale) {}
528     }
529 }
530