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