• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY;
20 import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY;
21 import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI;
22 
23 import static com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH;
24 import static com.android.settings.SettingsActivity.EXTRA_USER_HANDLE;
25 
26 import android.animation.LayoutTransition;
27 import android.app.ActivityManager;
28 import android.app.settings.SettingsEnums;
29 import android.content.ComponentName;
30 import android.content.Intent;
31 import android.content.pm.ActivityInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.PackageManager.ApplicationInfoFlags;
34 import android.content.pm.UserInfo;
35 import android.content.res.Configuration;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Process;
39 import android.os.UserHandle;
40 import android.os.UserManager;
41 import android.text.TextUtils;
42 import android.util.ArraySet;
43 import android.util.FeatureFlagUtils;
44 import android.util.Log;
45 import android.view.View;
46 import android.view.Window;
47 import android.view.WindowManager;
48 import android.widget.FrameLayout;
49 import android.widget.ImageView;
50 import android.widget.Toolbar;
51 
52 import androidx.annotation.VisibleForTesting;
53 import androidx.core.graphics.Insets;
54 import androidx.core.util.Consumer;
55 import androidx.core.view.ViewCompat;
56 import androidx.core.view.WindowCompat;
57 import androidx.core.view.WindowInsetsCompat;
58 import androidx.fragment.app.Fragment;
59 import androidx.fragment.app.FragmentActivity;
60 import androidx.fragment.app.FragmentManager;
61 import androidx.fragment.app.FragmentTransaction;
62 import androidx.window.embedding.SplitController;
63 import androidx.window.embedding.SplitInfo;
64 import androidx.window.embedding.SplitRule;
65 import androidx.window.java.embedding.SplitControllerCallbackAdapter;
66 
67 import com.android.settings.R;
68 import com.android.settings.Settings;
69 import com.android.settings.SettingsActivity;
70 import com.android.settings.SettingsApplication;
71 import com.android.settings.accounts.AvatarViewMixin;
72 import com.android.settings.activityembedding.ActivityEmbeddingRulesController;
73 import com.android.settings.activityembedding.ActivityEmbeddingUtils;
74 import com.android.settings.activityembedding.EmbeddedDeepLinkUtils;
75 import com.android.settings.core.CategoryMixin;
76 import com.android.settings.core.FeatureFlags;
77 import com.android.settings.flags.Flags;
78 import com.android.settings.homepage.contextualcards.ContextualCardsFragment;
79 import com.android.settings.overlay.FeatureFactory;
80 import com.android.settings.safetycenter.SafetyCenterManagerWrapper;
81 import com.android.settingslib.Utils;
82 import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
83 
84 import com.google.android.setupcompat.util.WizardManagerHelper;
85 
86 import java.net.URISyntaxException;
87 import java.util.List;
88 import java.util.Set;
89 
90 /** Settings homepage activity */
91 public class SettingsHomepageActivity extends FragmentActivity implements
92         CategoryMixin.CategoryHandler {
93 
94     private static final String TAG = "SettingsHomepageActivity";
95 
96     // Additional extra of Settings#ACTION_SETTINGS_LARGE_SCREEN_DEEP_LINK.
97     // Put true value to the intent when startActivity for a deep link intent from this Activity.
98     public static final String EXTRA_IS_FROM_SETTINGS_HOMEPAGE = "is_from_settings_homepage";
99 
100     // Additional extra of Settings#ACTION_SETTINGS_LARGE_SCREEN_DEEP_LINK.
101     // Set & get Uri of the Intent separately to prevent failure of Intent#ParseUri.
102     public static final String EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA =
103             "settings_large_screen_deep_link_intent_data";
104 
105     // The referrer who fires the initial intent to start the homepage
106     @VisibleForTesting
107     static final String EXTRA_INITIAL_REFERRER = "initial_referrer";
108 
109     static final int DEFAULT_HIGHLIGHT_MENU_KEY = R.string.menu_key_network;
110     private static final long HOMEPAGE_LOADING_TIMEOUT_MS = 300;
111 
112     private TopLevelSettings mMainFragment;
113     private View mHomepageView;
114     private View mSuggestionView;
115     private View mTwoPaneSuggestionView;
116     private CategoryMixin mCategoryMixin;
117     private Set<HomepageLoadedListener> mLoadedListeners;
118     private boolean mIsEmbeddingActivityEnabled;
119     private boolean mIsTwoPane;
120     // A regular layout shows icons on homepage, whereas a simplified layout doesn't.
121     private boolean mIsRegularLayout = true;
122 
123     private SplitControllerCallbackAdapter mSplitControllerAdapter;
124     private SplitInfoCallback mCallback;
125     private boolean mAllowUpdateSuggestion = true;
126 
127     /** A listener receiving homepage loaded events. */
128     public interface HomepageLoadedListener {
129         /** Called when the homepage is loaded. */
onHomepageLoaded()130         void onHomepageLoaded();
131     }
132 
133     private interface FragmentCreator<T extends Fragment> {
create()134         T create();
135 
136         /** To initialize after {@link #create} */
init(Fragment fragment)137         default void init(Fragment fragment) {}
138     }
139 
140     /**
141      * Try to add a {@link HomepageLoadedListener}. If homepage is already loaded, the listener
142      * will not be notified.
143      *
144      * @return Whether the listener is added.
145      */
addHomepageLoadedListener(HomepageLoadedListener listener)146     public boolean addHomepageLoadedListener(HomepageLoadedListener listener) {
147         if (mHomepageView == null) {
148             return false;
149         } else {
150             if (!mLoadedListeners.contains(listener)) {
151                 mLoadedListeners.add(listener);
152             }
153             return true;
154         }
155     }
156 
157     /**
158      * Shows the homepage and shows/hides the suggestion together. Only allows to be executed once
159      * to avoid the flicker caused by the suggestion suddenly appearing/disappearing.
160      */
showHomepageWithSuggestion(boolean showSuggestion)161     public void showHomepageWithSuggestion(boolean showSuggestion) {
162         if (mAllowUpdateSuggestion) {
163             Log.i(TAG, "showHomepageWithSuggestion: " + showSuggestion);
164             mAllowUpdateSuggestion = false;
165             if (Flags.homepageRevamp()) {
166                 mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE);
167             } else {
168                 mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE);
169                 mTwoPaneSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE);
170             }
171         }
172 
173         if (mHomepageView == null) {
174             return;
175         }
176         final View homepageView = mHomepageView;
177         mHomepageView = null;
178         mLoadedListeners.forEach(listener -> listener.onHomepageLoaded());
179         mLoadedListeners.clear();
180         homepageView.setVisibility(View.VISIBLE);
181     }
182 
183     /** Returns the main content fragment */
getMainFragment()184     public TopLevelSettings getMainFragment() {
185         return mMainFragment;
186     }
187 
188     @Override
getCategoryMixin()189     public CategoryMixin getCategoryMixin() {
190         return mCategoryMixin;
191     }
192 
193     @Override
onCreate(Bundle savedInstanceState)194     protected void onCreate(Bundle savedInstanceState) {
195         super.onCreate(savedInstanceState);
196 
197         // Ensure device is provisioned in order to access Settings home
198         // TODO(b/331254029): This should later be replaced in favor of an allowlist
199         boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(),
200                 android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0;
201         if (unprovisioned) {
202             Log.e(TAG, "Device is not provisioned, exiting Settings");
203             finish();
204             return;
205         }
206 
207         // Settings homepage should be the task root, otherwise there will be UI issues.
208         boolean isTaskRoot = isTaskRoot();
209 
210         mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this);
211         if (mIsEmbeddingActivityEnabled) {
212             final UserManager um = getSystemService(UserManager.class);
213             final UserInfo userInfo = um.getUserInfo(getUserId());
214             if (EmbeddedDeepLinkUtils.isSubProfile(userInfo)) {
215                 final Intent intent = new Intent(getIntent())
216                         .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
217                         .putExtra(EXTRA_USER_HANDLE, getUser())
218                         .putExtra(EXTRA_INITIAL_REFERRER, getCurrentReferrer());
219                 if (TextUtils.equals(intent.getAction(), ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)
220                         && this instanceof DeepLinkHomepageActivity) {
221                     intent.setClass(this, DeepLinkHomepageActivityInternal.class);
222                 } else {
223                     intent.setPackage(getPackageName());
224                 }
225                 if (!isTaskRoot) {
226                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
227                 } else {
228                     intent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
229                 }
230                 startActivityAsUser(intent, um.getProfileParent(userInfo.id).getUserHandle());
231                 finish();
232                 return;
233             }
234         }
235 
236         final boolean isDeepLinkStartedFromSearch = getIntent().getBooleanExtra(
237                 EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false /* defaultValue */);
238         if (!isTaskRoot && !isDeepLinkStartedFromSearch) {
239             if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
240                 Log.i(TAG, "Activity has been started, finishing");
241             } else {
242                 Log.i(TAG, "Homepage should be started with FLAG_ACTIVITY_NEW_TASK, restarting");
243                 Intent intent = new Intent(getIntent())
244                         .setPackage(getPackageName())
245                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
246                                 | Intent.FLAG_ACTIVITY_FORWARD_RESULT)
247                         .putExtra(EXTRA_USER_HANDLE, getUser())
248                         .putExtra(EXTRA_INITIAL_REFERRER, getCurrentReferrer());
249                 startActivity(intent);
250             }
251             finish();
252             return;
253         }
254 
255         setupEdgeToEdge();
256         setContentView(
257                 Flags.homepageRevamp()
258                         ? R.layout.settings_homepage_container_v2
259                         : R.layout.settings_homepage_container);
260 
261         mIsTwoPane = ActivityEmbeddingUtils.isAlreadyEmbedded(this);
262 
263         updateAppBarMinHeight();
264         initHomepageContainer();
265         updateHomepageAppBar();
266         updateHomepageBackground();
267         mLoadedListeners = new ArraySet<>();
268 
269         initSearchBarView();
270 
271         getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
272         mCategoryMixin = new CategoryMixin(this);
273         getLifecycle().addObserver(mCategoryMixin);
274 
275         final String highlightMenuKey = getHighlightMenuKey();
276         // Only allow features on high ram devices.
277         if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
278             initAvatarView();
279             final boolean scrollNeeded = mIsEmbeddingActivityEnabled
280                     && !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);
281             showSuggestionFragment(scrollNeeded);
282             if (!Flags.updatedSuggestionCardAosp()
283                     && FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {
284                 showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);
285                 ((FrameLayout) findViewById(R.id.main_content))
286                         .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
287             }
288         }
289         mMainFragment = showFragment(() -> {
290             final TopLevelSettings fragment = new TopLevelSettings();
291             fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
292                     highlightMenuKey);
293             return fragment;
294         }, R.id.main_content);
295 
296         // Launch the intent from deep link for large screen devices.
297         if (shouldLaunchDeepLinkIntentToRight()) {
298             launchDeepLinkIntentToRight();
299         }
300 
301         // Settings app may be launched on an existing task. Reset SplitPairRule of SubSettings here
302         // to prevent SplitPairRule of an existing task applied on a new started Settings app.
303         if (mIsEmbeddingActivityEnabled
304                 && (getIntent().getFlags() & Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
305             initSplitPairRules();
306         }
307 
308         updateHomepagePaddings();
309         updateSplitLayout();
310 
311         enableTaskLocaleOverride();
312     }
313 
314     @VisibleForTesting
initSplitPairRules()315     void initSplitPairRules() {
316         new ActivityEmbeddingRulesController(getApplicationContext()).initRules();
317     }
318 
319     @Override
onStart()320     protected void onStart() {
321         ((SettingsApplication) getApplication()).setHomeActivity(this);
322         super.onStart();
323         if (mIsEmbeddingActivityEnabled) {
324             final SplitController splitController = SplitController.getInstance(this);
325             mSplitControllerAdapter = new SplitControllerCallbackAdapter(splitController);
326             mCallback = new SplitInfoCallback(this);
327             mSplitControllerAdapter.addSplitListener(this, Runnable::run, mCallback);
328         }
329     }
330 
331     @Override
onStop()332     protected void onStop() {
333         super.onStop();
334         mAllowUpdateSuggestion = true;
335         if (mSplitControllerAdapter != null && mCallback != null) {
336             mSplitControllerAdapter.removeSplitListener(mCallback);
337             mCallback = null;
338             mSplitControllerAdapter = null;
339         }
340     }
341 
342     @Override
onNewIntent(Intent intent)343     protected void onNewIntent(Intent intent) {
344         super.onNewIntent(intent);
345 
346         // When it's large screen 2-pane and Settings app is in the background, receiving an Intent
347         // will not recreate this activity. Update the intent for this case.
348         setIntent(intent);
349         reloadHighlightMenuKey();
350         if (isFinishing()) {
351             return;
352         }
353         // Launch the intent from deep link for large screen devices.
354         if (shouldLaunchDeepLinkIntentToRight()) {
355             launchDeepLinkIntentToRight();
356         }
357     }
358 
359     @Override
onConfigurationChanged(Configuration newConfig)360     public void onConfigurationChanged(Configuration newConfig) {
361         super.onConfigurationChanged(newConfig);
362         updateHomepageUI();
363     }
364 
updateSplitLayout()365     private void updateSplitLayout() {
366         if (!mIsEmbeddingActivityEnabled) {
367             return;
368         }
369         if (mIsTwoPane) {
370             if (mIsRegularLayout == ActivityEmbeddingUtils.isRegularHomepageLayout(this)) {
371                 // Layout unchanged
372                 return;
373             }
374         } else if (mIsRegularLayout) {
375             // One pane mode with the regular layout, not needed to change
376             return;
377         }
378         mIsRegularLayout = !mIsRegularLayout;
379 
380         // Update search title padding
381         View searchTitle = findViewById(R.id.search_bar_title);
382         if (searchTitle != null) {
383             int paddingStart = getResources().getDimensionPixelSize(
384                     mIsRegularLayout
385                             ? R.dimen.search_bar_title_padding_start_regular_two_pane
386                             : R.dimen.search_bar_title_padding_start);
387             searchTitle.setPaddingRelative(paddingStart, 0, 0, 0);
388         }
389         // Notify fragments
390         getSupportFragmentManager().getFragments().forEach(fragment -> {
391             if (fragment instanceof SplitLayoutListener) {
392                 ((SplitLayoutListener) fragment).onSplitLayoutChanged(mIsRegularLayout);
393             }
394         });
395     }
396 
setupEdgeToEdge()397     private void setupEdgeToEdge() {
398         WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
399         ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content),
400                 (v, windowInsets) -> {
401                     Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
402                             | WindowInsetsCompat.Type.displayCutout());
403                     // Apply the insets paddings to the view.
404                     v.setPadding(insets.left, 0, insets.right, insets.bottom);
405 
406                     // reset the top padding of search bar container to original top padding
407                     // plus insets top.
408                     View container = findViewById(R.id.app_bar_container);
409                     final int top_padding = getResources().getDimensionPixelSize(
410                             R.dimen.search_bar_container_top_padding);
411                     container.setPadding(container.getPaddingLeft(), top_padding + insets.top,
412                             container.getPaddingRight(), container.getPaddingBottom());
413 
414                     // Return CONSUMED if you don't want the window insets to keep being
415                     // passed down to descendant views.
416                     return WindowInsetsCompat.CONSUMED;
417                 });
418     }
419 
initSearchBarView()420     private void initSearchBarView() {
421         if (Flags.homepageRevamp()) {
422             View toolbar = findViewById(R.id.search_action_bar);
423             FeatureFactory.getFeatureFactory().getSearchFeatureProvider()
424                     .initSearchToolbar(this /* activity */, toolbar,
425                             SettingsEnums.SETTINGS_HOMEPAGE);
426         } else {
427             final Toolbar toolbar = findViewById(R.id.search_action_bar);
428             FeatureFactory.getFeatureFactory().getSearchFeatureProvider()
429                     .initSearchToolbar(this /* activity */, toolbar,
430                             SettingsEnums.SETTINGS_HOMEPAGE);
431 
432             if (mIsEmbeddingActivityEnabled) {
433                 final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane);
434                 FeatureFactory.getFeatureFactory().getSearchFeatureProvider()
435                         .initSearchToolbar(this /* activity */, toolbarTwoPaneVersion,
436                                 SettingsEnums.SETTINGS_HOMEPAGE);
437             }
438         }
439     }
440 
initAvatarView()441     private void initAvatarView() {
442         if (Flags.homepageRevamp()) {
443             return;
444         }
445 
446         final ImageView avatarView = findViewById(R.id.account_avatar);
447         final ImageView avatarTwoPaneView = findViewById(R.id.account_avatar_two_pane_version);
448         if (AvatarViewMixin.isAvatarSupported(this)) {
449             avatarView.setVisibility(View.VISIBLE);
450             getLifecycle().addObserver(new AvatarViewMixin(this, avatarView));
451 
452             if (mIsEmbeddingActivityEnabled) {
453                 avatarTwoPaneView.setVisibility(View.VISIBLE);
454                 getLifecycle().addObserver(new AvatarViewMixin(this, avatarTwoPaneView));
455             }
456         }
457     }
458 
updateHomepageUI()459     private void updateHomepageUI() {
460         final boolean newTwoPaneState = ActivityEmbeddingUtils.isAlreadyEmbedded(this);
461         if (mIsTwoPane != newTwoPaneState) {
462             mIsTwoPane = newTwoPaneState;
463             updateHomepageAppBar();
464             updateHomepageBackground();
465             updateHomepagePaddings();
466         }
467         updateSplitLayout();
468     }
469 
updateHomepageBackground()470     private void updateHomepageBackground() {
471         if (!Flags.homepageRevamp() && !mIsEmbeddingActivityEnabled) {
472             return;
473         }
474 
475         final Window window = getWindow();
476         final int color = mIsTwoPane
477                 ? getColor(R.color.settings_two_pane_background_color)
478                 : Utils.getColorAttrDefaultColor(this, android.R.attr.colorBackground);
479 
480         window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
481 
482         // Update content background.
483         findViewById(android.R.id.content).setBackgroundColor(color);
484         if (Flags.homepageRevamp()) {
485             //Update search bar background
486             findViewById(R.id.app_bar_container).setBackgroundColor(color);
487         }
488     }
489 
showSuggestionFragment(boolean scrollNeeded)490     private void showSuggestionFragment(boolean scrollNeeded) {
491         final Class<? extends Fragment> fragmentClass = FeatureFactory.getFeatureFactory()
492                 .getSuggestionFeatureProvider().getSuggestionFragment();
493         if (fragmentClass == null) {
494             return;
495         }
496 
497         if (Flags.homepageRevamp()) {
498             mSuggestionView = findViewById(R.id.suggestion_content);
499         } else {
500             mSuggestionView = findViewById(R.id.suggestion_content);
501             mTwoPaneSuggestionView = findViewById(R.id.two_pane_suggestion_content);
502         }
503         mHomepageView = findViewById(R.id.settings_homepage_container);
504         // Hide the homepage for preparing the suggestion. If scrolling is needed, the list views
505         // should be initialized in the invisible homepage view to prevent a scroll flicker.
506         mHomepageView.setVisibility(scrollNeeded ? View.INVISIBLE : View.GONE);
507         // Schedule a timer to show the homepage and hide the suggestion on timeout.
508         mHomepageView.postDelayed(() -> showHomepageWithSuggestion(false),
509                 HOMEPAGE_LOADING_TIMEOUT_MS);
510         if (Flags.homepageRevamp()) {
511             showFragment(new SuggestionFragCreator(fragmentClass, true),
512                     R.id.suggestion_content);
513         } else {
514             showFragment(new SuggestionFragCreator(fragmentClass, /* isTwoPaneLayout= */ false),
515                     R.id.suggestion_content);
516             if (mIsEmbeddingActivityEnabled) {
517                 showFragment(new SuggestionFragCreator(fragmentClass, /* isTwoPaneLayout= */ true),
518                         R.id.two_pane_suggestion_content);
519             }
520         }
521     }
522 
showFragment(FragmentCreator<T> fragmentCreator, int id)523     private <T extends Fragment> T showFragment(FragmentCreator<T> fragmentCreator, int id) {
524         final FragmentManager fragmentManager = getSupportFragmentManager();
525         final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
526         T showFragment = (T) fragmentManager.findFragmentById(id);
527 
528         if (showFragment == null) {
529             showFragment = fragmentCreator.create();
530             fragmentCreator.init(showFragment);
531             fragmentTransaction.add(id, showFragment);
532         } else {
533             fragmentCreator.init(showFragment);
534             fragmentTransaction.show(showFragment);
535         }
536         fragmentTransaction.commit();
537         return showFragment;
538     }
539 
shouldLaunchDeepLinkIntentToRight()540     private boolean shouldLaunchDeepLinkIntentToRight() {
541         if (!ActivityEmbeddingUtils.isSettingsSplitEnabled(this)
542                 || !FeatureFlagUtils.isEnabled(this,
543                         FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN)) {
544             return false;
545         }
546 
547         Intent intent = getIntent();
548         return intent != null && TextUtils.equals(intent.getAction(),
549                 ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY);
550     }
551 
launchDeepLinkIntentToRight()552     private void launchDeepLinkIntentToRight() {
553         if (!(this instanceof DeepLinkHomepageActivity
554                 || this instanceof DeepLinkHomepageActivityInternal)) {
555             Log.e(TAG, "Not a deep link component");
556             finish();
557             return;
558         }
559 
560         if (!WizardManagerHelper.isUserSetupComplete(this)) {
561             Log.e(TAG, "Cancel deep link before SUW completed");
562             finish();
563             return;
564         }
565 
566         final Intent intent = getIntent();
567         final String intentUriString = intent.getStringExtra(
568                 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI);
569         if (TextUtils.isEmpty(intentUriString)) {
570             Log.e(TAG, "No EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI to deep link");
571             finish();
572             return;
573         }
574 
575         final Intent targetIntent;
576         try {
577             targetIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME);
578         } catch (URISyntaxException e) {
579             Log.e(TAG, "Failed to parse deep link intent: " + e);
580             finish();
581             return;
582         }
583 
584         targetIntent.setData(intent.getParcelableExtra(
585                 SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA));
586         final ComponentName targetComponentName = targetIntent.resolveActivity(getPackageManager());
587         if (targetComponentName == null) {
588             Log.e(TAG, "No valid target for the deep link intent: " + targetIntent);
589             finish();
590             return;
591         }
592 
593         ActivityInfo targetActivityInfo;
594         try {
595             targetActivityInfo = getPackageManager().getActivityInfo(targetComponentName,
596                     /* flags= */ 0);
597         } catch (PackageManager.NameNotFoundException e) {
598             Log.e(TAG, "Failed to get target ActivityInfo: " + e);
599             finish();
600             return;
601         }
602 
603         UserHandle user = intent.getParcelableExtra(EXTRA_USER_HANDLE, UserHandle.class);
604         String caller = getInitialReferrer();
605         int callerUid = -1;
606         if (caller != null) {
607             try {
608                 callerUid = getPackageManager().getApplicationInfoAsUser(caller,
609                         ApplicationInfoFlags.of(/* flags= */ 0),
610                         user != null ? user.getIdentifier() : getUserId()).uid;
611             } catch (PackageManager.NameNotFoundException e) {
612                 Log.e(TAG, "Not able to get callerUid: " + e);
613                 finish();
614                 return;
615             }
616         }
617 
618         if (!hasPrivilegedAccess(caller, callerUid, targetActivityInfo.packageName)) {
619             if (!targetActivityInfo.exported) {
620                 Log.e(TAG, "Target Activity is not exported");
621                 finish();
622                 return;
623             }
624 
625             if (!isCallingAppPermitted(targetActivityInfo.permission, callerUid)) {
626                 Log.e(TAG, "Calling app must have the permission of deep link Activity");
627                 finish();
628                 return;
629             }
630         }
631 
632         // Only allow FLAG_GRANT_READ/WRITE_URI_PERMISSION if calling app has the permission to
633         // access specified Uri.
634         int uriPermissionFlags = targetIntent.getFlags()
635                 & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
636         if (targetIntent.getData() != null
637                 && uriPermissionFlags != 0
638                 && checkUriPermission(targetIntent.getData(), /* pid= */ -1, callerUid,
639                         uriPermissionFlags) == PackageManager.PERMISSION_DENIED) {
640             Log.e(TAG, "Calling app must have the permission to access Uri and grant permission");
641             finish();
642             return;
643         }
644 
645         targetIntent.setComponent(targetComponentName);
646 
647         // To prevent launchDeepLinkIntentToRight again for configuration change.
648         intent.setAction(null);
649 
650         targetIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
651         targetIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
652 
653         // Sender of intent may want to send intent extra data to the destination of targetIntent.
654         targetIntent.replaceExtras(intent);
655 
656         targetIntent.putExtra(EXTRA_IS_FROM_SETTINGS_HOMEPAGE, true);
657         targetIntent.putExtra(SettingsActivity.EXTRA_IS_FROM_SLICE, false);
658 
659         // Set 2-pane pair rule for the deep link page.
660         ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
661                 new ComponentName(getApplicationContext(), getClass()),
662                 targetComponentName,
663                 targetIntent.getAction(),
664                 SplitRule.FinishBehavior.ALWAYS,
665                 SplitRule.FinishBehavior.ALWAYS,
666                 true /* clearTop */);
667         ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
668                 new ComponentName(getApplicationContext(), Settings.class),
669                 targetComponentName,
670                 targetIntent.getAction(),
671                 SplitRule.FinishBehavior.ALWAYS,
672                 SplitRule.FinishBehavior.ALWAYS,
673                 true /* clearTop */);
674 
675         if (user != null) {
676             startActivityAsUser(targetIntent, user);
677         } else {
678             startActivity(targetIntent);
679         }
680     }
681 
682     // Check if the caller has privileged access to launch the target page.
hasPrivilegedAccess(String callerPkg, int callerUid, String targetPackage)683     private boolean hasPrivilegedAccess(String callerPkg, int callerUid, String targetPackage) {
684         if (TextUtils.equals(callerPkg, getPackageName())) {
685             return true;
686         }
687 
688         int targetUid = -1;
689         try {
690             targetUid = getPackageManager().getApplicationInfo(targetPackage,
691                     ApplicationInfoFlags.of(/* flags= */ 0)).uid;
692         } catch (PackageManager.NameNotFoundException e) {
693             Log.e(TAG, "Not able to get targetUid: " + e);
694             return false;
695         }
696 
697         // When activityInfo.exported is false, Activity still can be launched if applications have
698         // the same user ID.
699         if (UserHandle.isSameApp(callerUid, targetUid)) {
700             return true;
701         }
702 
703         // When activityInfo.exported is false, Activity still can be launched if calling app has
704         // root or system privilege.
705         int callingAppId = UserHandle.getAppId(callerUid);
706         if (callingAppId == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID) {
707             return true;
708         }
709 
710         return false;
711     }
712 
713     @VisibleForTesting
getInitialReferrer()714     String getInitialReferrer() {
715         String referrer = getCurrentReferrer();
716         if (!TextUtils.equals(referrer, getPackageName())) {
717             return referrer;
718         }
719 
720         String initialReferrer = getIntent().getStringExtra(EXTRA_INITIAL_REFERRER);
721         return TextUtils.isEmpty(initialReferrer) ? referrer : initialReferrer;
722     }
723 
724     @VisibleForTesting
getCurrentReferrer()725     String getCurrentReferrer() {
726         Intent intent = getIntent();
727         // Clear extras to get the real referrer
728         intent.removeExtra(Intent.EXTRA_REFERRER);
729         intent.removeExtra(Intent.EXTRA_REFERRER_NAME);
730         Uri referrer = getReferrer();
731         return referrer != null ? referrer.getHost() : null;
732     }
733 
734     @VisibleForTesting
isCallingAppPermitted(String permission, int callerUid)735     boolean isCallingAppPermitted(String permission, int callerUid) {
736         return TextUtils.isEmpty(permission)
737                 || checkPermission(permission, /* pid= */ -1, callerUid)
738                         == PackageManager.PERMISSION_GRANTED;
739     }
740 
getHighlightMenuKey()741     private String getHighlightMenuKey() {
742         final Intent intent = getIntent();
743         if (intent != null && TextUtils.equals(intent.getAction(),
744                 ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) {
745             final String menuKey = intent.getStringExtra(
746                     EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY);
747             if (!TextUtils.isEmpty(menuKey)) {
748                 return maybeRemapMenuKey(menuKey);
749             }
750         }
751         return getString(DEFAULT_HIGHLIGHT_MENU_KEY);
752     }
753 
maybeRemapMenuKey(String menuKey)754     private String maybeRemapMenuKey(String menuKey) {
755         boolean isPrivacyOrSecurityMenuKey =
756                 getString(R.string.menu_key_privacy).equals(menuKey)
757                         || getString(R.string.menu_key_security).equals(menuKey);
758         boolean isSafetyCenterMenuKey = getString(R.string.menu_key_safety_center).equals(menuKey);
759 
760         if (isPrivacyOrSecurityMenuKey && SafetyCenterManagerWrapper.get().isEnabled(this)) {
761             return getString(R.string.menu_key_safety_center);
762         }
763         if (isSafetyCenterMenuKey && !SafetyCenterManagerWrapper.get().isEnabled(this)) {
764             // We don't know if security or privacy, default to security as it is above.
765             return getString(R.string.menu_key_security);
766         }
767         return menuKey;
768     }
769 
reloadHighlightMenuKey()770     private void reloadHighlightMenuKey() {
771         mMainFragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
772                 getHighlightMenuKey());
773         mMainFragment.reloadHighlightMenuKey();
774     }
775 
initHomepageContainer()776     private void initHomepageContainer() {
777         final View view = findViewById(R.id.homepage_container);
778         // Prevent inner RecyclerView gets focus and invokes scrolling.
779         view.setFocusableInTouchMode(true);
780         view.requestFocus();
781 
782         if (Flags.extendedScreenshotsExcludeNestedScrollables()) {
783             // Force scroll capture to select the NestedScrollView, instead of the non-scrollable
784             // RecyclerView which is contained inside it with no height constraint.
785             final View scrollableContainer = findViewById(R.id.main_content_scrollable_container);
786             if (scrollableContainer != null) {
787                 scrollableContainer.setScrollCaptureHint(
788                         View.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
789             }
790         }
791     }
792 
updateHomepageAppBar()793     private void updateHomepageAppBar() {
794         if (Flags.homepageRevamp() || !mIsEmbeddingActivityEnabled) {
795             return;
796         }
797         updateAppBarMinHeight();
798         if (mIsTwoPane) {
799             findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.GONE);
800             findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.VISIBLE);
801             findViewById(R.id.suggestion_container_two_pane).setVisibility(View.VISIBLE);
802         } else {
803             findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.VISIBLE);
804             findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.GONE);
805             findViewById(R.id.suggestion_container_two_pane).setVisibility(View.GONE);
806         }
807     }
808 
updateHomepagePaddings()809     private void updateHomepagePaddings() {
810         if (Flags.homepageRevamp() || !mIsEmbeddingActivityEnabled) {
811             return;
812         }
813         if (mIsTwoPane) {
814             int padding = getResources().getDimensionPixelSize(
815                     R.dimen.homepage_padding_horizontal_two_pane);
816             mMainFragment.setPaddingHorizontal(padding);
817         } else {
818             mMainFragment.setPaddingHorizontal(0);
819         }
820         mMainFragment.updatePreferencePadding(mIsTwoPane);
821     }
822 
updateAppBarMinHeight()823     private void updateAppBarMinHeight() {
824         if (Flags.homepageRevamp()) {
825             return;
826         }
827         final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height);
828         final int margin = getResources().getDimensionPixelSize(
829                 mIsEmbeddingActivityEnabled && mIsTwoPane
830                         ? R.dimen.homepage_app_bar_padding_two_pane
831                         : R.dimen.search_bar_margin);
832         findViewById(R.id.app_bar_container).setMinimumHeight(searchBarHeight + margin * 2);
833     }
834 
835     private static class SuggestionFragCreator implements FragmentCreator {
836 
837         private final Class<? extends Fragment> mClass;
838         private final boolean mIsTwoPaneLayout;
839 
SuggestionFragCreator(Class<? extends Fragment> clazz, boolean isTwoPaneLayout)840         SuggestionFragCreator(Class<? extends Fragment> clazz, boolean isTwoPaneLayout) {
841             mClass = clazz;
842             mIsTwoPaneLayout = isTwoPaneLayout;
843         }
844 
845         @Override
create()846         public Fragment create() {
847             try {
848                 Fragment fragment = mClass.getConstructor().newInstance();
849                 return fragment;
850             } catch (Exception e) {
851                 Log.w(TAG, "Cannot show fragment", e);
852             }
853             return null;
854         }
855 
856         @Override
init(Fragment fragment)857         public void init(Fragment fragment) {
858             if (fragment instanceof SplitLayoutListener) {
859                 ((SplitLayoutListener) fragment).setSplitLayoutSupported(mIsTwoPaneLayout);
860             }
861         }
862     }
863 
864     /** The callback invoked while AE splitting. */
865     private static class SplitInfoCallback implements Consumer<List<SplitInfo>> {
866         private final SettingsHomepageActivity mActivity;
867 
868         private boolean mIsSplitUpdatedUI = false;
869 
SplitInfoCallback(SettingsHomepageActivity activity)870         SplitInfoCallback(SettingsHomepageActivity activity) {
871             mActivity = activity;
872         }
873 
874         @Override
accept(List<SplitInfo> splitInfoList)875         public void accept(List<SplitInfo> splitInfoList) {
876             if (!splitInfoList.isEmpty() && !mIsSplitUpdatedUI && !mActivity.isFinishing()
877                     && ActivityEmbeddingUtils.isAlreadyEmbedded(mActivity)) {
878                 mIsSplitUpdatedUI = true;
879                 mActivity.updateHomepageUI();
880             }
881         }
882     }
883 }
884