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