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