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