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> 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      */
CaptioningManager(Context context)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      */
isEnabled()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
getRawLocale()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
getLocale()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      */
getFontScale()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      */
getRawUserStyle()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
getUserStyle()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      */
addCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 
registerObserver(String key)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      */
removeCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 
notifyEnabledChanged()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 
notifyUserStyleChanged()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 
notifyLocaleChanged()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 
notifyFontScaleChanged()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 
MyContentObserver(Handler handler)227         public MyContentObserver(Handler handler) {
228             super(handler);
229 
230             mHandler = handler;
231         }
232 
233         @Override
onChange(boolean selfChange, Uri uri)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 
CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor, int windowColor, String rawTypeface)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          */
hasColor(int packedColor)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
applyStyle(@onNull CaptionStyle overlay)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          */
hasBackgroundColor()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          */
hasForegroundColor()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          */
hasEdgeType()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          */
hasEdgeColor()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          */
hasWindowColor()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
getTypeface()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
getCustomStyle(ContentResolver cr)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          */
onEnabledChanged(boolean enabled)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          */
onUserStyleChanged(@onNull CaptionStyle userStyle)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          */
onLocaleChanged(@ullable Locale locale)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          */
onFontScaleChanged(float fontScale)558         public void onFontScaleChanged(float fontScale) {}
559     }
560 }
561