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