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