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