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