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