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