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 private static final int MIN_LARGESCREEN_WIDTH_DP = 600; 82 83 private boolean mA11yTutorialDialogShown = false; 84 85 private IOverlayManager mOverlayManager; 86 87 private IllustrationPreference mVideoPreference; 88 89 @Override onCreate(@ullable Bundle savedInstanceState)90 public void onCreate(@Nullable Bundle savedInstanceState) { 91 super.onCreate(savedInstanceState); 92 if (savedInstanceState != null) { 93 mA11yTutorialDialogShown = 94 savedInstanceState.getBoolean(KEY_SHOW_A11Y_TUTORIAL_DIALOG, false); 95 if (mA11yTutorialDialogShown) { 96 AccessibilityShortcutsTutorial.showGestureNavigationTutorialDialog( 97 getContext(), dialog -> mA11yTutorialDialogShown = false); 98 } 99 } 100 } 101 102 @Override onSaveInstanceState(Bundle outState)103 public void onSaveInstanceState(Bundle outState) { 104 outState.putBoolean(KEY_SHOW_A11Y_TUTORIAL_DIALOG, mA11yTutorialDialogShown); 105 super.onSaveInstanceState(outState); 106 } 107 108 @Override onAttach(Context context)109 public void onAttach(Context context) { 110 super.onAttach(context); 111 112 SuggestionFeatureProvider suggestionFeatureProvider = 113 FeatureFactory.getFeatureFactory().getSuggestionFeatureProvider(); 114 SharedPreferences prefs = suggestionFeatureProvider.getSharedPrefs(context); 115 prefs.edit().putBoolean(PREF_KEY_SUGGESTION_COMPLETE, true).apply(); 116 117 mOverlayManager = IOverlayManager.Stub.asInterface( 118 ServiceManager.getService(Context.OVERLAY_SERVICE)); 119 120 mVideoPreference = new IllustrationPreference(context); 121 Context windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null); 122 if (windowContext.getResources() 123 .getConfiguration().smallestScreenWidthDp >= MIN_LARGESCREEN_WIDTH_DP) { 124 mVideoPreference.applyDynamicColor(); 125 } 126 setIllustrationVideo(mVideoPreference, getDefaultKey()); 127 128 migrateOverlaySensitivityToSettings(context, mOverlayManager); 129 } 130 131 @Override getMetricsCategory()132 public int getMetricsCategory() { 133 return SettingsEnums.SETTINGS_GESTURE_SWIPE_UP; 134 } 135 136 @Override updateCandidates()137 public void updateCandidates() { 138 final String defaultKey = getDefaultKey(); 139 final String systemDefaultKey = getSystemDefaultKey(); 140 final PreferenceScreen screen = getPreferenceScreen(); 141 screen.removeAll(); 142 screen.addPreference(mVideoPreference); 143 144 addPreferencesFromResource(getPreferenceScreenResId()); 145 final List<BasePreferenceController> preferenceControllers = PreferenceControllerListHelper 146 .getPreferenceControllersFromXml(getContext(), getPreferenceScreenResId()); 147 preferenceControllers.forEach(controller -> { 148 controller.updateState(findPreference(controller.getPreferenceKey())); 149 controller.displayPreference(screen); 150 }); 151 152 final List<? extends CandidateInfo> candidateList = getCandidates(); 153 if (candidateList == null) { 154 return; 155 } 156 for (CandidateInfo info : candidateList) { 157 SelectorWithWidgetPreference pref = new SelectorWithWidgetPreference(getPrefContext()); 158 bindPreference(pref, info.getKey(), info, defaultKey); 159 bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey); 160 screen.addPreference(pref); 161 } 162 mayCheckOnlyRadioButton(); 163 } 164 165 @Override bindPreferenceExtra(SelectorWithWidgetPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey)166 public void bindPreferenceExtra(SelectorWithWidgetPreference pref, 167 String key, CandidateInfo info, String defaultKey, String systemDefaultKey) { 168 if (!(info instanceof CandidateInfoExtra)) { 169 return; 170 } 171 172 pref.setSummary(((CandidateInfoExtra) info).loadSummary()); 173 174 if (KEY_SYSTEM_NAV_GESTURAL.equals(info.getKey())) { 175 pref.setExtraWidgetOnClickListener((v) -> startActivity(new Intent( 176 GestureNavigationSettingsFragment.GESTURE_NAVIGATION_SETTINGS) 177 .setPackage(getContext().getPackageName()))); 178 } 179 180 if ((KEY_SYSTEM_NAV_2BUTTONS.equals(info.getKey()) 181 || KEY_SYSTEM_NAV_3BUTTONS.equals(info.getKey())) 182 // Don't add the settings button if that page will be blank. 183 && !PreferenceControllerListHelper.areAllPreferencesUnavailable( 184 getContext(), getPreferenceManager(), R.xml.button_navigation_settings)) { 185 pref.setExtraWidgetOnClickListener((v) -> 186 new SubSettingLauncher(getContext()) 187 .setDestination(ButtonNavigationSettingsFragment.class.getName()) 188 .setSourceMetricsCategory(SettingsEnums.SETTINGS_GESTURE_SWIPE_UP) 189 .launch()); 190 } 191 } 192 193 @Override getPreferenceScreenResId()194 protected int getPreferenceScreenResId() { 195 return R.xml.system_navigation_gesture_settings; 196 } 197 198 @Override getCandidates()199 protected List<? extends CandidateInfo> getCandidates() { 200 final Context c = getContext(); 201 List<CandidateInfoExtra> candidates = new ArrayList<>(); 202 203 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, 204 NAV_BAR_MODE_GESTURAL_OVERLAY)) { 205 candidates.add(new CandidateInfoExtra( 206 c.getText(R.string.edge_to_edge_navigation_title), 207 c.getText(R.string.edge_to_edge_navigation_summary), 208 KEY_SYSTEM_NAV_GESTURAL, true /* enabled */)); 209 } 210 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, 211 NAV_BAR_MODE_2BUTTON_OVERLAY)) { 212 candidates.add(new CandidateInfoExtra( 213 c.getText(R.string.swipe_up_to_switch_apps_title), 214 c.getText(R.string.swipe_up_to_switch_apps_summary), 215 KEY_SYSTEM_NAV_2BUTTONS, true /* enabled */)); 216 } 217 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, 218 NAV_BAR_MODE_3BUTTON_OVERLAY)) { 219 candidates.add(new CandidateInfoExtra( 220 c.getText(R.string.legacy_navigation_title), 221 c.getText(R.string.legacy_navigation_summary), 222 KEY_SYSTEM_NAV_3BUTTONS, true /* enabled */)); 223 } 224 225 return candidates; 226 } 227 228 @Override getDefaultKey()229 protected String getDefaultKey() { 230 return getCurrentSystemNavigationMode(getContext()); 231 } 232 233 @Override setDefaultKey(String key)234 protected boolean setDefaultKey(String key) { 235 setCurrentSystemNavigationMode(mOverlayManager, key); 236 setIllustrationVideo(mVideoPreference, key); 237 if (!android.provider.Flags.a11yStandaloneGestureEnabled()) { 238 setGestureNavigationTutorialDialog(key); 239 } 240 return true; 241 } 242 migrateOverlaySensitivityToSettings(Context context, IOverlayManager overlayManager)243 static void migrateOverlaySensitivityToSettings(Context context, 244 IOverlayManager overlayManager) { 245 if (!SystemNavigationPreferenceController.isGestureNavigationEnabled(context)) { 246 return; 247 } 248 249 OverlayInfo info = null; 250 try { 251 info = overlayManager.getOverlayInfo(NAV_BAR_MODE_GESTURAL_OVERLAY, USER_CURRENT); 252 } catch (RemoteException e) { /* Do nothing */ } 253 if (info != null && !info.isEnabled()) { 254 // Enable the default gesture nav overlay. Back sensitivity for left and right are 255 // stored as separate settings values, and other gesture nav overlays are deprecated. 256 setCurrentSystemNavigationMode(overlayManager, KEY_SYSTEM_NAV_GESTURAL); 257 Settings.Secure.putFloat(context.getContentResolver(), 258 Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, 1.0f); 259 Settings.Secure.putFloat(context.getContentResolver(), 260 Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, 1.0f); 261 } 262 } 263 264 @VisibleForTesting getCurrentSystemNavigationMode(Context context)265 static String getCurrentSystemNavigationMode(Context context) { 266 if (SystemNavigationPreferenceController.isGestureNavigationEnabled(context)) { 267 return KEY_SYSTEM_NAV_GESTURAL; 268 } else if (SystemNavigationPreferenceController.is2ButtonNavigationEnabled(context)) { 269 return KEY_SYSTEM_NAV_2BUTTONS; 270 } else { 271 return KEY_SYSTEM_NAV_3BUTTONS; 272 } 273 } 274 275 @VisibleForTesting setCurrentSystemNavigationMode(IOverlayManager overlayManager, String key)276 static void setCurrentSystemNavigationMode(IOverlayManager overlayManager, String key) { 277 String overlayPackage = NAV_BAR_MODE_GESTURAL_OVERLAY; 278 switch (key) { 279 case KEY_SYSTEM_NAV_GESTURAL: 280 overlayPackage = NAV_BAR_MODE_GESTURAL_OVERLAY; 281 break; 282 case KEY_SYSTEM_NAV_2BUTTONS: 283 overlayPackage = NAV_BAR_MODE_2BUTTON_OVERLAY; 284 break; 285 case KEY_SYSTEM_NAV_3BUTTONS: 286 overlayPackage = NAV_BAR_MODE_3BUTTON_OVERLAY; 287 break; 288 } 289 290 try { 291 overlayManager.setEnabledExclusiveInCategory(overlayPackage, USER_CURRENT); 292 } catch (RemoteException e) { 293 throw e.rethrowFromSystemServer(); 294 } 295 } 296 setIllustrationVideo(IllustrationPreference videoPref, String systemNavKey)297 private void setIllustrationVideo(IllustrationPreference videoPref, 298 String systemNavKey) { 299 switch (systemNavKey) { 300 case KEY_SYSTEM_NAV_GESTURAL: 301 videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_fully_gestural); 302 break; 303 case KEY_SYSTEM_NAV_2BUTTONS: 304 videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_2_button); 305 break; 306 case KEY_SYSTEM_NAV_3BUTTONS: 307 videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_3_button); 308 break; 309 } 310 } 311 setGestureNavigationTutorialDialog(String systemNavKey)312 private void setGestureNavigationTutorialDialog(String systemNavKey) { 313 if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, systemNavKey) 314 && !isAccessibilityFloatingMenuEnabled() 315 && (isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) { 316 mA11yTutorialDialogShown = true; 317 AccessibilityShortcutsTutorial.showGestureNavigationTutorialDialog(getContext(), 318 dialog -> mA11yTutorialDialogShown = false); 319 } else { 320 mA11yTutorialDialogShown = false; 321 } 322 } 323 isAnyServiceSupportAccessibilityButton()324 private boolean isAnyServiceSupportAccessibilityButton() { 325 final AccessibilityManager ams = getContext().getSystemService(AccessibilityManager.class); 326 final List<String> targets = ams.getAccessibilityShortcutTargets( 327 ShortcutConstants.UserShortcutType.SOFTWARE); 328 return !targets.isEmpty(); 329 } 330 isNavBarMagnificationEnabled()331 private boolean isNavBarMagnificationEnabled() { 332 return Settings.Secure.getInt(getContext().getContentResolver(), 333 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1; 334 } 335 isAccessibilityFloatingMenuEnabled()336 private boolean isAccessibilityFloatingMenuEnabled() { 337 return Settings.Secure.getInt(getContext().getContentResolver(), 338 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1) 339 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 340 } 341 342 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 343 new BaseSearchIndexProvider(R.xml.system_navigation_gesture_settings) { 344 345 @Override 346 protected boolean isPageSearchEnabled(Context context) { 347 return SystemNavigationPreferenceController.isGestureAvailable(context); 348 } 349 350 @Override 351 public List<SearchIndexableRaw> getRawDataToIndex(Context context, 352 boolean enabled) { 353 final Resources res = context.getResources(); 354 final List<SearchIndexableRaw> result = new ArrayList<>(); 355 356 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(context, 357 NAV_BAR_MODE_GESTURAL_OVERLAY)) { 358 SearchIndexableRaw data = new SearchIndexableRaw(context); 359 data.title = res.getString(R.string.edge_to_edge_navigation_title); 360 data.key = KEY_SYSTEM_NAV_GESTURAL; 361 result.add(data); 362 } 363 364 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(context, 365 NAV_BAR_MODE_2BUTTON_OVERLAY)) { 366 SearchIndexableRaw data = new SearchIndexableRaw(context); 367 data.title = res.getString(R.string.swipe_up_to_switch_apps_title); 368 data.key = KEY_SYSTEM_NAV_2BUTTONS; 369 result.add(data); 370 } 371 372 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(context, 373 NAV_BAR_MODE_3BUTTON_OVERLAY)) { 374 SearchIndexableRaw data = new SearchIndexableRaw(context); 375 data.title = res.getString(R.string.legacy_navigation_title); 376 data.key = KEY_SYSTEM_NAV_3BUTTONS; 377 data.keywords = res.getString(R.string.keywords_3_button_navigation); 378 result.add(data); 379 } 380 381 return result; 382 } 383 }; 384 385 // From HelpResourceProvider 386 @Override getHelpResource()387 public int getHelpResource() { 388 // TODO(b/146001201): Replace with system navigation help page when ready. 389 return R.string.help_uri_default; 390 } 391 } 392