Home | History | Annotate | Download | only in accessibility
      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      */
     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      */
     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
     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
     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      */
    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      */
    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
    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      */
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    227         public MyContentObserver(Handler handler) {
    228             super(handler);
    229 
    230             mHandler = handler;
    231         }
    232 
    233         @Override
    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 
    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          */
    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
    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          */
    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          */
    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          */
    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          */
    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          */
    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
    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
    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          */
    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          */
    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          */
    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          */
    558         public void onFontScaleChanged(float fontScale) {}
    559     }
    560 }
    561