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 */ CaptioningManager(Context context)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 */ isEnabled()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 getRawLocale()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 getLocale()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 */ getFontScale()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 */ getRawUserStyle()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 getUserStyle()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 */ addCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 registerObserver(String key)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 */ removeCaptioningChangeListener(@onNull CaptioningChangeListener listener)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 notifyEnabledChanged()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 notifyUserStyleChanged()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 notifyLocaleChanged()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 notifyFontScaleChanged()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 MyContentObserver(Handler handler)227 public MyContentObserver(Handler handler) { 228 super(handler); 229 230 mHandler = handler; 231 } 232 233 @Override onChange(boolean selfChange, Uri uri)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 CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor, int windowColor, String rawTypeface)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 */ hasColor(int packedColor)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 applyStyle(@onNull CaptionStyle overlay)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 */ hasBackgroundColor()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 */ hasForegroundColor()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 */ hasEdgeType()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 */ hasEdgeColor()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 */ hasWindowColor()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 getTypeface()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 getCustomStyle(ContentResolver cr)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 */ onEnabledChanged(boolean enabled)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 */ onUserStyleChanged(@onNull CaptionStyle userStyle)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 */ onLocaleChanged(@ullable Locale locale)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 */ onFontScaleChanged(float fontScale)558 public void onFontScaleChanged(float fontScale) {} 559 } 560 } 561