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 */ CaptioningManager(Context context)66 public CaptioningManager(Context context) { 67 mContentResolver = context.getContentResolver(); 68 } 69 70 /** 71 * @return the user's preferred captioning enabled state 72 */ isEnabled()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 getRawLocale()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 getLocale()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 */ getFontScale()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 */ getRawUserStyle()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 getUserStyle()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 */ addCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 registerObserver(String key)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 */ removeCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 notifyEnabledChanged()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 notifyUserStyleChanged()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 notifyLocaleChanged()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 notifyFontScaleChanged()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 CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor, int windowColor, String rawTypeface)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 applyStyle(@onNull CaptionStyle overlay)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 */ hasBackgroundColor()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 */ hasForegroundColor()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 */ hasEdgeType()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 */ hasEdgeColor()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 */ hasWindowColor()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 getTypeface()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 getCustomStyle(ContentResolver cr)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 */ onEnabledChanged(boolean enabled)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 */ onUserStyleChanged(@onNull CaptionStyle userStyle)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 */ onLocaleChanged(@ullable Locale locale)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 */ onFontScaleChanged(float fontScale)527 public void onFontScaleChanged(float fontScale) {} 528 } 529 } 530