1 /* 2 * Copyright (C) 2015 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.launcher3.settings; 18 19 import static android.provider.Settings.Global.DEVELOPMENT_SETTINGS_ENABLED; 20 21 import static androidx.preference.PreferenceFragmentCompat.ARG_PREFERENCE_ROOT; 22 23 import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE; 24 import static com.android.launcher3.BuildConfig.IS_STUDIO_BUILD; 25 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; 26 import static com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET; 27 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; 28 29 import android.app.Activity; 30 import android.content.Intent; 31 import android.content.pm.ActivityInfo; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.provider.Settings; 35 import android.text.TextUtils; 36 import android.view.MenuItem; 37 import android.view.View; 38 39 import androidx.annotation.Nullable; 40 import androidx.annotation.VisibleForTesting; 41 import androidx.core.view.WindowCompat; 42 import androidx.fragment.app.DialogFragment; 43 import androidx.fragment.app.Fragment; 44 import androidx.fragment.app.FragmentActivity; 45 import androidx.fragment.app.FragmentManager; 46 import androidx.preference.Preference; 47 import androidx.preference.PreferenceFragmentCompat; 48 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback; 49 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback; 50 import androidx.preference.PreferenceGroup; 51 import androidx.preference.PreferenceGroup.PreferencePositionCallback; 52 import androidx.preference.PreferenceScreen; 53 import androidx.recyclerview.widget.RecyclerView; 54 55 import com.android.launcher3.BuildConfig; 56 import com.android.launcher3.Flags; 57 import com.android.launcher3.InvariantDeviceProfile; 58 import com.android.launcher3.LauncherFiles; 59 import com.android.launcher3.R; 60 import com.android.launcher3.states.RotationHelper; 61 import com.android.launcher3.util.DisplayController; 62 import com.android.launcher3.util.SettingsCache; 63 64 /** 65 * Settings activity for Launcher. Currently implements the following setting: Allow rotation 66 */ 67 public class SettingsActivity extends FragmentActivity 68 implements OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback { 69 70 @VisibleForTesting 71 static final String DEVELOPER_OPTIONS_KEY = "pref_developer_options"; 72 73 public static final String FIXED_LANDSCAPE_MODE = "pref_fixed_landscape_mode"; 74 75 private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging"; 76 77 public static final String EXTRA_FRAGMENT_ARGS = ":settings:fragment_args"; 78 79 // Intent extra to indicate the pref-key to highlighted when opening the settings activity 80 public static final String EXTRA_FRAGMENT_HIGHLIGHT_KEY = ":settings:fragment_args_key"; 81 // Intent extra to indicate the pref-key of the root screen when opening the settings activity 82 public static final String EXTRA_FRAGMENT_ROOT_KEY = ARG_PREFERENCE_ROOT; 83 84 private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600; 85 public static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted"; 86 87 @Override onCreate(Bundle savedInstanceState)88 protected void onCreate(Bundle savedInstanceState) { 89 super.onCreate(savedInstanceState); 90 setContentView(R.layout.settings_activity); 91 92 setActionBar(findViewById(R.id.action_bar)); 93 WindowCompat.setDecorFitsSystemWindows(getWindow(), false); 94 95 Intent intent = getIntent(); 96 if (intent.hasExtra(EXTRA_FRAGMENT_ROOT_KEY) || intent.hasExtra(EXTRA_FRAGMENT_ARGS) 97 || intent.hasExtra(EXTRA_FRAGMENT_HIGHLIGHT_KEY)) { 98 getActionBar().setDisplayHomeAsUpEnabled(true); 99 } 100 101 if (savedInstanceState == null) { 102 Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS); 103 if (args == null) { 104 args = new Bundle(); 105 } 106 107 String highlight = intent.getStringExtra(EXTRA_FRAGMENT_HIGHLIGHT_KEY); 108 if (!TextUtils.isEmpty(highlight)) { 109 args.putString(EXTRA_FRAGMENT_HIGHLIGHT_KEY, highlight); 110 } 111 String root = intent.getStringExtra(EXTRA_FRAGMENT_ROOT_KEY); 112 if (!TextUtils.isEmpty(root)) { 113 args.putString(EXTRA_FRAGMENT_ROOT_KEY, root); 114 } 115 116 final FragmentManager fm = getSupportFragmentManager(); 117 final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(), 118 getString(R.string.settings_fragment_name)); 119 f.setArguments(args); 120 // Display the fragment as the main content. 121 fm.beginTransaction().replace(R.id.content_frame, f).commit(); 122 } 123 } 124 startPreference(String fragment, Bundle args, String key)125 private boolean startPreference(String fragment, Bundle args, String key) { 126 if (getSupportFragmentManager().isStateSaved()) { 127 // Sometimes onClick can come after onPause because of being posted on the handler. 128 // Skip starting new preferences in that case. 129 return false; 130 } 131 final FragmentManager fm = getSupportFragmentManager(); 132 final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(), fragment); 133 if (f instanceof DialogFragment) { 134 f.setArguments(args); 135 ((DialogFragment) f).show(fm, key); 136 } else { 137 startActivity(new Intent(this, SettingsActivity.class) 138 .putExtra(EXTRA_FRAGMENT_ARGS, args)); 139 } 140 return true; 141 } 142 143 @Override onPreferenceStartFragment( PreferenceFragmentCompat preferenceFragment, Preference pref)144 public boolean onPreferenceStartFragment( 145 PreferenceFragmentCompat preferenceFragment, Preference pref) { 146 return startPreference(pref.getFragment(), pref.getExtras(), pref.getKey()); 147 } 148 149 @Override onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref)150 public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) { 151 Bundle args = new Bundle(); 152 args.putString(ARG_PREFERENCE_ROOT, pref.getKey()); 153 return startPreference(getString(R.string.settings_fragment_name), args, pref.getKey()); 154 } 155 156 @Override onOptionsItemSelected(MenuItem item)157 public boolean onOptionsItemSelected(MenuItem item) { 158 if (item.getItemId() == android.R.id.home) { 159 onBackPressed(); 160 return true; 161 } 162 return super.onOptionsItemSelected(item); 163 } 164 165 /** 166 * This fragment shows the launcher preferences. 167 */ 168 public static class LauncherSettingsFragment extends PreferenceFragmentCompat implements 169 SettingsCache.OnChangeListener { 170 171 protected boolean mDeveloperOptionsEnabled = false; 172 173 private boolean mRestartOnResume = false; 174 175 private String mHighLightKey; 176 177 private boolean mPreferenceHighlighted = false; 178 179 @Override onCreate(@ullable Bundle savedInstanceState)180 public void onCreate(@Nullable Bundle savedInstanceState) { 181 if (BuildConfig.IS_DEBUG_DEVICE) { 182 Uri devUri = Settings.Global.getUriFor(DEVELOPMENT_SETTINGS_ENABLED); 183 SettingsCache settingsCache = SettingsCache.INSTANCE.get(getContext()); 184 mDeveloperOptionsEnabled = settingsCache.getValue(devUri); 185 settingsCache.register(devUri, this); 186 } 187 super.onCreate(savedInstanceState); 188 } 189 190 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)191 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 192 final Bundle args = getArguments(); 193 mHighLightKey = args == null ? null : args.getString(EXTRA_FRAGMENT_HIGHLIGHT_KEY); 194 195 if (savedInstanceState != null) { 196 mPreferenceHighlighted = savedInstanceState.getBoolean(SAVE_HIGHLIGHTED_KEY); 197 } 198 199 getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY); 200 setPreferencesFromResource(R.xml.launcher_preferences, rootKey); 201 202 PreferenceScreen screen = getPreferenceScreen(); 203 for (int i = screen.getPreferenceCount() - 1; i >= 0; i--) { 204 Preference preference = screen.getPreference(i); 205 if (!initPreference(preference)) { 206 screen.removePreference(preference); 207 } 208 } 209 210 // If the target preference is not in the current preference screen, find the parent 211 // preference screen that contains the target preference and set it as the preference 212 // screen. 213 if (Flags.navigateToChildPreference() 214 && mHighLightKey != null 215 && !isKeyInPreferenceGroup(mHighLightKey, screen)) { 216 final PreferenceScreen parentPreferenceScreen = 217 findParentPreference(screen, mHighLightKey); 218 if (parentPreferenceScreen != null && getActivity() != null) { 219 if (!TextUtils.isEmpty(parentPreferenceScreen.getTitle())) { 220 getActivity().setTitle(parentPreferenceScreen.getTitle()); 221 } 222 setPreferenceScreen(parentPreferenceScreen); 223 return; 224 } 225 } 226 227 if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) { 228 getActivity().setTitle(getPreferenceScreen().getTitle()); 229 } 230 } 231 isKeyInPreferenceGroup(String targetKey, PreferenceGroup parent)232 private boolean isKeyInPreferenceGroup(String targetKey, PreferenceGroup parent) { 233 for (int i = 0; i < parent.getPreferenceCount(); i++) { 234 Preference pref = parent.getPreference(i); 235 if (pref.getKey() != null && pref.getKey().equals(targetKey)) { 236 return true; 237 } 238 } 239 return false; 240 } 241 242 /** 243 * Finds the parent preference screen for the given target key. 244 * 245 * @param parent the parent preference screen 246 * @param targetKey the key of the preference to find 247 * @return the parent preference screen that contains the target preference 248 */ 249 @Nullable findParentPreference(PreferenceScreen parent, String targetKey)250 private PreferenceScreen findParentPreference(PreferenceScreen parent, String targetKey) { 251 for (int i = 0; i < parent.getPreferenceCount(); i++) { 252 Preference pref = parent.getPreference(i); 253 if (pref instanceof PreferenceScreen) { 254 PreferenceScreen foundKey = findParentPreference((PreferenceScreen) pref, 255 targetKey); 256 if (foundKey != null) { 257 return foundKey; 258 } 259 } else if (pref.getKey() != null && pref.getKey().equals(targetKey)) { 260 return parent; 261 } 262 } 263 return null; 264 } 265 266 @Override onViewCreated(View view, Bundle savedInstanceState)267 public void onViewCreated(View view, Bundle savedInstanceState) { 268 super.onViewCreated(view, savedInstanceState); 269 View listView = getListView(); 270 final int bottomPadding = listView.getPaddingBottom(); 271 listView.setOnApplyWindowInsetsListener((v, insets) -> { 272 v.setPadding( 273 v.getPaddingLeft(), 274 v.getPaddingTop(), 275 v.getPaddingRight(), 276 bottomPadding + insets.getSystemWindowInsetBottom()); 277 return insets.consumeSystemWindowInsets(); 278 }); 279 280 // Overriding Text Direction in the Androidx preference library to support RTL 281 view.setTextDirection(View.TEXT_DIRECTION_LOCALE); 282 } 283 284 @Override onSaveInstanceState(Bundle outState)285 public void onSaveInstanceState(Bundle outState) { 286 super.onSaveInstanceState(outState); 287 outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted); 288 } 289 290 /** 291 * Initializes a preference. This is called for every preference. Returning false here 292 * will remove that preference from the list. 293 */ initPreference(Preference preference)294 protected boolean initPreference(Preference preference) { 295 DisplayController.Info info = DisplayController.INSTANCE.get(getContext()).getInfo(); 296 switch (preference.getKey()) { 297 case NOTIFICATION_DOTS_PREFERENCE_KEY: 298 return BuildConfig.NOTIFICATION_DOTS_ENABLED; 299 case ALLOW_ROTATION_PREFERENCE_KEY: 300 if (Flags.oneGridSpecs()) { 301 return false; 302 } 303 if (info.isTablet(info.realBounds)) { 304 // Launcher supports rotation by default. No need to show this setting. 305 return false; 306 } 307 // Initialize the UI once 308 preference.setDefaultValue(RotationHelper.getAllowRotationDefaultValue(info)); 309 return true; 310 case DEVELOPER_OPTIONS_KEY: 311 if (IS_STUDIO_BUILD) { 312 preference.setOrder(0); 313 } 314 return mDeveloperOptionsEnabled; 315 case FIXED_LANDSCAPE_MODE: 316 if (!Flags.oneGridSpecs() 317 // adding this condition until fixing b/378972567 318 || InvariantDeviceProfile.INSTANCE.get(getContext()).deviceType 319 == TYPE_MULTI_DISPLAY 320 || InvariantDeviceProfile.INSTANCE.get(getContext()).deviceType 321 == TYPE_TABLET) { 322 return false; 323 } 324 // When the setting changes rotate the screen accordingly to showcase the result 325 // of the setting 326 preference.setOnPreferenceChangeListener( 327 (pref, newValue) -> { 328 getActivity().setRequestedOrientation( 329 (boolean) newValue 330 ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 331 : ActivityInfo.SCREEN_ORIENTATION_USER 332 ); 333 return true; 334 } 335 ); 336 return !info.isTablet(info.realBounds); 337 } 338 return true; 339 } 340 341 @Override onResume()342 public void onResume() { 343 super.onResume(); 344 345 if (isAdded() && !mPreferenceHighlighted) { 346 PreferenceHighlighter highlighter = createHighlighter(); 347 if (highlighter != null) { 348 getView().postDelayed(highlighter, DELAY_HIGHLIGHT_DURATION_MILLIS); 349 mPreferenceHighlighted = true; 350 } 351 } 352 353 if (mRestartOnResume) { 354 recreateActivityNow(); 355 } 356 } 357 358 @Override onSettingsChanged(boolean isEnabled)359 public void onSettingsChanged(boolean isEnabled) { 360 // Developer options changed, try recreate 361 tryRecreateActivity(); 362 } 363 364 @Override onDestroy()365 public void onDestroy() { 366 super.onDestroy(); 367 if (IS_DEBUG_DEVICE) { 368 SettingsCache.INSTANCE.get(getContext()) 369 .unregister(Settings.Global.getUriFor(DEVELOPMENT_SETTINGS_ENABLED), this); 370 } 371 } 372 373 /** 374 * Tries to recreate the preference 375 */ tryRecreateActivity()376 protected void tryRecreateActivity() { 377 if (isResumed()) { 378 recreateActivityNow(); 379 } else { 380 mRestartOnResume = true; 381 } 382 } 383 recreateActivityNow()384 private void recreateActivityNow() { 385 Activity activity = getActivity(); 386 if (activity != null) { 387 activity.recreate(); 388 } 389 } 390 createHighlighter()391 private PreferenceHighlighter createHighlighter() { 392 if (TextUtils.isEmpty(mHighLightKey)) { 393 return null; 394 } 395 396 PreferenceScreen screen = getPreferenceScreen(); 397 if (screen == null) { 398 return null; 399 } 400 401 RecyclerView list = getListView(); 402 PreferencePositionCallback callback = (PreferencePositionCallback) list.getAdapter(); 403 int position = callback.getPreferenceAdapterPosition(mHighLightKey); 404 return position >= 0 ? new PreferenceHighlighter( 405 list, position, screen.findPreference(mHighLightKey)) 406 : null; 407 } 408 } 409 } 410