1 /*
2  * Copyright (C) 2018 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.gestures;
18 
19 import static android.os.UserHandle.USER_CURRENT;
20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
21 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
22 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY;
23 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
24 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY;
25 
26 import android.app.settings.SettingsEnums;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.om.IOverlayManager;
31 import android.content.om.OverlayInfo;
32 import android.content.res.Resources;
33 import android.os.Bundle;
34 import android.os.RemoteException;
35 import android.os.ServiceManager;
36 import android.provider.Settings;
37 import android.text.TextUtils;
38 import android.view.accessibility.AccessibilityManager;
39 
40 import androidx.annotation.Nullable;
41 import androidx.annotation.VisibleForTesting;
42 import androidx.preference.PreferenceScreen;
43 
44 import com.android.internal.accessibility.common.ShortcutConstants;
45 import com.android.settings.R;
46 import com.android.settings.accessibility.AccessibilityShortcutsTutorial;
47 import com.android.settings.core.BasePreferenceController;
48 import com.android.settings.core.PreferenceControllerListHelper;
49 import com.android.settings.core.SubSettingLauncher;
50 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
51 import com.android.settings.overlay.FeatureFactory;
52 import com.android.settings.search.BaseSearchIndexProvider;
53 import com.android.settings.support.actionbar.HelpResourceProvider;
54 import com.android.settings.utils.CandidateInfoExtra;
55 import com.android.settings.widget.RadioButtonPickerFragment;
56 import com.android.settingslib.search.SearchIndexable;
57 import com.android.settingslib.search.SearchIndexableRaw;
58 import com.android.settingslib.widget.CandidateInfo;
59 import com.android.settingslib.widget.IllustrationPreference;
60 import com.android.settingslib.widget.SelectorWithWidgetPreference;
61 
62 import java.util.ArrayList;
63 import java.util.List;
64 
65 @SearchIndexable
66 public class SystemNavigationGestureSettings extends RadioButtonPickerFragment implements
67         HelpResourceProvider {
68 
69     @VisibleForTesting
70     static final String KEY_SYSTEM_NAV_3BUTTONS = "system_nav_3buttons";
71     @VisibleForTesting
72     static final String KEY_SYSTEM_NAV_2BUTTONS = "system_nav_2buttons";
73     @VisibleForTesting
74     static final String KEY_SYSTEM_NAV_GESTURAL = "system_nav_gestural";
75 
76     public static final String PREF_KEY_SUGGESTION_COMPLETE =
77             "pref_system_navigation_suggestion_complete";
78 
79     private static final String KEY_SHOW_A11Y_TUTORIAL_DIALOG = "show_a11y_tutorial_dialog_bool";
80 
81     static final String LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher";
82 
83     static final String ACTION_GESTURE_SANDBOX = "com.android.quickstep.action.GESTURE_SANDBOX";
84 
85     final Intent mLaunchSandboxIntent = new Intent(ACTION_GESTURE_SANDBOX)
86             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
87             .putExtra("use_tutorial_menu", true)
88             .setPackage(LAUNCHER_PACKAGE_NAME);
89 
90     private static final int MIN_LARGESCREEN_WIDTH_DP = 600;
91 
92     private boolean mA11yTutorialDialogShown = false;
93 
94     private IOverlayManager mOverlayManager;
95 
96     private IllustrationPreference mVideoPreference;
97 
98     @Override
onCreate(@ullable Bundle savedInstanceState)99     public void onCreate(@Nullable Bundle savedInstanceState) {
100         super.onCreate(savedInstanceState);
101         if (savedInstanceState != null) {
102             mA11yTutorialDialogShown =
103                     savedInstanceState.getBoolean(KEY_SHOW_A11Y_TUTORIAL_DIALOG, false);
104             if (mA11yTutorialDialogShown) {
105                 AccessibilityShortcutsTutorial.showGestureNavigationTutorialDialog(
106                         getContext(), dialog -> mA11yTutorialDialogShown = false);
107             }
108         }
109     }
110 
111     @Override
onSaveInstanceState(Bundle outState)112     public void onSaveInstanceState(Bundle outState) {
113         outState.putBoolean(KEY_SHOW_A11Y_TUTORIAL_DIALOG, mA11yTutorialDialogShown);
114         super.onSaveInstanceState(outState);
115     }
116 
117     @Override
onAttach(Context context)118     public void onAttach(Context context) {
119         super.onAttach(context);
120 
121         SuggestionFeatureProvider suggestionFeatureProvider =
122                 FeatureFactory.getFeatureFactory().getSuggestionFeatureProvider();
123         SharedPreferences prefs = suggestionFeatureProvider.getSharedPrefs(context);
124         prefs.edit().putBoolean(PREF_KEY_SUGGESTION_COMPLETE, true).apply();
125 
126         mOverlayManager = IOverlayManager.Stub.asInterface(
127                 ServiceManager.getService(Context.OVERLAY_SERVICE));
128 
129         mVideoPreference = new IllustrationPreference(context);
130         Context windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
131         if (windowContext.getResources()
132                 .getConfiguration().smallestScreenWidthDp >= MIN_LARGESCREEN_WIDTH_DP) {
133             mVideoPreference.applyDynamicColor();
134         }
135         setIllustrationVideo(mVideoPreference, getDefaultKey());
136         setIllustrationClickListener(mVideoPreference, getDefaultKey());
137 
138         migrateOverlaySensitivityToSettings(context, mOverlayManager);
139     }
140 
141     @Override
getMetricsCategory()142     public int getMetricsCategory() {
143         return SettingsEnums.SETTINGS_GESTURE_SWIPE_UP;
144     }
145 
146     @Override
updateCandidates()147     public void updateCandidates() {
148         final String defaultKey = getDefaultKey();
149         final String systemDefaultKey = getSystemDefaultKey();
150         final PreferenceScreen screen = getPreferenceScreen();
151         screen.removeAll();
152         screen.addPreference(mVideoPreference);
153         addPreferencesFromResource(getPreferenceScreenResId());
154         final List<BasePreferenceController> preferenceControllers = PreferenceControllerListHelper
155                 .getPreferenceControllersFromXml(getContext(), getPreferenceScreenResId());
156         preferenceControllers.forEach(controller -> {
157             controller.updateState(findPreference(controller.getPreferenceKey()));
158             controller.displayPreference(screen);
159         });
160 
161         final List<? extends CandidateInfo> candidateList = getCandidates();
162         if (candidateList == null) {
163             return;
164         }
165         for (CandidateInfo info : candidateList) {
166             SelectorWithWidgetPreference pref = new SelectorWithWidgetPreference(getPrefContext());
167             bindPreference(pref, info.getKey(), info, defaultKey);
168             bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey);
169             screen.addPreference(pref);
170         }
171         mayCheckOnlyRadioButton();
172     }
173 
174     @Override
bindPreferenceExtra(SelectorWithWidgetPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey)175     public void bindPreferenceExtra(SelectorWithWidgetPreference pref,
176             String key, CandidateInfo info, String defaultKey, String systemDefaultKey) {
177         if (!(info instanceof CandidateInfoExtra)) {
178             return;
179         }
180 
181         pref.setSummary(((CandidateInfoExtra) info).loadSummary());
182 
183         if (KEY_SYSTEM_NAV_GESTURAL.equals(info.getKey())) {
184             pref.setExtraWidgetOnClickListener((v) -> startActivity(new Intent(
185                     GestureNavigationSettingsFragment.GESTURE_NAVIGATION_SETTINGS)
186                     .setPackage(getContext().getPackageName())));
187         }
188 
189         if ((KEY_SYSTEM_NAV_2BUTTONS.equals(info.getKey())
190                 || KEY_SYSTEM_NAV_3BUTTONS.equals(info.getKey()))
191                 // Don't add the settings button if that page will be blank.
192                 && !PreferenceControllerListHelper.areAllPreferencesUnavailable(
193                         getContext(), getPreferenceManager(), R.xml.button_navigation_settings)) {
194             pref.setExtraWidgetOnClickListener((v) ->
195                     new SubSettingLauncher(getContext())
196                             .setDestination(ButtonNavigationSettingsFragment.class.getName())
197                             .setSourceMetricsCategory(SettingsEnums.SETTINGS_GESTURE_SWIPE_UP)
198                             .launch());
199         }
200     }
201 
202     @Override
getPreferenceScreenResId()203     protected int getPreferenceScreenResId() {
204         return R.xml.system_navigation_gesture_settings;
205     }
206 
207     @Override
getCandidates()208     protected List<? extends CandidateInfo> getCandidates() {
209         final Context c = getContext();
210         List<CandidateInfoExtra> candidates = new ArrayList<>();
211 
212         if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c,
213                 NAV_BAR_MODE_GESTURAL_OVERLAY)) {
214             candidates.add(new CandidateInfoExtra(
215                     c.getText(R.string.edge_to_edge_navigation_title),
216                     c.getText(R.string.edge_to_edge_navigation_summary),
217                     KEY_SYSTEM_NAV_GESTURAL, true /* enabled */));
218         }
219         if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c,
220                 NAV_BAR_MODE_2BUTTON_OVERLAY)) {
221             candidates.add(new CandidateInfoExtra(
222                     c.getText(R.string.swipe_up_to_switch_apps_title),
223                     c.getText(R.string.swipe_up_to_switch_apps_summary),
224                     KEY_SYSTEM_NAV_2BUTTONS, true /* enabled */));
225         }
226         if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c,
227                 NAV_BAR_MODE_3BUTTON_OVERLAY)) {
228             candidates.add(new CandidateInfoExtra(
229                     c.getText(R.string.legacy_navigation_title),
230                     c.getText(R.string.legacy_navigation_summary),
231                     KEY_SYSTEM_NAV_3BUTTONS, true /* enabled */));
232         }
233 
234         return candidates;
235     }
236 
237     @Override
getDefaultKey()238     protected String getDefaultKey() {
239         return getCurrentSystemNavigationMode(getContext());
240     }
241 
242     @Override
setDefaultKey(String key)243     protected boolean setDefaultKey(String key) {
244         setCurrentSystemNavigationMode(mOverlayManager, key);
245         setIllustrationVideo(mVideoPreference, key);
246         setGestureNavigationTutorialDialog(key);
247         setIllustrationClickListener(mVideoPreference, key);
248         return true;
249     }
250 
isGestureTutorialAvailable()251     private boolean isGestureTutorialAvailable() {
252         Context context = getContext();
253         return context != null
254                 && mLaunchSandboxIntent.resolveActivity(context.getPackageManager()) != null;
255     }
256 
setIllustrationClickListener(IllustrationPreference videoPref, String systemNavKey)257     private void setIllustrationClickListener(IllustrationPreference videoPref,
258             String systemNavKey) {
259 
260         switch (systemNavKey) {
261             case KEY_SYSTEM_NAV_GESTURAL:
262                 if (isGestureTutorialAvailable()){
263                     videoPref.setContentDescription(R.string.nav_tutorial_button_description);
264                     videoPref.setOnPreferenceClickListener(preference -> {
265                         startActivity(mLaunchSandboxIntent);
266                         return true;
267                     });
268                 } else {
269                     videoPref.setOnPreferenceClickListener(null);
270                 }
271 
272                 break;
273             case KEY_SYSTEM_NAV_2BUTTONS:
274             case KEY_SYSTEM_NAV_3BUTTONS:
275             default:
276                 videoPref.setOnPreferenceClickListener(null);
277                 break;
278         }
279     }
280 
migrateOverlaySensitivityToSettings(Context context, IOverlayManager overlayManager)281     static void migrateOverlaySensitivityToSettings(Context context,
282             IOverlayManager overlayManager) {
283         if (!SystemNavigationPreferenceController.isGestureNavigationEnabled(context)) {
284             return;
285         }
286 
287         OverlayInfo info = null;
288         try {
289             info = overlayManager.getOverlayInfo(NAV_BAR_MODE_GESTURAL_OVERLAY, USER_CURRENT);
290         } catch (RemoteException e) { /* Do nothing */ }
291         if (info != null && !info.isEnabled()) {
292             // Enable the default gesture nav overlay. Back sensitivity for left and right are
293             // stored as separate settings values, and other gesture nav overlays are deprecated.
294             setCurrentSystemNavigationMode(overlayManager, KEY_SYSTEM_NAV_GESTURAL);
295             Settings.Secure.putFloat(context.getContentResolver(),
296                     Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, 1.0f);
297             Settings.Secure.putFloat(context.getContentResolver(),
298                     Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, 1.0f);
299         }
300     }
301 
302     @VisibleForTesting
getCurrentSystemNavigationMode(Context context)303     static String getCurrentSystemNavigationMode(Context context) {
304         if (SystemNavigationPreferenceController.isGestureNavigationEnabled(context)) {
305             return KEY_SYSTEM_NAV_GESTURAL;
306         } else if (SystemNavigationPreferenceController.is2ButtonNavigationEnabled(context)) {
307             return KEY_SYSTEM_NAV_2BUTTONS;
308         } else {
309             return KEY_SYSTEM_NAV_3BUTTONS;
310         }
311     }
312 
313     @VisibleForTesting
setCurrentSystemNavigationMode(IOverlayManager overlayManager, String key)314     static void setCurrentSystemNavigationMode(IOverlayManager overlayManager, String key) {
315         String overlayPackage = NAV_BAR_MODE_GESTURAL_OVERLAY;
316         switch (key) {
317             case KEY_SYSTEM_NAV_GESTURAL:
318                 overlayPackage = NAV_BAR_MODE_GESTURAL_OVERLAY;
319                 break;
320             case KEY_SYSTEM_NAV_2BUTTONS:
321                 overlayPackage = NAV_BAR_MODE_2BUTTON_OVERLAY;
322                 break;
323             case KEY_SYSTEM_NAV_3BUTTONS:
324                 overlayPackage = NAV_BAR_MODE_3BUTTON_OVERLAY;
325                 break;
326         }
327 
328         try {
329             overlayManager.setEnabledExclusiveInCategory(overlayPackage, USER_CURRENT);
330         } catch (RemoteException e) {
331             throw e.rethrowFromSystemServer();
332         }
333     }
334 
setIllustrationVideo(IllustrationPreference videoPref, String systemNavKey)335     private void setIllustrationVideo(IllustrationPreference videoPref,
336             String systemNavKey) {
337         switch (systemNavKey) {
338             case KEY_SYSTEM_NAV_GESTURAL:
339                 if (isGestureTutorialAvailable()) {
340                     videoPref.setLottieAnimationResId(
341                             R.raw.lottie_system_nav_fully_gestural_with_nav);
342                 } else {
343                     videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_fully_gestural);
344                 }
345                 break;
346             case KEY_SYSTEM_NAV_2BUTTONS:
347                 videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_2_button);
348                 break;
349             case KEY_SYSTEM_NAV_3BUTTONS:
350                 videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_3_button);
351                 break;
352         }
353     }
354 
setGestureNavigationTutorialDialog(String systemNavKey)355     private void setGestureNavigationTutorialDialog(String systemNavKey) {
356         if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, systemNavKey)
357                 && !isAccessibilityFloatingMenuEnabled()
358                 && (isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) {
359             mA11yTutorialDialogShown = true;
360             AccessibilityShortcutsTutorial.showGestureNavigationTutorialDialog(getContext(),
361                     dialog -> mA11yTutorialDialogShown = false);
362         } else {
363             mA11yTutorialDialogShown = false;
364         }
365     }
366 
isAnyServiceSupportAccessibilityButton()367     private boolean isAnyServiceSupportAccessibilityButton() {
368         final AccessibilityManager ams = getContext().getSystemService(AccessibilityManager.class);
369         final List<String> targets = ams.getAccessibilityShortcutTargets(
370                 ShortcutConstants.UserShortcutType.SOFTWARE);
371         return !targets.isEmpty();
372     }
373 
isNavBarMagnificationEnabled()374     private boolean isNavBarMagnificationEnabled() {
375         return Settings.Secure.getInt(getContext().getContentResolver(),
376                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1;
377     }
378 
isAccessibilityFloatingMenuEnabled()379     private boolean isAccessibilityFloatingMenuEnabled() {
380         return Settings.Secure.getInt(getContext().getContentResolver(),
381                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1)
382                 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
383     }
384 
385     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
386             new BaseSearchIndexProvider(R.xml.system_navigation_gesture_settings) {
387 
388                 @Override
389                 protected boolean isPageSearchEnabled(Context context) {
390                     return SystemNavigationPreferenceController.isGestureAvailable(context);
391                 }
392 
393                 @Override
394                 public List<SearchIndexableRaw> getRawDataToIndex(Context context,
395                         boolean enabled) {
396                     final Resources res = context.getResources();
397                     final List<SearchIndexableRaw> result = new ArrayList<>();
398 
399                     if (SystemNavigationPreferenceController.isOverlayPackageAvailable(context,
400                             NAV_BAR_MODE_GESTURAL_OVERLAY)) {
401                         SearchIndexableRaw data = new SearchIndexableRaw(context);
402                         data.title = res.getString(R.string.edge_to_edge_navigation_title);
403                         data.key = KEY_SYSTEM_NAV_GESTURAL;
404                         result.add(data);
405                     }
406 
407                     if (SystemNavigationPreferenceController.isOverlayPackageAvailable(context,
408                             NAV_BAR_MODE_2BUTTON_OVERLAY)) {
409                         SearchIndexableRaw data = new SearchIndexableRaw(context);
410                         data.title = res.getString(R.string.swipe_up_to_switch_apps_title);
411                         data.key = KEY_SYSTEM_NAV_2BUTTONS;
412                         result.add(data);
413                     }
414 
415                     if (SystemNavigationPreferenceController.isOverlayPackageAvailable(context,
416                             NAV_BAR_MODE_3BUTTON_OVERLAY)) {
417                         SearchIndexableRaw data = new SearchIndexableRaw(context);
418                         data.title = res.getString(R.string.legacy_navigation_title);
419                         data.key = KEY_SYSTEM_NAV_3BUTTONS;
420                         data.keywords = res.getString(R.string.keywords_3_button_navigation);
421                         result.add(data);
422                     }
423 
424                     return result;
425                 }
426             };
427 
428     // From HelpResourceProvider
429     @Override
getHelpResource()430     public int getHelpResource() {
431         // TODO(b/146001201): Replace with system navigation help page when ready.
432         return R.string.help_uri_default;
433     }
434 }
435