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.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      */
     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      */
     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
     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
     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      */
    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      */
    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
    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      */
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    222         public MyContentObserver(Handler handler) {
    223             super(handler);
    224 
    225             mHandler = handler;
    226         }
    227 
    228         @Override
    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 
    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          */
    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
    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          */
    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          */
    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          */
    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          */
    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          */
    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
    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
    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          */
    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          */
    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          */
    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          */
    553         public void onFontScaleChanged(float fontScale) {}
    554     }
    555 }
    556