content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.view.accessibility;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.SystemService;
  import android.annotation.UnsupportedAppUsage;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.database.ContentObserver;
  import android.graphics.Color;
  import android.graphics.Typeface;
  import android.net.Uri;
  import android.os.Handler;
  import android.provider.Settings.Secure;
  import android.text.TextUtils;
  
  import java.util.ArrayList;
  import java.util.Locale;
  
  /**
   * Contains methods for accessing and monitoring preferred video captioning state and visual
   * properties.
   */
  @SystemService(Context.CAPTIONING_SERVICE)
  public class CaptioningManager {
      /** Default captioning enabled value. */
      private static final int DEFAULT_ENABLED = 0;
  
      /** Default style preset as an index into {@link CaptionStyle#PRESETS}. */
      private static final int DEFAULT_PRESET = 0;
  
      /** Default scaling value for caption fonts. */
      private static final float DEFAULT_FONT_SCALE = 1;
  
      private final ArrayList<CaptioningChangeListener> mListeners = new ArrayList<>();
      private final ContentResolver mContentResolver;
      private final ContentObserver mContentObserver;
  
      /**
       * Creates a new captioning manager for the specified context.
       *
       * @hide
       */
      public CaptioningManager(Context context) {
          mContentResolver = context.getContentResolver();
  
          final Handler handler = new Handler(context.getMainLooper());
          mContentObserver = new MyContentObserver(handler);
      }
  
      /**
       * @return the user's preferred captioning enabled state
       */
      public final boolean isEnabled() {
          return Secure.getInt(
                  mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_ENABLED, DEFAULT_ENABLED) == 1;
      }
  
      /**
       * @return the raw locale string for the user's preferred captioning
       *         language
       * @hide
       */
      @Nullable
      public final String getRawLocale() {
          return Secure.getString(mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_LOCALE);
      }
  
      /**
       * @return the locale for the user's preferred captioning language, or null
       *         if not specified
       */
      @Nullable
      public final Locale getLocale() {
          final String rawLocale = getRawLocale();
          if (!TextUtils.isEmpty(rawLocale)) {
              final String[] splitLocale = rawLocale.split("_");
              switch (splitLocale.length) {
                  case 3:
                      return new Locale(splitLocale[0], splitLocale[1], splitLocale[2]);
                  case 2:
                      return new Locale(splitLocale[0], splitLocale[1]);
                  case 1:
                      return new Locale(splitLocale[0]);
              }
          }
  
          return null;
      }
  
      /**
       * @return the user's preferred font scaling factor for video captions, or 1 if not
       *         specified
       */
      public final float getFontScale() {
          return Secure.getFloat(
                  mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, DEFAULT_FONT_SCALE);
      }
  
      /**
       * @return the raw preset number, or the first preset if not specified
       * @hide
       */
      public int getRawUserStyle() {
          return Secure.getInt(
                  mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_PRESET, DEFAULT_PRESET);
      }
  
      /**
       * @return the user's preferred visual properties for captions as a
       *         {@link CaptionStyle}, or the default style if not specified
       */
      @NonNull
      public CaptionStyle getUserStyle() {
          final int preset = getRawUserStyle();
          if (preset == CaptionStyle.PRESET_CUSTOM) {
              return CaptionStyle.getCustomStyle(mContentResolver);
          }
  
          return CaptionStyle.PRESETS[preset];
      }
  
      /**
       * Adds a listener for changes in the user's preferred captioning enabled
       * state and visual properties.
       *
       * @param listener the listener to add
       */
      public void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
          synchronized (mListeners) {
              if (mListeners.isEmpty()) {
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_LOCALE);
                  registerObserver(Secure.ACCESSIBILITY_CAPTIONING_PRESET);
              }
  
              mListeners.add(listener);
          }
      }
  
      private void registerObserver(String key) {
          mContentResolver.registerContentObserver(Secure.getUriFor(key), false, mContentObserver);
      }
  
      /**
       * Removes a listener previously added using
       * {@link #addCaptioningChangeListener}.
       *
       * @param listener the listener to remove
       */
      public void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
          synchronized (mListeners) {
              mListeners.remove(listener);
  
              if (mListeners.isEmpty()) {
                  mContentResolver.unregisterContentObserver(mContentObserver);
              }
          }
      }
  
      private void notifyEnabledChanged() {
          final boolean enabled = isEnabled();
          synchronized (mListeners) {
              for (CaptioningChangeListener listener : mListeners) {
                  listener.onEnabledChanged(enabled);
              }
          }
      }
  
      private void notifyUserStyleChanged() {
          final CaptionStyle userStyle = getUserStyle();
          synchronized (mListeners) {
              for (CaptioningChangeListener listener : mListeners) {
                  listener.onUserStyleChanged(userStyle);
              }
          }
      }
  
      private void notifyLocaleChanged() {
          final Locale locale = getLocale();
          synchronized (mListeners) {
              for (CaptioningChangeListener listener : mListeners) {
                  listener.onLocaleChanged(locale);
              }
          }
      }
  
      private void notifyFontScaleChanged() {
          final float fontScale = getFontScale();
          synchronized (mListeners) {
              for (CaptioningChangeListener listener : mListeners) {
                  listener.onFontScaleChanged(fontScale);
              }
          }
      }
  
      private class MyContentObserver extends ContentObserver {
          private final Handler mHandler;
  
          public MyContentObserver(Handler handler) {
              super(handler);
  
              mHandler = handler;
          }
  
          @Override
          public void onChange(boolean selfChange, Uri uri) {
              final String uriPath = uri.getPath();
              final String name = uriPath.substring(uriPath.lastIndexOf('/') + 1);
              if (Secure.ACCESSIBILITY_CAPTIONING_ENABLED.equals(name)) {
                  notifyEnabledChanged();
              } else if (Secure.ACCESSIBILITY_CAPTIONING_LOCALE.equals(name)) {
                  notifyLocaleChanged();
              } else if (Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE.equals(name)) {
                  notifyFontScaleChanged();
              } else {
                  // We only need a single callback when multiple style properties
                  // change in rapid succession.
                  mHandler.removeCallbacks(mStyleChangedRunnable);
                  mHandler.post(mStyleChangedRunnable);
              }
          }
      };
  
      /**
       * Runnable posted when user style properties change. This is used to
       * prevent unnecessary change notifications when multiple properties change
       * in rapid succession.
       */
      private final Runnable mStyleChangedRunnable = new Runnable() {
          @Override
          public void run() {
              notifyUserStyleChanged();
          }
      };
  
      /**
       * Specifies visual properties for video captions, including foreground and
       * background colors, edge properties, and typeface.
       */
      public static final class CaptionStyle {
          /**
           * Packed value for a color of 'none' and a cached opacity of 100%.
           *
           * @hide
           */
          private static final int COLOR_NONE_OPAQUE = 0x000000FF;
  
          /**
           * Packed value for a color of 'default' and opacity of 100%.
           *
           * @hide
           */
          public static final int COLOR_UNSPECIFIED = 0x00FFFFFF;
  
          private static final CaptionStyle WHITE_ON_BLACK;
          private static final CaptionStyle BLACK_ON_WHITE;
          private static final CaptionStyle YELLOW_ON_BLACK;
          private static final CaptionStyle YELLOW_ON_BLUE;
          private static final CaptionStyle DEFAULT_CUSTOM;
          private static final CaptionStyle UNSPECIFIED;
  
          /** The default caption style used to fill in unspecified values. @hide */
          public static final CaptionStyle DEFAULT;
  
          /** @hide */
          @UnsupportedAppUsage
          public static final CaptionStyle[] PRESETS;
  
          /** @hide */
          public static final int PRESET_CUSTOM = -1;
  
          /** Unspecified edge type value. */
          public static final int EDGE_TYPE_UNSPECIFIED = -1;
  
          /** Edge type value specifying no character edges. */
          public static final int EDGE_TYPE_NONE = 0;
  
          /** Edge type value specifying uniformly outlined character edges. */
          public static final int EDGE_TYPE_OUTLINE = 1;
  
          /** Edge type value specifying drop-shadowed character edges. */
          public static final int EDGE_TYPE_DROP_SHADOW = 2;
  
          /** Edge type value specifying raised bevel character edges. */
          public static final int EDGE_TYPE_RAISED = 3;
  
          /** Edge type value specifying depressed bevel character edges. */
          public static final int EDGE_TYPE_DEPRESSED = 4;
  
          /** The preferred foreground color for video captions. */
          public final int foregroundColor;
  
          /** The preferred background color for video captions. */
          public final int backgroundColor;
  
          /**
           * The preferred edge type for video captions, one of:
           * <ul>
           * <li>{@link #EDGE_TYPE_UNSPECIFIED}
           * <li>{@link #EDGE_TYPE_NONE}
           * <li>{@link #EDGE_TYPE_OUTLINE}
           * <li>{@link #EDGE_TYPE_DROP_SHADOW}
           * <li>{@link #EDGE_TYPE_RAISED}
           * <li>{@link #EDGE_TYPE_DEPRESSED}
           * </ul>
           */
          public final int edgeType;
  
          /**
           * The preferred edge color for video captions, if using an edge type
           * other than {@link #EDGE_TYPE_NONE}.
           */
          public final int edgeColor;
  
          /** The preferred window color for video captions. */
          public final int windowColor;
  
          /**
           * @hide
           */
          public final String mRawTypeface;
  
          private final boolean mHasForegroundColor;
          private final boolean mHasBackgroundColor;
          private final boolean mHasEdgeType;
          private final boolean mHasEdgeColor;
          private final boolean mHasWindowColor;
  
          /** Lazily-created typeface based on the raw typeface string. */
          private Typeface mParsedTypeface;
  
          private CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor,
                  int windowColor, String rawTypeface) {
              mHasForegroundColor = hasColor(foregroundColor);
              mHasBackgroundColor = hasColor(backgroundColor);
              mHasEdgeType = edgeType != EDGE_TYPE_UNSPECIFIED;
              mHasEdgeColor = hasColor(edgeColor);
              mHasWindowColor = hasColor(windowColor);
  
              // Always use valid colors, even when no override is specified, to
              // ensure backwards compatibility with apps targeting KitKat MR2.
              this.foregroundColor = mHasForegroundColor ? foregroundColor : Color.WHITE;
              this.backgroundColor = mHasBackgroundColor ? backgroundColor : Color.BLACK;
              this.edgeType = mHasEdgeType ? edgeType : EDGE_TYPE_NONE;
              this.edgeColor = mHasEdgeColor ? edgeColor : Color.BLACK;
              this.windowColor = mHasWindowColor ? windowColor : COLOR_NONE_OPAQUE;
  
              mRawTypeface = rawTypeface;
          }
  
          /**
           * Returns whether a packed color indicates a non-default value.
           *
           * @param packedColor the packed color value
           * @return {@code true} if a non-default value is specified
           * @hide
           */
          public static boolean hasColor(int packedColor) {
              // Matches the color packing code from Settings. "Default" packed
              // colors are indicated by zero alpha and non-zero red/blue. The
              // cached alpha value used by Settings is stored in green.
              return (packedColor >>> 24) != 0 || (packedColor & 0xFFFF00) == 0;
          }
  
          /**
           * Applies a caption style, overriding any properties that are specified
           * in the overlay caption.
           *
           * @param overlay The style to apply
           * @return A caption style with the overlay style applied
           * @hide
           */
          @NonNull
          public CaptionStyle applyStyle(@NonNull CaptionStyle overlay) {
              final int newForegroundColor = overlay.hasForegroundColor() ?
                      overlay.foregroundColor : foregroundColor;
              final int newBackgroundColor = overlay.hasBackgroundColor() ?
                      overlay.backgroundColor : backgroundColor;
              final int newEdgeType = overlay.hasEdgeType() ?
                      overlay.edgeType : edgeType;
              final int newEdgeColor = overlay.hasEdgeColor() ?
                      overlay.edgeColor : edgeColor;
              final int newWindowColor = overlay.hasWindowColor() ?
                      overlay.windowColor : windowColor;
              final String newRawTypeface = overlay.mRawTypeface != null ?
                      overlay.mRawTypeface : mRawTypeface;
              return new CaptionStyle(newForegroundColor, newBackgroundColor, newEdgeType,
                      newEdgeColor, newWindowColor, newRawTypeface);
          }
  
          /**
           * @return {@code true} if the user has specified a background color
           *         that should override the application default, {@code false}
           *         otherwise
           */
          public boolean hasBackgroundColor() {
              return mHasBackgroundColor;
          }
  
          /**
           * @return {@code true} if the user has specified a foreground color
           *         that should override the application default, {@code false}
           *         otherwise
           */
          public boolean hasForegroundColor() {
              return mHasForegroundColor;
          }
  
          /**
           * @return {@code true} if the user has specified an edge type that
           *         should override the application default, {@code false}
           *         otherwise
           */
          public boolean hasEdgeType() {
              return mHasEdgeType;
          }
  
          /**
           * @return {@code true} if the user has specified an edge color that
           *         should override the application default, {@code false}
           *         otherwise
           */
          public boolean hasEdgeColor() {
              return mHasEdgeColor;
          }
  
          /**
           * @return {@code true} if the user has specified a window color that
           *         should override the application default, {@code false}
           *         otherwise
           */
          public boolean hasWindowColor() {
              return mHasWindowColor;
          }
  
          /**
           * @return the preferred {@link Typeface} for video captions, or null if
           *         not specified
           */
          @Nullable
          public Typeface getTypeface() {
              if (mParsedTypeface == null && !TextUtils.isEmpty(mRawTypeface)) {
                  mParsedTypeface = Typeface.create(mRawTypeface, Typeface.NORMAL);
              }
              return mParsedTypeface;
          }
  
          /**
           * @hide
           */
          @NonNull
          public static CaptionStyle getCustomStyle(ContentResolver cr) {
              final CaptionStyle defStyle = CaptionStyle.DEFAULT_CUSTOM;
              final int foregroundColor = Secure.getInt(
                      cr, Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, defStyle.foregroundColor);
              final int backgroundColor = Secure.getInt(
                      cr, Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, defStyle.backgroundColor);
              final int edgeType = Secure.getInt(
                      cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, defStyle.edgeType);
              final int edgeColor = Secure.getInt(
                      cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, defStyle.edgeColor);
              final int windowColor = Secure.getInt(
                      cr, Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, defStyle.windowColor);
  
              String rawTypeface = Secure.getString(cr, Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE);
              if (rawTypeface == null) {
                  rawTypeface = defStyle.mRawTypeface;
              }
  
              return new CaptionStyle(foregroundColor, backgroundColor, edgeType, edgeColor,
                      windowColor, rawTypeface);
          }
  
          static {
              WHITE_ON_BLACK = new CaptionStyle(Color.WHITE, Color.BLACK, EDGE_TYPE_NONE,
                      Color.BLACK, COLOR_NONE_OPAQUE, null);
              BLACK_ON_WHITE = new CaptionStyle(Color.BLACK, Color.WHITE, EDGE_TYPE_NONE,
                      Color.BLACK, COLOR_NONE_OPAQUE, null);
              YELLOW_ON_BLACK = new CaptionStyle(Color.YELLOW, Color.BLACK, EDGE_TYPE_NONE,
                      Color.BLACK, COLOR_NONE_OPAQUE, null);
              YELLOW_ON_BLUE = new CaptionStyle(Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE,
                      Color.BLACK, COLOR_NONE_OPAQUE, null);
              UNSPECIFIED = new CaptionStyle(COLOR_UNSPECIFIED, COLOR_UNSPECIFIED,
                      EDGE_TYPE_UNSPECIFIED, COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, null);
  
              // The ordering of these cannot change since we store the index
              // directly in preferences.
              PRESETS = new CaptionStyle[] {
                      WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE, UNSPECIFIED
              };
  
              DEFAULT_CUSTOM = WHITE_ON_BLACK;
              DEFAULT = WHITE_ON_BLACK;
          }
      }
  
      /**
       * Listener for changes in captioning properties, including enabled state
       * and user style preferences.
       */
      public static abstract class CaptioningChangeListener {
          /**
           * Called when the captioning enabled state changes.
           *
           * @param enabled the user's new preferred captioning enabled state
           */
          public void onEnabledChanged(boolean enabled) {}
  
          /**
           * Called when the captioning user style changes.
           *
           * @param userStyle the user's new preferred style
           * @see CaptioningManager#getUserStyle()
           */
          public void onUserStyleChanged(@NonNull CaptionStyle userStyle) {}
  
          /**
           * Called when the captioning locale changes.
           *
           * @param locale the preferred captioning locale, or {@code null} if not specified
           * @see CaptioningManager#getLocale()
           */
          public void onLocaleChanged(@Nullable Locale locale) {}
  
          /**
           * Called when the captioning font scaling factor changes.
           *
           * @param fontScale the preferred font scaling factor
           * @see CaptioningManager#getFontScale()
           */
          public void onFontScaleChanged(float fontScale) {}
      }
  }
  