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.homepage; 18 19 import static com.android.settings.search.actionbar.SearchMenuController.NEED_SEARCH_ICON_IN_ACTION_BAR; 20 import static com.android.settingslib.search.SearchIndexable.MOBILE; 21 22 import android.app.ActivityManager; 23 import android.app.settings.SettingsEnums; 24 import android.content.Context; 25 import android.content.res.Configuration; 26 import android.graphics.drawable.Drawable; 27 import android.os.Bundle; 28 import android.provider.SearchIndexableResource; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.ViewGroup; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.fragment.app.Fragment; 37 import androidx.preference.Preference; 38 import androidx.preference.PreferenceCategory; 39 import androidx.preference.PreferenceFragmentCompat; 40 import androidx.preference.PreferenceGroup; 41 import androidx.preference.PreferenceScreen; 42 import androidx.recyclerview.widget.RecyclerView; 43 import androidx.window.embedding.ActivityEmbeddingController; 44 45 import com.android.settings.R; 46 import com.android.settings.Utils; 47 import com.android.settings.activityembedding.ActivityEmbeddingRulesController; 48 import com.android.settings.activityembedding.ActivityEmbeddingUtils; 49 import com.android.settings.core.RoundCornerPreferenceAdapter; 50 import com.android.settings.core.SubSettingLauncher; 51 import com.android.settings.dashboard.DashboardFragment; 52 import com.android.settings.flags.Flags; 53 import com.android.settings.overlay.FeatureFactory; 54 import com.android.settings.search.BaseSearchIndexProvider; 55 import com.android.settings.support.SupportPreferenceController; 56 import com.android.settings.widget.HomepagePreference; 57 import com.android.settings.widget.HomepagePreferenceLayoutHelper.HomepagePreferenceLayout; 58 import com.android.settingslib.core.instrumentation.Instrumentable; 59 import com.android.settingslib.drawer.Tile; 60 import com.android.settingslib.search.SearchIndexable; 61 import com.android.settingslib.widget.SettingsThemeHelper; 62 63 import java.util.List; 64 65 @SearchIndexable(forTarget = MOBILE) 66 public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener, 67 PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { 68 69 private static final String TAG = "TopLevelSettings"; 70 private static final String SAVED_HIGHLIGHT_MIXIN = "highlight_mixin"; 71 private static final String PREF_KEY_SUPPORT = "top_level_support"; 72 73 private boolean mIsEmbeddingActivityEnabled; 74 private TopLevelHighlightMixin mHighlightMixin; 75 private int mPaddingHorizontal; 76 private boolean mScrollNeeded = true; 77 private boolean mFirstStarted = true; 78 private ActivityEmbeddingController mActivityEmbeddingController; 79 TopLevelSettings()80 public TopLevelSettings() { 81 final Bundle args = new Bundle(); 82 // Disable the search icon because this page uses a full search view in actionbar. 83 args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false); 84 setArguments(args); 85 } 86 87 /** Dependency injection ctor only for testing. */ 88 @VisibleForTesting TopLevelSettings(TopLevelHighlightMixin highlightMixin)89 public TopLevelSettings(TopLevelHighlightMixin highlightMixin) { 90 this(); 91 mHighlightMixin = highlightMixin; 92 } 93 94 @Override getPreferenceScreenResId()95 protected int getPreferenceScreenResId() { 96 return getPreferenceLayoutResId(getContext()); 97 } 98 99 @Override getLogTag()100 protected String getLogTag() { 101 return TAG; 102 } 103 104 @Override getMetricsCategory()105 public int getMetricsCategory() { 106 return SettingsEnums.DASHBOARD_SUMMARY; 107 } 108 109 @Override onAttach(Context context)110 public void onAttach(Context context) { 111 super.onAttach(context); 112 HighlightableMenu.fromXml(context, getPreferenceScreenResId()); 113 use(SupportPreferenceController.class).setActivity(getActivity()); 114 } 115 116 @Override getHelpResource()117 public int getHelpResource() { 118 // Disable the help icon because this page uses a full search view in actionbar. 119 return 0; 120 } 121 122 @Override getCallbackFragment()123 public Fragment getCallbackFragment() { 124 return this; 125 } 126 127 @Override onPreferenceTreeClick(Preference preference)128 public boolean onPreferenceTreeClick(Preference preference) { 129 if (isDuplicateClick(preference)) { 130 return true; 131 } 132 133 // Register SplitPairRule for SubSettings. 134 ActivityEmbeddingRulesController.registerSubSettingsPairRule(getContext(), 135 true /* clearTop */); 136 137 setHighlightPreferenceKey(preference.getKey()); 138 return super.onPreferenceTreeClick(preference); 139 } 140 141 @Override onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)142 public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { 143 new SubSettingLauncher(getActivity()) 144 .setDestination(pref.getFragment()) 145 .setArguments(pref.getExtras()) 146 .setSourceMetricsCategory(caller instanceof Instrumentable 147 ? ((Instrumentable) caller).getMetricsCategory() 148 : Instrumentable.METRICS_CATEGORY_UNKNOWN) 149 .setTitleRes(-1) 150 .setIsSecondLayerPage(true) 151 .launch(); 152 return true; 153 } 154 155 @Override onCreate(Bundle icicle)156 public void onCreate(Bundle icicle) { 157 super.onCreate(icicle); 158 mIsEmbeddingActivityEnabled = 159 ActivityEmbeddingUtils.isEmbeddingActivityEnabled(getContext()); 160 if (!mIsEmbeddingActivityEnabled) { 161 return; 162 } 163 164 boolean activityEmbedded = isActivityEmbedded(); 165 if (icicle != null) { 166 mHighlightMixin = icicle.getParcelable(SAVED_HIGHLIGHT_MIXIN); 167 if (mHighlightMixin != null) { 168 mScrollNeeded = !mHighlightMixin.isActivityEmbedded() && activityEmbedded; 169 mHighlightMixin.setActivityEmbedded(activityEmbedded); 170 } 171 } 172 if (mHighlightMixin == null) { 173 mHighlightMixin = new TopLevelHighlightMixin(activityEmbedded); 174 } 175 } 176 177 /** Wrap ActivityEmbeddingController#isActivityEmbedded for testing. */ 178 @VisibleForTesting isActivityEmbedded()179 public boolean isActivityEmbedded() { 180 if (mActivityEmbeddingController == null) { 181 mActivityEmbeddingController = ActivityEmbeddingController.getInstance(getActivity()); 182 } 183 return mActivityEmbeddingController.isActivityEmbedded(getActivity()); 184 } 185 186 @Override onStart()187 public void onStart() { 188 if (mFirstStarted) { 189 mFirstStarted = false; 190 FeatureFactory.getFeatureFactory().getSearchFeatureProvider().sendPreIndexIntent( 191 getContext()); 192 } else if (mIsEmbeddingActivityEnabled && isOnlyOneActivityInTask() 193 && !isActivityEmbedded()) { 194 // Set default highlight menu key for 1-pane homepage since it will show the placeholder 195 // page once changing back to 2-pane. 196 Log.i(TAG, "Set default menu key"); 197 setHighlightMenuKey(getString(SettingsHomepageActivity.DEFAULT_HIGHLIGHT_MENU_KEY), 198 /* scrollNeeded= */ false); 199 } 200 super.onStart(); 201 } 202 isOnlyOneActivityInTask()203 private boolean isOnlyOneActivityInTask() { 204 final ActivityManager.RunningTaskInfo taskInfo = getSystemService(ActivityManager.class) 205 .getRunningTasks(1).get(0); 206 return taskInfo.numActivities == 1; 207 } 208 209 @Override onSaveInstanceState(Bundle outState)210 public void onSaveInstanceState(Bundle outState) { 211 super.onSaveInstanceState(outState); 212 if (mHighlightMixin != null) { 213 outState.putParcelable(SAVED_HIGHLIGHT_MIXIN, mHighlightMixin); 214 } 215 } 216 217 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)218 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 219 super.onCreatePreferences(savedInstanceState, rootKey); 220 if (Flags.homepageRevamp()) { 221 return; 222 } 223 int tintColor = Utils.getHomepageIconColor(getContext()); 224 iteratePreferences(preference -> { 225 Drawable icon = preference.getIcon(); 226 if (icon != null) { 227 icon.setTint(tintColor); 228 } 229 }); 230 } 231 232 @Override onConfigurationChanged(Configuration newConfig)233 public void onConfigurationChanged(Configuration newConfig) { 234 super.onConfigurationChanged(newConfig); 235 highlightPreferenceIfNeeded(); 236 } 237 238 @Override onSplitLayoutChanged(boolean isRegularLayout)239 public void onSplitLayoutChanged(boolean isRegularLayout) { 240 iteratePreferences(preference -> { 241 if (preference instanceof HomepagePreferenceLayout) { 242 ((HomepagePreferenceLayout) preference).getHelper().setIconVisible(isRegularLayout); 243 } 244 }); 245 } 246 247 @Override highlightPreferenceIfNeeded()248 public void highlightPreferenceIfNeeded() { 249 if (mHighlightMixin != null) { 250 mHighlightMixin.highlightPreferenceIfNeeded(); 251 } 252 } 253 254 @Override onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)255 public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, 256 Bundle savedInstanceState) { 257 RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, 258 savedInstanceState); 259 recyclerView.setVerticalScrollBarEnabled(false); 260 recyclerView.setHorizontalScrollBarEnabled(false); 261 recyclerView.setPadding(mPaddingHorizontal, 0, mPaddingHorizontal, 0); 262 return recyclerView; 263 } 264 265 /** Sets the horizontal padding */ setPaddingHorizontal(int padding)266 public void setPaddingHorizontal(int padding) { 267 mPaddingHorizontal = padding; 268 RecyclerView recyclerView = getListView(); 269 if (recyclerView != null) { 270 recyclerView.setPadding(padding, 0, padding, 0); 271 } 272 } 273 274 /** Updates the preference internal paddings */ updatePreferencePadding(boolean isTwoPane)275 public void updatePreferencePadding(boolean isTwoPane) { 276 iteratePreferences(new PreferenceJob() { 277 private int mIconPaddingStart; 278 private int mTextPaddingStart; 279 280 @Override 281 public void init() { 282 mIconPaddingStart = getResources().getDimensionPixelSize(isTwoPane 283 ? R.dimen.homepage_preference_icon_padding_start_two_pane 284 : R.dimen.homepage_preference_icon_padding_start); 285 mTextPaddingStart = getResources().getDimensionPixelSize(isTwoPane 286 ? R.dimen.homepage_preference_text_padding_start_two_pane 287 : R.dimen.homepage_preference_text_padding_start); 288 } 289 290 @Override 291 public void doForEach(Preference preference) { 292 if (preference instanceof HomepagePreferenceLayout) { 293 ((HomepagePreferenceLayout) preference).getHelper() 294 .setIconPaddingStart(mIconPaddingStart); 295 ((HomepagePreferenceLayout) preference).getHelper() 296 .setTextPaddingStart(mTextPaddingStart); 297 } 298 } 299 }); 300 } 301 302 /** Returns a {@link TopLevelHighlightMixin} that performs highlighting */ getHighlightMixin()303 public TopLevelHighlightMixin getHighlightMixin() { 304 return mHighlightMixin; 305 } 306 307 /** Highlight a preference with specified preference key */ setHighlightPreferenceKey(String prefKey)308 public void setHighlightPreferenceKey(String prefKey) { 309 // Skip Tips & support since it's full screen 310 if (mHighlightMixin != null && !TextUtils.equals(prefKey, PREF_KEY_SUPPORT)) { 311 mHighlightMixin.setHighlightPreferenceKey(prefKey); 312 } 313 } 314 315 /** Returns whether clicking the specified preference is considered as a duplicate click. */ isDuplicateClick(Preference pref)316 public boolean isDuplicateClick(Preference pref) { 317 /* Return true if 318 * 1. the device supports activity embedding, and 319 * 2. the target preference is highlighted, and 320 * 3. the current activity is embedded */ 321 return mHighlightMixin != null 322 && TextUtils.equals(pref.getKey(), mHighlightMixin.getHighlightPreferenceKey()) 323 && isActivityEmbedded(); 324 } 325 326 /** Show/hide the highlight on the menu entry for the search page presence */ setMenuHighlightShowed(boolean show)327 public void setMenuHighlightShowed(boolean show) { 328 if (mHighlightMixin != null) { 329 mHighlightMixin.setMenuHighlightShowed(show); 330 } 331 } 332 333 /** Highlight and scroll to a preference with specified menu key */ setHighlightMenuKey(String menuKey, boolean scrollNeeded)334 public void setHighlightMenuKey(String menuKey, boolean scrollNeeded) { 335 if (mHighlightMixin != null) { 336 mHighlightMixin.setHighlightMenuKey(menuKey, scrollNeeded); 337 } 338 } 339 340 @Override shouldForceRoundedIcon()341 protected boolean shouldForceRoundedIcon() { 342 return getContext().getResources() 343 .getBoolean(R.bool.config_force_rounded_icon_TopLevelSettings); 344 } 345 346 @Override onCreateAdapter(PreferenceScreen preferenceScreen)347 protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { 348 if (mIsEmbeddingActivityEnabled && (getActivity() instanceof SettingsHomepageActivity)) { 349 return mHighlightMixin.onCreateAdapter(this, preferenceScreen, mScrollNeeded); 350 } 351 352 if (Flags.homepageRevamp()) { 353 return new RoundCornerPreferenceAdapter(preferenceScreen); 354 } 355 return super.onCreateAdapter(preferenceScreen); 356 } 357 358 @Override createPreference(Tile tile)359 protected Preference createPreference(Tile tile) { 360 return new HomepagePreference(getPrefContext()); 361 } 362 reloadHighlightMenuKey()363 void reloadHighlightMenuKey() { 364 if (mHighlightMixin != null) { 365 mHighlightMixin.reloadHighlightMenuKey(getArguments()); 366 } 367 } 368 iteratePreferences(PreferenceJob job)369 private void iteratePreferences(PreferenceJob job) { 370 if (job == null || getPreferenceManager() == null) { 371 return; 372 } 373 PreferenceScreen screen = getPreferenceScreen(); 374 if (screen == null) { 375 return; 376 } 377 378 job.init(); 379 iteratePreferences(screen, job); 380 } 381 iteratePreferences(PreferenceGroup group, PreferenceJob job)382 private void iteratePreferences(PreferenceGroup group, PreferenceJob job) { 383 int count = group.getPreferenceCount(); 384 for (int i = 0; i < count; i++) { 385 Preference preference = group.getPreference(i); 386 job.doForEach(preference); 387 if (preference instanceof PreferenceCategory) { 388 iteratePreferences((PreferenceCategory) preference, job); 389 } 390 } 391 } 392 393 private interface PreferenceJob { init()394 default void init() { 395 } 396 doForEach(Preference preference)397 void doForEach(Preference preference); 398 } 399 getPreferenceLayoutResId(Context context)400 private static int getPreferenceLayoutResId(Context context) { 401 return Flags.homepageRevamp() 402 ? SettingsThemeHelper.isExpressiveTheme(context) 403 ? R.xml.top_level_settings_expressive 404 : R.xml.top_level_settings_v2 405 : R.xml.top_level_settings; 406 } 407 408 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 409 new BaseSearchIndexProvider() { 410 411 @Override 412 @NonNull 413 public List<SearchIndexableResource> getXmlResourcesToIndex( 414 @NonNull Context context, boolean enabled) { 415 SearchIndexableResource sir = new SearchIndexableResource(context); 416 sir.xmlResId = getPreferenceLayoutResId(context); 417 return List.of(sir); 418 } 419 420 @Override 421 protected boolean isPageSearchEnabled(Context context) { 422 // Never searchable, all entries in this page are already indexed elsewhere. 423 return false; 424 } 425 }; 426 } 427