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.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY; 21 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; 22 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; 23 24 import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_GONE; 25 import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_INFO; 26 import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_SETTING; 27 28 import android.accessibilityservice.AccessibilityServiceInfo; 29 import android.app.settings.SettingsEnums; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.SharedPreferences; 33 import android.content.om.IOverlayManager; 34 import android.content.om.OverlayInfo; 35 import android.graphics.drawable.Drawable; 36 import android.os.RemoteException; 37 import android.os.ServiceManager; 38 import android.provider.SearchIndexableResource; 39 import android.provider.Settings; 40 import android.text.TextUtils; 41 import android.view.accessibility.AccessibilityManager; 42 43 import androidx.annotation.VisibleForTesting; 44 import androidx.preference.PreferenceScreen; 45 46 import com.android.settings.SettingsTutorialDialogWrapperActivity; 47 import com.android.settings.R; 48 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; 49 import com.android.settings.overlay.FeatureFactory; 50 import com.android.settings.search.BaseSearchIndexProvider; 51 import com.android.settings.search.Indexable; 52 import com.android.settings.widget.RadioButtonPickerFragment; 53 import com.android.settings.widget.RadioButtonPreference; 54 import com.android.settings.widget.RadioButtonPreferenceWithExtraWidget; 55 import com.android.settings.widget.VideoPreference; 56 import com.android.settingslib.search.SearchIndexable; 57 import com.android.settingslib.widget.CandidateInfo; 58 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.List; 62 63 @SearchIndexable 64 public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { 65 66 private static final String TAG = "SystemNavigationGesture"; 67 68 @VisibleForTesting 69 static final String SHARED_PREFERENCES_NAME = "system_navigation_settings_preferences"; 70 @VisibleForTesting 71 static final String PREFS_BACK_SENSITIVITY_KEY = "system_navigation_back_sensitivity"; 72 73 @VisibleForTesting 74 static final String KEY_SYSTEM_NAV_3BUTTONS = "system_nav_3buttons"; 75 @VisibleForTesting 76 static final String KEY_SYSTEM_NAV_2BUTTONS = "system_nav_2buttons"; 77 @VisibleForTesting 78 static final String KEY_SYSTEM_NAV_GESTURAL = "system_nav_gestural"; 79 80 public static final String PREF_KEY_SUGGESTION_COMPLETE = 81 "pref_system_navigation_suggestion_complete"; 82 83 @VisibleForTesting 84 static final String NAV_BAR_MODE_GESTURAL_OVERLAY_NARROW_BACK 85 = "com.android.internal.systemui.navbar.gestural_narrow_back"; 86 @VisibleForTesting 87 static final String NAV_BAR_MODE_GESTURAL_OVERLAY_WIDE_BACK 88 = "com.android.internal.systemui.navbar.gestural_wide_back"; 89 @VisibleForTesting 90 static final String NAV_BAR_MODE_GESTURAL_OVERLAY_EXTRA_WIDE_BACK 91 = "com.android.internal.systemui.navbar.gestural_extra_wide_back"; 92 @VisibleForTesting 93 static final String[] BACK_GESTURE_INSET_OVERLAYS = { 94 NAV_BAR_MODE_GESTURAL_OVERLAY_NARROW_BACK, 95 NAV_BAR_MODE_GESTURAL_OVERLAY, 96 NAV_BAR_MODE_GESTURAL_OVERLAY_WIDE_BACK, 97 NAV_BAR_MODE_GESTURAL_OVERLAY_EXTRA_WIDE_BACK 98 }; 99 @VisibleForTesting 100 static int BACK_GESTURE_INSET_DEFAULT_OVERLAY = 1; 101 102 private IOverlayManager mOverlayManager; 103 104 private VideoPreference mVideoPreference; 105 106 @Override onAttach(Context context)107 public void onAttach(Context context) { 108 super.onAttach(context); 109 SuggestionFeatureProvider suggestionFeatureProvider = FeatureFactory.getFactory(context) 110 .getSuggestionFeatureProvider(context); 111 SharedPreferences prefs = suggestionFeatureProvider.getSharedPrefs(context); 112 prefs.edit().putBoolean(PREF_KEY_SUGGESTION_COMPLETE, true).apply(); 113 114 mOverlayManager = IOverlayManager.Stub.asInterface( 115 ServiceManager.getService(Context.OVERLAY_SERVICE)); 116 117 mVideoPreference = new VideoPreference(context); 118 setIllustrationVideo(mVideoPreference, getDefaultKey()); 119 mVideoPreference.setHeight( /* Illustration height in dp */ 120 getResources().getDimension(R.dimen.system_navigation_illustration_height) 121 / getResources().getDisplayMetrics().density); 122 } 123 124 @Override getMetricsCategory()125 public int getMetricsCategory() { 126 return SettingsEnums.SETTINGS_GESTURE_SWIPE_UP; 127 } 128 129 @Override updateCandidates()130 public void updateCandidates() { 131 final String defaultKey = getDefaultKey(); 132 final String systemDefaultKey = getSystemDefaultKey(); 133 final PreferenceScreen screen = getPreferenceScreen(); 134 screen.removeAll(); 135 screen.addPreference(mVideoPreference); 136 137 final List<? extends CandidateInfo> candidateList = getCandidates(); 138 if (candidateList == null) { 139 return; 140 } 141 for (CandidateInfo info : candidateList) { 142 RadioButtonPreferenceWithExtraWidget pref = 143 new RadioButtonPreferenceWithExtraWidget(getPrefContext()); 144 bindPreference(pref, info.getKey(), info, defaultKey); 145 bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey); 146 screen.addPreference(pref); 147 } 148 mayCheckOnlyRadioButton(); 149 } 150 151 @Override bindPreferenceExtra(RadioButtonPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey)152 public void bindPreferenceExtra(RadioButtonPreference pref, 153 String key, CandidateInfo info, String defaultKey, String systemDefaultKey) { 154 if (!(info instanceof NavModeCandidateInfo) 155 || !(pref instanceof RadioButtonPreferenceWithExtraWidget)) { 156 return; 157 } 158 159 pref.setSummary(((NavModeCandidateInfo) info).loadSummary()); 160 161 RadioButtonPreferenceWithExtraWidget p = (RadioButtonPreferenceWithExtraWidget) pref; 162 if (info.getKey() == KEY_SYSTEM_NAV_GESTURAL) { 163 if (SystemNavigationPreferenceController.isGestureNavSupportedByDefaultLauncher( 164 getContext())) { 165 p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_SETTING); 166 p.setExtraWidgetOnClickListener((v) -> GestureNavigationBackSensitivityDialog 167 .show(this, getBackSensitivity(getContext(), mOverlayManager))); 168 } else { 169 p.setEnabled(false); 170 p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_INFO); 171 p.setExtraWidgetOnClickListener((v) -> 172 GestureNavigationNotAvailableDialog.show(this)); 173 } 174 } else { 175 p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_GONE); 176 } 177 } 178 179 @Override getPreferenceScreenResId()180 protected int getPreferenceScreenResId() { 181 return R.xml.system_navigation_gesture_settings; 182 } 183 184 @Override getCandidates()185 protected List<? extends CandidateInfo> getCandidates() { 186 final Context c = getContext(); 187 List<NavModeCandidateInfo> candidates = new ArrayList<>(); 188 189 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, 190 NAV_BAR_MODE_GESTURAL_OVERLAY)) { 191 candidates.add(new NavModeCandidateInfo( 192 c.getText(R.string.edge_to_edge_navigation_title), 193 c.getText(R.string.edge_to_edge_navigation_summary), 194 KEY_SYSTEM_NAV_GESTURAL, true /* enabled */)); 195 } 196 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, 197 NAV_BAR_MODE_2BUTTON_OVERLAY)) { 198 candidates.add(new NavModeCandidateInfo( 199 c.getText(R.string.swipe_up_to_switch_apps_title), 200 c.getText(R.string.swipe_up_to_switch_apps_summary), 201 KEY_SYSTEM_NAV_2BUTTONS, true /* enabled */)); 202 } 203 if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, 204 NAV_BAR_MODE_3BUTTON_OVERLAY)) { 205 candidates.add(new NavModeCandidateInfo( 206 c.getText(R.string.legacy_navigation_title), 207 c.getText(R.string.legacy_navigation_summary), 208 KEY_SYSTEM_NAV_3BUTTONS, true /* enabled */)); 209 } 210 211 return candidates; 212 } 213 214 @Override getDefaultKey()215 protected String getDefaultKey() { 216 return getCurrentSystemNavigationMode(getContext()); 217 } 218 219 @Override setDefaultKey(String key)220 protected boolean setDefaultKey(String key) { 221 final Context c = getContext(); 222 if (key == KEY_SYSTEM_NAV_GESTURAL && 223 !SystemNavigationPreferenceController.isGestureNavSupportedByDefaultLauncher(c)) { 224 // This should not happen since the preference is disabled. Return to be safe. 225 return false; 226 } 227 228 setCurrentSystemNavigationMode(c, mOverlayManager, key); 229 setIllustrationVideo(mVideoPreference, key); 230 if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, key) && ( 231 isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) { 232 Intent intent = new Intent(getActivity(), SettingsTutorialDialogWrapperActivity.class); 233 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 234 startActivity(intent); 235 } 236 return true; 237 } 238 239 @VisibleForTesting setBackSensitivity(Context context, IOverlayManager overlayManager, int sensitivity)240 static void setBackSensitivity(Context context, IOverlayManager overlayManager, 241 int sensitivity) { 242 if (sensitivity < 0 || sensitivity >= BACK_GESTURE_INSET_OVERLAYS.length) { 243 throw new IllegalArgumentException("Sensitivity out of range."); 244 } 245 246 // Store the sensitivity level, to be able to restore when user returns to Gesture Nav mode 247 context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE).edit() 248 .putInt(PREFS_BACK_SENSITIVITY_KEY, sensitivity).apply(); 249 if (getCurrentSystemNavigationMode(context) == KEY_SYSTEM_NAV_GESTURAL) { 250 setNavBarInteractionMode(overlayManager, BACK_GESTURE_INSET_OVERLAYS[sensitivity]); 251 } 252 } 253 254 @VisibleForTesting getBackSensitivity(Context context, IOverlayManager overlayManager)255 static int getBackSensitivity(Context context, IOverlayManager overlayManager) { 256 for (int i = 0; i < BACK_GESTURE_INSET_OVERLAYS.length; i++) { 257 OverlayInfo info = null; 258 try { 259 info = overlayManager.getOverlayInfo(BACK_GESTURE_INSET_OVERLAYS[i], USER_CURRENT); 260 } catch (RemoteException e) { /* Do nothing */ } 261 if (info != null && info.isEnabled()) { 262 return i; 263 } 264 } 265 // If Gesture nav is not selected, read the value from shared preferences. 266 return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) 267 .getInt(PREFS_BACK_SENSITIVITY_KEY, BACK_GESTURE_INSET_DEFAULT_OVERLAY); 268 } 269 270 @VisibleForTesting getCurrentSystemNavigationMode(Context context)271 static String getCurrentSystemNavigationMode(Context context) { 272 if (SystemNavigationPreferenceController.isEdgeToEdgeEnabled(context)) { 273 return KEY_SYSTEM_NAV_GESTURAL; 274 } else if (SystemNavigationPreferenceController.isSwipeUpEnabled(context)) { 275 return KEY_SYSTEM_NAV_2BUTTONS; 276 } else { 277 return KEY_SYSTEM_NAV_3BUTTONS; 278 } 279 } 280 281 @VisibleForTesting setCurrentSystemNavigationMode(Context context, IOverlayManager overlayManager, String key)282 static void setCurrentSystemNavigationMode(Context context, IOverlayManager overlayManager, 283 String key) { 284 switch (key) { 285 case KEY_SYSTEM_NAV_GESTURAL: 286 int sensitivity = getBackSensitivity(context, overlayManager); 287 setNavBarInteractionMode(overlayManager, BACK_GESTURE_INSET_OVERLAYS[sensitivity]); 288 break; 289 case KEY_SYSTEM_NAV_2BUTTONS: 290 setNavBarInteractionMode(overlayManager, NAV_BAR_MODE_2BUTTON_OVERLAY); 291 break; 292 case KEY_SYSTEM_NAV_3BUTTONS: 293 setNavBarInteractionMode(overlayManager, NAV_BAR_MODE_3BUTTON_OVERLAY); 294 break; 295 } 296 } 297 setNavBarInteractionMode(IOverlayManager overlayManager, String overlayPackage)298 private static void setNavBarInteractionMode(IOverlayManager overlayManager, 299 String overlayPackage) { 300 try { 301 overlayManager.setEnabledExclusiveInCategory(overlayPackage, USER_CURRENT); 302 } catch (RemoteException e) { 303 throw e.rethrowFromSystemServer(); 304 } 305 } 306 setIllustrationVideo(VideoPreference videoPref, String systemNavKey)307 private static void setIllustrationVideo(VideoPreference videoPref, String systemNavKey) { 308 videoPref.setVideo(0, 0); 309 switch (systemNavKey) { 310 case KEY_SYSTEM_NAV_GESTURAL: 311 videoPref.setVideo(R.raw.system_nav_fully_gestural, 312 R.drawable.system_nav_fully_gestural); 313 break; 314 case KEY_SYSTEM_NAV_2BUTTONS: 315 videoPref.setVideo(R.raw.system_nav_2_button, R.drawable.system_nav_2_button); 316 break; 317 case KEY_SYSTEM_NAV_3BUTTONS: 318 videoPref.setVideo(R.raw.system_nav_3_button, R.drawable.system_nav_3_button); 319 break; 320 } 321 } 322 isAnyServiceSupportAccessibilityButton()323 private boolean isAnyServiceSupportAccessibilityButton() { 324 final AccessibilityManager ams = (AccessibilityManager) getContext().getSystemService( 325 Context.ACCESSIBILITY_SERVICE); 326 final List<AccessibilityServiceInfo> services = ams.getEnabledAccessibilityServiceList( 327 AccessibilityServiceInfo.FEEDBACK_ALL_MASK); 328 329 for (AccessibilityServiceInfo info : services) { 330 if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { 331 return true; 332 } 333 } 334 335 return false; 336 } 337 isNavBarMagnificationEnabled()338 private boolean isNavBarMagnificationEnabled() { 339 return Settings.Secure.getInt(getContext().getContentResolver(), 340 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1; 341 } 342 343 static class NavModeCandidateInfo extends CandidateInfo { 344 private final CharSequence mLabel; 345 private final CharSequence mSummary; 346 private final String mKey; 347 NavModeCandidateInfo(CharSequence label, CharSequence summary, String key, boolean enabled)348 NavModeCandidateInfo(CharSequence label, CharSequence summary, String key, 349 boolean enabled) { 350 super(enabled); 351 mLabel = label; 352 mSummary = summary; 353 mKey = key; 354 } 355 356 @Override loadLabel()357 public CharSequence loadLabel() { 358 return mLabel; 359 } 360 loadSummary()361 public CharSequence loadSummary() { 362 return mSummary; 363 } 364 365 @Override loadIcon()366 public Drawable loadIcon() { 367 return null; 368 } 369 370 @Override getKey()371 public String getKey() { 372 return mKey; 373 } 374 } 375 376 public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 377 new BaseSearchIndexProvider() { 378 @Override 379 public List<SearchIndexableResource> getXmlResourcesToIndex( 380 Context context, boolean enabled) { 381 final SearchIndexableResource sir = new SearchIndexableResource(context); 382 sir.xmlResId = R.xml.system_navigation_gesture_settings; 383 return Arrays.asList(sir); 384 } 385 386 @Override 387 protected boolean isPageSearchEnabled(Context context) { 388 return SystemNavigationPreferenceController.isGestureAvailable(context); 389 } 390 }; 391 } 392