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