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