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