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