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