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 static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
20 import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
21 
22 import android.app.Dialog;
23 import android.app.settings.SettingsEnums;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.os.Bundle;
27 import android.provider.Settings;
28 import android.text.method.LinkMovementMethod;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.widget.AdapterView;
33 import android.widget.ListView;
34 import android.widget.TextView;
35 
36 import androidx.annotation.DrawableRes;
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 import androidx.preference.Preference;
41 import androidx.preference.PreferenceScreen;
42 
43 import com.android.settings.DialogCreatable;
44 import com.android.settings.R;
45 import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
46 import com.android.settings.core.BasePreferenceController;
47 import com.android.settings.utils.AnnotationSpan;
48 import com.android.settingslib.core.lifecycle.LifecycleObserver;
49 import com.android.settingslib.core.lifecycle.events.OnCreate;
50 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /** Controller that shows the magnification area mode summary and the preference click behavior. */
56 public class MagnificationModePreferenceController extends BasePreferenceController implements
57         DialogCreatable, LifecycleObserver, OnCreate, OnSaveInstanceState {
58 
59     static final String PREF_KEY = "screen_magnification_mode";
60     private static final int DIALOG_ID_BASE = 10;
61     @VisibleForTesting
62     static final int DIALOG_MAGNIFICATION_MODE = DIALOG_ID_BASE + 1;
63     @VisibleForTesting
64     static final int DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING = DIALOG_ID_BASE + 2;
65     @VisibleForTesting
66     static final String EXTRA_MODE = "mode";
67 
68     private static final String TAG = "MagnificationModePreferenceController";
69 
70     private DialogHelper mDialogHelper;
71     // The magnification mode in the dialog.
72     @MagnificationMode
73     private int mModeCache = MagnificationMode.NONE;
74     private Preference mModePreference;
75     private ShortcutPreference mLinkPreference;
76 
77     @VisibleForTesting
78     ListView mMagnificationModesListView;
79 
80     private final List<MagnificationModeInfo> mModeInfos = new ArrayList<>();
81 
MagnificationModePreferenceController(Context context, String preferenceKey)82     public MagnificationModePreferenceController(Context context, String preferenceKey) {
83         super(context, preferenceKey);
84         initModeInfos();
85     }
86 
initModeInfos()87     private void initModeInfos() {
88         mModeInfos.add(new MagnificationModeInfo(mContext.getText(
89                 R.string.accessibility_magnification_mode_dialog_option_full_screen), null,
90                 R.drawable.a11y_magnification_mode_fullscreen, MagnificationMode.FULLSCREEN));
91         mModeInfos.add(new MagnificationModeInfo(
92                 mContext.getText(R.string.accessibility_magnification_mode_dialog_option_window),
93                 null, R.drawable.a11y_magnification_mode_window, MagnificationMode.WINDOW));
94         mModeInfos.add(new MagnificationModeInfo(
95                 mContext.getText(R.string.accessibility_magnification_mode_dialog_option_switch),
96                 mContext.getText(
97                         R.string.accessibility_magnification_area_settings_mode_switch_summary),
98                 R.drawable.a11y_magnification_mode_switch, MagnificationMode.ALL));
99     }
100 
101     @Override
getAvailabilityStatus()102     public int getAvailabilityStatus() {
103         return AVAILABLE;
104     }
105 
106     @Override
getSummary()107     public CharSequence getSummary() {
108         final int capabilities = MagnificationCapabilities.getCapabilities(mContext);
109         return MagnificationCapabilities.getSummary(mContext, capabilities);
110     }
111 
112     @Override
onCreate(Bundle savedInstanceState)113     public void onCreate(Bundle savedInstanceState) {
114         if (savedInstanceState != null) {
115             mModeCache = savedInstanceState.getInt(EXTRA_MODE, MagnificationMode.NONE);
116         }
117     }
118 
119     @Override
displayPreference(PreferenceScreen screen)120     public void displayPreference(PreferenceScreen screen) {
121         super.displayPreference(screen);
122         mModePreference = screen.findPreference(getPreferenceKey());
123         mLinkPreference = screen.findPreference(
124                 ToggleFeaturePreferenceFragment.KEY_SHORTCUT_PREFERENCE);
125         mModePreference.setOnPreferenceClickListener(preference -> {
126             mModeCache = MagnificationCapabilities.getCapabilities(mContext);
127             mDialogHelper.showDialog(DIALOG_MAGNIFICATION_MODE);
128             return true;
129         });
130     }
131 
132     @Override
onSaveInstanceState(Bundle outState)133     public void onSaveInstanceState(Bundle outState) {
134         outState.putInt(EXTRA_MODE, mModeCache);
135     }
136 
137     /**
138      * Sets {@link DialogHelper} used to show the dialog.
139      */
setDialogHelper(DialogHelper dialogHelper)140     public void setDialogHelper(DialogHelper dialogHelper) {
141         mDialogHelper = dialogHelper;
142         mDialogHelper.setDialogDelegate(this);
143     }
144 
145     @Override
onCreateDialog(int dialogId)146     public Dialog onCreateDialog(int dialogId) {
147         switch (dialogId) {
148             case DIALOG_MAGNIFICATION_MODE:
149                 return createMagnificationModeDialog();
150 
151             case DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
152                 return createMagnificationTripleTapWarningDialog();
153         }
154         return null;
155     }
156 
157     @Override
getDialogMetricsCategory(int dialogId)158     public int getDialogMetricsCategory(int dialogId) {
159         switch (dialogId) {
160             case DIALOG_MAGNIFICATION_MODE:
161                 return SettingsEnums.DIALOG_MAGNIFICATION_CAPABILITY;
162             case DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
163                 return SettingsEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING;
164             default:
165                 return 0;
166         }
167     }
168 
createMagnificationModeDialog()169     private Dialog createMagnificationModeDialog() {
170         mMagnificationModesListView = AccessibilityDialogUtils.createSingleChoiceListView(
171                 mContext, mModeInfos, this::onMagnificationModeSelected);
172 
173         final View headerView = LayoutInflater.from(mContext).inflate(
174                 R.layout.accessibility_magnification_mode_header, mMagnificationModesListView,
175                 false);
176         mMagnificationModesListView.addHeaderView(headerView, /* data= */ null, /* isSelectable= */
177                 false);
178 
179         mMagnificationModesListView.setItemChecked(computeSelectionIndex(), true);
180         final CharSequence title = mContext.getString(
181                 R.string.accessibility_magnification_mode_dialog_title);
182         final CharSequence positiveBtnText = mContext.getString(R.string.save);
183         final CharSequence negativeBtnText = mContext.getString(R.string.cancel);
184 
185         return AccessibilityDialogUtils.createCustomDialog(mContext, title,
186                 mMagnificationModesListView,
187                 positiveBtnText, this::onMagnificationModeDialogPositiveButtonClicked,
188                 negativeBtnText, /* negativeListener= */ null);
189     }
190 
191     @VisibleForTesting
onMagnificationModeDialogPositiveButtonClicked(DialogInterface dialogInterface, int which)192     void onMagnificationModeDialogPositiveButtonClicked(DialogInterface dialogInterface,
193             int which) {
194         final int selectedIndex = mMagnificationModesListView.getCheckedItemPosition();
195         if (selectedIndex == AdapterView.INVALID_POSITION) {
196             Log.w(TAG, "invalid index");
197             return;
198         }
199 
200         mModeCache = ((MagnificationModeInfo) mMagnificationModesListView.getItemAtPosition(
201                         selectedIndex)).mMagnificationMode;
202 
203         // Do not save mode until user clicks positive button in triple tap warning dialog.
204         if (isTripleTapEnabled(mContext) && mModeCache != MagnificationMode.FULLSCREEN) {
205             mDialogHelper.showDialog(DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING);
206         } else { // Save mode (capabilities) value, don't need to show dialog to confirm.
207             updateCapabilitiesAndSummary(mModeCache);
208         }
209     }
210 
updateCapabilitiesAndSummary(@agnificationMode int mode)211     private void updateCapabilitiesAndSummary(@MagnificationMode int mode) {
212         mModeCache = mode;
213         MagnificationCapabilities.setCapabilities(mContext, mModeCache);
214         mModePreference.setSummary(
215                 MagnificationCapabilities.getSummary(mContext, mModeCache));
216     }
217 
onMagnificationModeSelected(AdapterView<?> parent, View view, int position, long id)218     private void onMagnificationModeSelected(AdapterView<?> parent, View view, int position,
219             long id) {
220         final MagnificationModeInfo modeInfo =
221                 (MagnificationModeInfo) mMagnificationModesListView.getItemAtPosition(
222                         position);
223         if (modeInfo.mMagnificationMode == mModeCache) {
224             return;
225         }
226         mModeCache = modeInfo.mMagnificationMode;
227     }
228 
computeSelectionIndex()229     private int computeSelectionIndex() {
230         final int modesSize = mModeInfos.size();
231         for (int i = 0; i < modesSize; i++) {
232             if (mModeInfos.get(i).mMagnificationMode == mModeCache) {
233                 return i + mMagnificationModesListView.getHeaderViewsCount();
234             }
235         }
236         Log.w(TAG, "computeSelectionIndex failed");
237         return 0;
238     }
239 
240     @VisibleForTesting
isTripleTapEnabled(Context context)241     static boolean isTripleTapEnabled(Context context) {
242         return Settings.Secure.getInt(context.getContentResolver(),
243                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, OFF) == ON;
244     }
245 
createMagnificationTripleTapWarningDialog()246     private Dialog createMagnificationTripleTapWarningDialog() {
247         final View contentView = LayoutInflater.from(mContext).inflate(
248                 R.layout.magnification_triple_tap_warning_dialog, /* root= */ null);
249         final CharSequence title = mContext.getString(
250                 R.string.accessibility_magnification_triple_tap_warning_title);
251         final CharSequence positiveBtnText = mContext.getString(
252                 R.string.accessibility_magnification_triple_tap_warning_positive_button);
253         final CharSequence negativeBtnText = mContext.getString(
254                 R.string.accessibility_magnification_triple_tap_warning_negative_button);
255 
256         final Dialog dialog = AccessibilityDialogUtils.createCustomDialog(mContext, title,
257                 contentView,
258                 positiveBtnText, this::onMagnificationTripleTapWarningDialogPositiveButtonClicked,
259                 negativeBtnText, this::onMagnificationTripleTapWarningDialogNegativeButtonClicked);
260 
261         updateLinkInTripleTapWarningDialog(dialog, contentView);
262 
263         return dialog;
264     }
265 
updateLinkInTripleTapWarningDialog(Dialog dialog, View contentView)266     private void updateLinkInTripleTapWarningDialog(Dialog dialog, View contentView) {
267         final TextView messageView = contentView.findViewById(R.id.message);
268         // TODO(b/225682559): Need to remove performClick() after refactoring accessibility dialog.
269         final View.OnClickListener linkListener = view -> {
270             updateCapabilitiesAndSummary(mModeCache);
271             mLinkPreference.performClick();
272             dialog.dismiss();
273         };
274         final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(
275                 AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener);
276         final CharSequence textWithLink = AnnotationSpan.linkify(mContext.getText(
277                 R.string.accessibility_magnification_triple_tap_warning_message), linkInfo);
278 
279         if (messageView != null) {
280             messageView.setText(textWithLink);
281             messageView.setMovementMethod(LinkMovementMethod.getInstance());
282         }
283         dialog.setContentView(contentView);
284     }
285 
286     @VisibleForTesting
onMagnificationTripleTapWarningDialogNegativeButtonClicked( DialogInterface dialogInterface, int which)287     void onMagnificationTripleTapWarningDialogNegativeButtonClicked(
288             DialogInterface dialogInterface, int which) {
289         mModeCache = MagnificationCapabilities.getCapabilities(mContext);
290         mDialogHelper.showDialog(DIALOG_MAGNIFICATION_MODE);
291     }
292 
293     @VisibleForTesting
onMagnificationTripleTapWarningDialogPositiveButtonClicked( DialogInterface dialogInterface, int which)294     void onMagnificationTripleTapWarningDialogPositiveButtonClicked(
295             DialogInterface dialogInterface, int which) {
296         updateCapabilitiesAndSummary(mModeCache);
297     }
298 
299     /**
300      * An interface to help the delegate to show the dialog. It will be injected to the delegate.
301      */
302     interface DialogHelper extends DialogCreatable {
showDialog(int dialogId)303         void showDialog(int dialogId);
setDialogDelegate(DialogCreatable delegate)304         void setDialogDelegate(DialogCreatable delegate);
305     }
306 
307     @VisibleForTesting
308     static class MagnificationModeInfo extends ItemInfoArrayAdapter.ItemInfo {
309         @MagnificationMode
310         public final int mMagnificationMode;
311 
MagnificationModeInfo(@onNull CharSequence title, @Nullable CharSequence summary, @DrawableRes int drawableId, @MagnificationMode int magnificationMode)312         MagnificationModeInfo(@NonNull CharSequence title, @Nullable CharSequence summary,
313                 @DrawableRes int drawableId, @MagnificationMode int magnificationMode) {
314             super(title, summary, drawableId);
315             mMagnificationMode = magnificationMode;
316         }
317     }
318 }
319