1 /* 2 * Copyright (C) 2019 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 com.android.settings.accessibility; 18 19 import static com.android.settings.accessibility.AccessibilityUtil.State.ON; 20 21 import android.app.settings.SettingsEnums; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Color; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.provider.Settings; 30 import android.view.View; 31 import android.view.accessibility.CaptioningManager; 32 33 import androidx.preference.ListPreference; 34 import androidx.preference.Preference; 35 import androidx.preference.Preference.OnPreferenceChangeListener; 36 import androidx.preference.PreferenceCategory; 37 38 import com.android.internal.widget.SubtitleView; 39 import com.android.settings.R; 40 import com.android.settings.accessibility.ListDialogPreference.OnValueChangedListener; 41 import com.android.settings.dashboard.DashboardFragment; 42 import com.android.settings.search.BaseSearchIndexProvider; 43 import com.android.settingslib.accessibility.AccessibilityUtils; 44 import com.android.settingslib.search.SearchIndexable; 45 import com.android.settingslib.widget.LayoutPreference; 46 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.Locale; 50 51 /** Settings fragment containing font style of captioning properties. */ 52 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) 53 public class CaptionAppearanceFragment extends DashboardFragment 54 implements OnPreferenceChangeListener, OnValueChangedListener { 55 56 private static final String TAG = "CaptionAppearanceFragment"; 57 private static final String PREF_CAPTION_PREVIEW = "caption_preview"; 58 private static final String PREF_BACKGROUND_COLOR = "captioning_background_color"; 59 private static final String PREF_BACKGROUND_OPACITY = "captioning_background_opacity"; 60 private static final String PREF_FOREGROUND_COLOR = "captioning_foreground_color"; 61 private static final String PREF_FOREGROUND_OPACITY = "captioning_foreground_opacity"; 62 private static final String PREF_WINDOW_COLOR = "captioning_window_color"; 63 private static final String PREF_WINDOW_OPACITY = "captioning_window_opacity"; 64 private static final String PREF_EDGE_COLOR = "captioning_edge_color"; 65 private static final String PREF_EDGE_TYPE = "captioning_edge_type"; 66 private static final String PREF_FONT_SIZE = "captioning_font_size"; 67 private static final String PREF_TYPEFACE = "captioning_typeface"; 68 private static final String PREF_PRESET = "captioning_preset"; 69 private static final String PREF_CUSTOM = "custom"; 70 71 /* WebVtt specifies line height as 5.3% of the viewport height. */ 72 private static final float LINE_HEIGHT_RATIO = 0.0533f; 73 74 private CaptioningManager mCaptioningManager; 75 private SubtitleView mPreviewText; 76 private View mPreviewWindow; 77 private View mPreviewViewport; 78 79 // Standard options. 80 private ListPreference mFontSize; 81 private PresetPreference mPreset; 82 83 // Custom options. 84 private ListPreference mTypeface; 85 private ColorPreference mForegroundColor; 86 private ColorPreference mForegroundOpacity; 87 private EdgeTypePreference mEdgeType; 88 private ColorPreference mEdgeColor; 89 private ColorPreference mBackgroundColor; 90 private ColorPreference mBackgroundOpacity; 91 private ColorPreference mWindowColor; 92 private ColorPreference mWindowOpacity; 93 private PreferenceCategory mCustom; 94 95 private boolean mShowingCustom; 96 97 private final List<Preference> mPreferenceList = new ArrayList<>(); 98 99 private final Handler mHandler = new Handler(Looper.getMainLooper()); 100 private final View.OnLayoutChangeListener mLayoutChangeListener = 101 new View.OnLayoutChangeListener() { 102 @Override 103 public void onLayoutChange(View v, int left, int top, int right, int bottom, 104 int oldLeft, int oldTop, int oldRight, int oldBottom) { 105 // Remove the listener once the callback is triggered. 106 mPreviewViewport.removeOnLayoutChangeListener(this); 107 mHandler.post(() ->refreshPreviewText()); 108 } 109 }; 110 111 @Override getMetricsCategory()112 public int getMetricsCategory() { 113 return SettingsEnums.ACCESSIBILITY_CAPTION_APPEARANCE; 114 } 115 116 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)117 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 118 super.onCreatePreferences(savedInstanceState, rootKey); 119 120 mCaptioningManager = (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); 121 122 initializeAllPreferences(); 123 updateAllPreferences(); 124 refreshShowingCustom(); 125 installUpdateListeners(); 126 refreshPreviewText(); 127 } 128 129 @Override getPreferenceScreenResId()130 protected int getPreferenceScreenResId() { 131 return R.xml.captioning_appearance; 132 } 133 134 @Override getLogTag()135 protected String getLogTag() { 136 return TAG; 137 } 138 refreshPreviewText()139 private void refreshPreviewText() { 140 final Context context = getActivity(); 141 if (context == null) { 142 // We've been destroyed, abort! 143 return; 144 } 145 146 final SubtitleView preview = mPreviewText; 147 if (preview != null) { 148 final int styleId = mCaptioningManager.getRawUserStyle(); 149 applyCaptionProperties(mCaptioningManager, preview, mPreviewViewport, styleId); 150 151 final Locale locale = mCaptioningManager.getLocale(); 152 if (locale != null) { 153 final CharSequence localizedText = AccessibilityUtils.getTextForLocale( 154 context, locale, R.string.captioning_preview_text); 155 preview.setText(localizedText); 156 } else { 157 preview.setText(R.string.captioning_preview_text); 158 } 159 160 final CaptioningManager.CaptionStyle style = mCaptioningManager.getUserStyle(); 161 if (style.hasWindowColor()) { 162 mPreviewWindow.setBackgroundColor(style.windowColor); 163 } else { 164 final CaptioningManager.CaptionStyle defStyle = 165 CaptioningManager.CaptionStyle.DEFAULT; 166 mPreviewWindow.setBackgroundColor(defStyle.windowColor); 167 } 168 } 169 } 170 171 /** 172 * Updates font style of captioning properties for preview screen. 173 * 174 * @param manager caption manager 175 * @param previewText preview text 176 * @param previewWindow preview window 177 * @param styleId font style id 178 */ applyCaptionProperties(CaptioningManager manager, SubtitleView previewText, View previewWindow, int styleId)179 public static void applyCaptionProperties(CaptioningManager manager, SubtitleView previewText, 180 View previewWindow, int styleId) { 181 previewText.setStyle(styleId); 182 183 final Context context = previewText.getContext(); 184 final ContentResolver cr = context.getContentResolver(); 185 final float fontScale = manager.getFontScale(); 186 if (previewWindow != null) { 187 // Assume the viewport is clipped with a 16:9 aspect ratio. 188 final float virtualHeight = Math.max(9 * previewWindow.getWidth(), 189 16 * previewWindow.getHeight()) / 16.0f; 190 previewText.setTextSize(virtualHeight * LINE_HEIGHT_RATIO * fontScale); 191 } else { 192 final float textSize = context.getResources().getDimension( 193 R.dimen.caption_preview_text_size); 194 previewText.setTextSize(textSize * fontScale); 195 } 196 197 final Locale locale = manager.getLocale(); 198 if (locale != null) { 199 final CharSequence localizedText = AccessibilityUtils.getTextForLocale( 200 context, locale, R.string.captioning_preview_characters); 201 previewText.setText(localizedText); 202 } else { 203 previewText.setText(R.string.captioning_preview_characters); 204 } 205 } 206 initializeAllPreferences()207 private void initializeAllPreferences() { 208 final LayoutPreference captionPreview = findPreference(PREF_CAPTION_PREVIEW); 209 210 mPreviewText = captionPreview.findViewById(R.id.preview_text); 211 212 mPreviewWindow = captionPreview.findViewById(R.id.preview_window); 213 214 mPreviewViewport = captionPreview.findViewById(R.id.preview_viewport); 215 mPreviewViewport.addOnLayoutChangeListener(mLayoutChangeListener); 216 217 final Resources res = getResources(); 218 final int[] presetValues = res.getIntArray(R.array.captioning_preset_selector_values); 219 final String[] presetTitles = res.getStringArray(R.array.captioning_preset_selector_titles); 220 mPreset = (PresetPreference) findPreference(PREF_PRESET); 221 mPreset.setValues(presetValues); 222 mPreset.setTitles(presetTitles); 223 224 mFontSize = (ListPreference) findPreference(PREF_FONT_SIZE); 225 226 // Initialize the preference list 227 mPreferenceList.add(mFontSize); 228 mPreferenceList.add(mPreset); 229 230 mCustom = (PreferenceCategory) findPreference(PREF_CUSTOM); 231 mShowingCustom = true; 232 233 final int[] colorValues = res.getIntArray(R.array.captioning_color_selector_values); 234 final String[] colorTitles = res.getStringArray(R.array.captioning_color_selector_titles); 235 mForegroundColor = (ColorPreference) mCustom.findPreference(PREF_FOREGROUND_COLOR); 236 mForegroundColor.setTitles(colorTitles); 237 mForegroundColor.setValues(colorValues); 238 239 final int[] opacityValues = res.getIntArray(R.array.captioning_opacity_selector_values); 240 final String[] opacityTitles = res.getStringArray( 241 R.array.captioning_opacity_selector_titles); 242 mForegroundOpacity = (ColorPreference) mCustom.findPreference(PREF_FOREGROUND_OPACITY); 243 mForegroundOpacity.setTitles(opacityTitles); 244 mForegroundOpacity.setValues(opacityValues); 245 246 mEdgeColor = (ColorPreference) mCustom.findPreference(PREF_EDGE_COLOR); 247 mEdgeColor.setTitles(colorTitles); 248 mEdgeColor.setValues(colorValues); 249 250 // Add "none" as an additional option for backgrounds. 251 final int[] bgColorValues = new int[colorValues.length + 1]; 252 final String[] bgColorTitles = new String[colorTitles.length + 1]; 253 System.arraycopy(colorValues, 0, bgColorValues, 1, colorValues.length); 254 System.arraycopy(colorTitles, 0, bgColorTitles, 1, colorTitles.length); 255 bgColorValues[0] = Color.TRANSPARENT; 256 bgColorTitles[0] = getString(R.string.color_none); 257 mBackgroundColor = (ColorPreference) mCustom.findPreference(PREF_BACKGROUND_COLOR); 258 mBackgroundColor.setTitles(bgColorTitles); 259 mBackgroundColor.setValues(bgColorValues); 260 261 mBackgroundOpacity = (ColorPreference) mCustom.findPreference(PREF_BACKGROUND_OPACITY); 262 mBackgroundOpacity.setTitles(opacityTitles); 263 mBackgroundOpacity.setValues(opacityValues); 264 265 mWindowColor = (ColorPreference) mCustom.findPreference(PREF_WINDOW_COLOR); 266 mWindowColor.setTitles(bgColorTitles); 267 mWindowColor.setValues(bgColorValues); 268 269 mWindowOpacity = (ColorPreference) mCustom.findPreference(PREF_WINDOW_OPACITY); 270 mWindowOpacity.setTitles(opacityTitles); 271 mWindowOpacity.setValues(opacityValues); 272 273 mEdgeType = (EdgeTypePreference) mCustom.findPreference(PREF_EDGE_TYPE); 274 mTypeface = (ListPreference) mCustom.findPreference(PREF_TYPEFACE); 275 } 276 installUpdateListeners()277 private void installUpdateListeners() { 278 mPreset.setOnValueChangedListener(this); 279 mForegroundColor.setOnValueChangedListener(this); 280 mForegroundOpacity.setOnValueChangedListener(this); 281 mEdgeColor.setOnValueChangedListener(this); 282 mBackgroundColor.setOnValueChangedListener(this); 283 mBackgroundOpacity.setOnValueChangedListener(this); 284 mWindowColor.setOnValueChangedListener(this); 285 mWindowOpacity.setOnValueChangedListener(this); 286 mEdgeType.setOnValueChangedListener(this); 287 288 mTypeface.setOnPreferenceChangeListener(this); 289 mFontSize.setOnPreferenceChangeListener(this); 290 } 291 updateAllPreferences()292 private void updateAllPreferences() { 293 final int preset = mCaptioningManager.getRawUserStyle(); 294 mPreset.setValue(preset); 295 296 final float fontSize = mCaptioningManager.getFontScale(); 297 mFontSize.setValue(Float.toString(fontSize)); 298 299 final ContentResolver cr = getContentResolver(); 300 final CaptioningManager.CaptionStyle attrs = CaptioningManager.CaptionStyle.getCustomStyle( 301 cr); 302 mEdgeType.setValue(attrs.edgeType); 303 mEdgeColor.setValue(attrs.edgeColor); 304 305 final int foregroundColor = attrs.hasForegroundColor() ? attrs.foregroundColor 306 : CaptioningManager.CaptionStyle.COLOR_UNSPECIFIED; 307 parseColorOpacity(mForegroundColor, mForegroundOpacity, foregroundColor); 308 309 final int backgroundColor = attrs.hasBackgroundColor() ? attrs.backgroundColor 310 : CaptioningManager.CaptionStyle.COLOR_UNSPECIFIED; 311 parseColorOpacity(mBackgroundColor, mBackgroundOpacity, backgroundColor); 312 313 final int windowColor = attrs.hasWindowColor() ? attrs.windowColor 314 : CaptioningManager.CaptionStyle.COLOR_UNSPECIFIED; 315 parseColorOpacity(mWindowColor, mWindowOpacity, windowColor); 316 317 final String rawTypeface = attrs.mRawTypeface; 318 mTypeface.setValue(rawTypeface == null ? "" : rawTypeface); 319 } 320 321 /** 322 * Unpacks the specified color value and update the preferences. 323 * 324 * @param color color preference 325 * @param opacity opacity preference 326 * @param value packed value 327 */ parseColorOpacity(ColorPreference color, ColorPreference opacity, int value)328 private void parseColorOpacity(ColorPreference color, ColorPreference opacity, int value) { 329 final int colorValue; 330 final int opacityValue; 331 if (!CaptioningManager.CaptionStyle.hasColor(value)) { 332 // "Default" color with variable alpha. 333 colorValue = CaptioningManager.CaptionStyle.COLOR_UNSPECIFIED; 334 opacityValue = (value & 0xFF) << 24; 335 } else if ((value >>> 24) == 0) { 336 // "None" color with variable alpha. 337 colorValue = Color.TRANSPARENT; 338 opacityValue = (value & 0xFF) << 24; 339 } else { 340 // Normal color. 341 colorValue = value | 0xFF000000; 342 opacityValue = value & 0xFF000000; 343 } 344 345 // Opacity value is always white. 346 opacity.setValue(opacityValue | 0xFFFFFF); 347 color.setValue(colorValue); 348 } 349 mergeColorOpacity(ColorPreference color, ColorPreference opacity)350 private int mergeColorOpacity(ColorPreference color, ColorPreference opacity) { 351 final int colorValue = color.getValue(); 352 final int opacityValue = opacity.getValue(); 353 final int value; 354 // "Default" is 0x00FFFFFF or, for legacy support, 0x00000100. 355 if (!CaptioningManager.CaptionStyle.hasColor(colorValue)) { 356 // Encode "default" as 0x00FFFFaa. 357 value = 0x00FFFF00 | Color.alpha(opacityValue); 358 } else if (colorValue == Color.TRANSPARENT) { 359 // Encode "none" as 0x000000aa. 360 value = Color.alpha(opacityValue); 361 } else { 362 // Encode custom color normally. 363 value = colorValue & 0x00FFFFFF | opacityValue & 0xFF000000; 364 } 365 return value; 366 } 367 refreshShowingCustom()368 private void refreshShowingCustom() { 369 final boolean customPreset = 370 mPreset.getValue() == CaptioningManager.CaptionStyle.PRESET_CUSTOM; 371 if (!customPreset && mShowingCustom) { 372 getPreferenceScreen().removePreference(mCustom); 373 mShowingCustom = false; 374 } else if (customPreset && !mShowingCustom) { 375 getPreferenceScreen().addPreference(mCustom); 376 mShowingCustom = true; 377 } 378 } 379 380 @Override onValueChanged(ListDialogPreference preference, int value)381 public void onValueChanged(ListDialogPreference preference, int value) { 382 final ContentResolver cr = getActivity().getContentResolver(); 383 if (mForegroundColor == preference || mForegroundOpacity == preference) { 384 final int merged = mergeColorOpacity(mForegroundColor, mForegroundOpacity); 385 Settings.Secure.putInt( 386 cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, merged); 387 } else if (mBackgroundColor == preference || mBackgroundOpacity == preference) { 388 final int merged = mergeColorOpacity(mBackgroundColor, mBackgroundOpacity); 389 Settings.Secure.putInt( 390 cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, merged); 391 } else if (mWindowColor == preference || mWindowOpacity == preference) { 392 final int merged = mergeColorOpacity(mWindowColor, mWindowOpacity); 393 Settings.Secure.putInt( 394 cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, merged); 395 } else if (mEdgeColor == preference) { 396 Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, value); 397 } else if (mPreset == preference) { 398 Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET, value); 399 refreshShowingCustom(); 400 } else if (mEdgeType == preference) { 401 Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, value); 402 } 403 404 refreshPreviewText(); 405 enableCaptioningManager(); 406 } 407 408 @Override onPreferenceChange(Preference preference, Object value)409 public boolean onPreferenceChange(Preference preference, Object value) { 410 final ContentResolver cr = getActivity().getContentResolver(); 411 if (mTypeface == preference) { 412 Settings.Secure.putString( 413 cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE, (String) value); 414 refreshPreviewText(); 415 enableCaptioningManager(); 416 } else if (mFontSize == preference) { 417 Settings.Secure.putFloat( 418 cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 419 Float.parseFloat((String) value)); 420 refreshPreviewText(); 421 enableCaptioningManager(); 422 } 423 424 return true; 425 } 426 enableCaptioningManager()427 private void enableCaptioningManager() { 428 if (mCaptioningManager.isEnabled()) { 429 return; 430 } 431 Settings.Secure.putInt(getContentResolver(), 432 Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, ON); 433 } 434 435 @Override getHelpResource()436 public int getHelpResource() { 437 return R.string.help_url_caption; 438 } 439 440 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 441 new BaseSearchIndexProvider(R.xml.captioning_appearance); 442 } 443 444