From e427cc6752fac18cdc026754a3da392ea5fc6259 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Mon, 25 Oct 2021 14:11:16 +0800 Subject: [PATCH] Search experience improvement for large screen - Support fragment and direct link in SearchResultTrampoline - Start activity for SI case and start deep link trampoline for others - Disable menu highlight whenever the search bar is clicked - Don't overwrite SettingsApplication's homepage activity in SliceDeepLinkHomepageActivity - Scroll to highlighted menu entry after homepage is loaded to prevent UI overlapping Bug: 201724410 Test: manual, robotest build pass Change-Id: I5115d17d829e85036000da2e80f0e5b0598c733f --- .../android/settings/SettingsActivity.java | 37 ++++---- .../homepage/SettingsHomepageActivity.java | 39 +++++++- .../SliceDeepLinkHomepageActivity.java | 5 ++ .../settings/homepage/TopLevelSettings.java | 14 ++- .../search/SearchFeatureProvider.java | 25 ++++-- .../search/SearchResultTrampoline.java | 88 +++++++++++++++---- .../SettingsSearchIndexablesProvider.java | 1 - ...ighlightableTopLevelPreferenceAdapter.java | 17 ++-- .../search/SearchFeatureProviderImplTest.java | 7 +- 9 files changed, 176 insertions(+), 57 deletions(-) diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index df30d8b546c..9c81895e976 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -255,7 +255,8 @@ public class SettingsActivity extends SettingsBaseActivity // Should happen before any call to getIntent() getMetaData(); final Intent intent = getIntent(); - if (launchHomepageForTwoPaneDeepLink(intent)) { + if (shouldShowTwoPaneDeepLink(intent)) { + launchHomepageForTwoPaneDeepLink(intent); finishAndRemoveTask(); return; } @@ -368,16 +369,13 @@ public class SettingsActivity extends SettingsBaseActivity intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false); } - /** Returns true if the Activity is started by a deep link intent for large screen devices. */ - private boolean launchHomepageForTwoPaneDeepLink(Intent intent) { - if (!shouldShowTwoPaneDeepLink(intent)) { - return false; - } - + /** + * Returns the deep link trampoline intent for large screen devices. + */ + public static Intent getTrampolineIntent(Intent intent, String highlightMenuKey) { final Intent detailIntent = new Intent(intent); // It's a deep link intent, SettingsHomepageActivity will set SplitPairRule and start it. final Intent trampolineIntent = new Intent(ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY); - trampolineIntent.replaceExtras(detailIntent); // Relay detail intent data to prevent failure of Intent#ParseUri. @@ -391,22 +389,27 @@ public class SettingsActivity extends SettingsBaseActivity trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI, detailIntent.toUri(Intent.URI_INTENT_SCHEME)); - if (detailIntent.getBooleanExtra(EXTRA_IS_FROM_SLICE, false)) { - trampolineIntent.setClass(this, SliceDeepLinkHomepageActivity.class); + trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, + highlightMenuKey); + trampolineIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + return trampolineIntent; + } + + private void launchHomepageForTwoPaneDeepLink(Intent intent) { + final Intent trampolineIntent; + if (intent.getBooleanExtra(EXTRA_IS_FROM_SLICE, false)) { // Get menu key for slice deep link case. - final String highlightMenuKey = detailIntent.getStringExtra( + final String highlightMenuKey = intent.getStringExtra( EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY); if (!TextUtils.isEmpty(highlightMenuKey)) { mHighlightMenuKey = highlightMenuKey; } + trampolineIntent = getTrampolineIntent(intent, mHighlightMenuKey); + trampolineIntent.setClass(this, SliceDeepLinkHomepageActivity.class); + } else { + trampolineIntent = getTrampolineIntent(intent, mHighlightMenuKey); } - - trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, - mHighlightMenuKey); - trampolineIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); startActivity(trampolineIntent); - - return true; } private boolean shouldShowTwoPaneDeepLink(Intent intent) { diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 2bbc11e2b95..ae8c61e3b30 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -27,6 +27,7 @@ import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; +import android.util.ArraySet; import android.util.FeatureFlagUtils; import android.util.Log; import android.view.View; @@ -54,6 +55,7 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; import java.net.URISyntaxException; +import java.util.Set; /** Settings homepage activity */ public class SettingsHomepageActivity extends FragmentActivity implements @@ -79,10 +81,27 @@ public class SettingsHomepageActivity extends FragmentActivity implements private View mHomepageView; private View mSuggestionView; private CategoryMixin mCategoryMixin; + private Set mLoadedListeners; - @Override - public CategoryMixin getCategoryMixin() { - return mCategoryMixin; + /** A listener receiving homepage loaded events. */ + public interface HomepageLoadedListener { + /** Called when the homepage is loaded. */ + void onHomepageLoaded(); + } + + /** + * Try to register a {@link HomepageLoadedListener}. If homepage is already loaded, the + * listener will not be notified. + * + * @return Whether the listener should be registered. + */ + public boolean registerHomepageLoadedListenerIfNeeded(HomepageLoadedListener listener) { + if (mHomepageView == null) { + return false; + } else { + mLoadedListeners.add(listener); + return true; + } } /** @@ -97,17 +116,25 @@ public class SettingsHomepageActivity extends FragmentActivity implements mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); mHomepageView.setVisibility(View.VISIBLE); mHomepageView = null; + mLoadedListeners.forEach(listener -> listener.onHomepageLoaded()); + mLoadedListeners.clear(); + } + + @Override + public CategoryMixin getCategoryMixin() { + return mCategoryMixin; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((SettingsApplication) getApplication()).setHomeActivity(this); + setHomeActivity(); setContentView(R.layout.settings_homepage_container); final View appBar = findViewById(R.id.app_bar_container); appBar.setMinimumHeight(getSearchBoxHeight()); initHomepageContainer(); + mLoadedListeners = new ArraySet<>(); final Toolbar toolbar = findViewById(R.id.search_action_bar); FeatureFactory.getFactory(this).getSearchFeatureProvider() @@ -158,6 +185,10 @@ public class SettingsHomepageActivity extends FragmentActivity implements launchDeepLinkIntentToRight(); } + protected void setHomeActivity() { + ((SettingsApplication) getApplication()).setHomeActivity(this); + } + private void showSuggestionFragment() { final Class fragment = FeatureFactory.getFactory(this) .getSuggestionFeatureProvider(this).getContextualSuggestionFragment(); diff --git a/src/com/android/settings/homepage/SliceDeepLinkHomepageActivity.java b/src/com/android/settings/homepage/SliceDeepLinkHomepageActivity.java index 2f836127e21..61e946d3bd8 100644 --- a/src/com/android/settings/homepage/SliceDeepLinkHomepageActivity.java +++ b/src/com/android/settings/homepage/SliceDeepLinkHomepageActivity.java @@ -20,6 +20,11 @@ import android.content.ComponentName; /** Activity for Slices to launch Settings deep link page */ public class SliceDeepLinkHomepageActivity extends SettingsHomepageActivity { + @Override + protected void setHomeActivity() { + // do not overwrite homepage activity in SettingsApplication + } + @Override protected ComponentName getDeepLinkComponent() { return new ComponentName(getApplicationContext(), getClass()); diff --git a/src/com/android/settings/homepage/TopLevelSettings.java b/src/com/android/settings/homepage/TopLevelSettings.java index d3bfa02ae27..e9c7ef89668 100644 --- a/src/com/android/settings/homepage/TopLevelSettings.java +++ b/src/com/android/settings/homepage/TopLevelSettings.java @@ -101,7 +101,7 @@ public class TopLevelSettings extends DashboardFragment implements public boolean onPreferenceTreeClick(Preference preference) { // Register SplitPairRule for SubSettings. ActivityEmbeddingRulesController.registerSubSettingsPairRuleIfNeeded(getContext(), - true /* clearTop*/); + true /* clearTop */); setHighlightPreferenceKey(preference.getKey()); return super.onPreferenceTreeClick(preference); @@ -184,6 +184,15 @@ public class TopLevelSettings extends DashboardFragment implements } } + /** Disable highlight on the menu entry */ + public void disableMenuHighlight() { + if (mTopLevelAdapter == null) { + return; + } + mHighlightedPreferenceKey = null; + mTopLevelAdapter.highlightPreference(mHighlightedPreferenceKey, /* scrollNeeded= */ false); + } + @Override protected boolean shouldForceRoundedIcon() { return getContext().getResources() @@ -202,7 +211,8 @@ public class TopLevelSettings extends DashboardFragment implements Log.d(TAG, "onCreateAdapter, pref key: " + mHighlightedPreferenceKey); mTopLevelAdapter = new HighlightableTopLevelPreferenceAdapter( - getActivity(), preferenceScreen, getListView(), mHighlightedPreferenceKey); + (SettingsHomepageActivity) getActivity(), preferenceScreen, getListView(), + mHighlightedPreferenceKey); return mTopLevelAdapter; } diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java index 05fd2aea4de..9b081e18925 100644 --- a/src/com/android/settings/search/SearchFeatureProvider.java +++ b/src/com/android/settings/search/SearchFeatureProvider.java @@ -19,7 +19,6 @@ package com.android.settings.search; import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO; import android.annotation.NonNull; -import android.app.Activity; import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Context; @@ -30,8 +29,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toolbar; +import androidx.fragment.app.FragmentActivity; + import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.activityembedding.ActivityEmbeddingUtils; +import com.android.settings.homepage.TopLevelSettings; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.search.SearchIndexableResources; @@ -66,7 +69,7 @@ public interface SearchFeatureProvider { /** * Initializes the search toolbar. */ - default void initSearchToolbar(Activity activity, Toolbar toolbar, int pageId) { + default void initSearchToolbar(FragmentActivity activity, Toolbar toolbar, int pageId) { if (activity == null || toolbar == null) { return; } @@ -91,7 +94,8 @@ public interface SearchFeatureProvider { toolbar.setOnClickListener(tb -> { final Context context = activity.getApplicationContext(); - final Intent intent = buildSearchIntent(context, pageId); + final Intent intent = buildSearchIntent(context, pageId) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (activity.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) { @@ -103,8 +107,19 @@ public interface SearchFeatureProvider { FeatureFactory.getFactory(context).getMetricsFeatureProvider() .logSettingsTileClick(KEY_HOMEPAGE_SEARCH_BAR, pageId); - final Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle(); - activity.startActivity(intent, bundle); + + if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) { + final TopLevelSettings fragment = (TopLevelSettings) activity + .getSupportFragmentManager().findFragmentById(R.id.main_content); + if (fragment != null) { + fragment.disableMenuHighlight(); + } + activity.startActivity(intent); + } else { + final Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(activity) + .toBundle(); + activity.startActivity(intent, bundle); + } }); } diff --git a/src/com/android/settings/search/SearchResultTrampoline.java b/src/com/android/settings/search/SearchResultTrampoline.java index 3414efeda72..d20a2ea860d 100644 --- a/src/com/android/settings/search/SearchResultTrampoline.java +++ b/src/com/android/settings/search/SearchResultTrampoline.java @@ -20,52 +20,102 @@ import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENT import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB; import android.app.Activity; +import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; import com.android.settings.SettingsActivity; import com.android.settings.SubSettings; import com.android.settings.activityembedding.ActivityEmbeddingRulesController; +import com.android.settings.activityembedding.ActivityEmbeddingUtils; import com.android.settings.overlay.FeatureFactory; +import java.net.URISyntaxException; + /** * A trampoline activity that launches setting result page. */ public class SearchResultTrampoline extends Activity { + private static final String TAG = "SearchResultTrampoline"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final ComponentName callingActivity = getCallingActivity(); // First make sure caller has privilege to launch a search result page. FeatureFactory.getFactory(this) .getSearchFeatureProvider() - .verifyLaunchSearchResultPageCaller(this, getCallingActivity()); + .verifyLaunchSearchResultPageCaller(this, callingActivity); // Didn't crash, proceed and launch the result as a subsetting. - final Intent intent = getIntent(); + Intent intent = getIntent(); + final String highlightMenuKey = intent.getStringExtra( + Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY); - // Hack to take EXTRA_FRAGMENT_ARG_KEY from intent and set into - // EXTRA_SHOW_FRAGMENT_ARGUMENTS. This is necessary because intent could be from external - // caller and args may not persisted. - final String settingKey = intent.getStringExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY); - final int tab = intent.getIntExtra(EXTRA_SHOW_FRAGMENT_TAB, 0); - final Bundle args = new Bundle(); - args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, settingKey); - args.putInt(EXTRA_SHOW_FRAGMENT_TAB, tab); - intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + final String fragment = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT); + if (!TextUtils.isEmpty(fragment)) { + // Hack to take EXTRA_FRAGMENT_ARG_KEY from intent and set into + // EXTRA_SHOW_FRAGMENT_ARGUMENTS. This is necessary because intent could be from + // external caller and args may not persisted. + final String settingKey = intent.getStringExtra( + SettingsActivity.EXTRA_FRAGMENT_ARG_KEY); + final int tab = intent.getIntExtra(EXTRA_SHOW_FRAGMENT_TAB, 0); + final Bundle args = new Bundle(); + args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, settingKey); + args.putInt(EXTRA_SHOW_FRAGMENT_TAB, tab); + intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); - // Register SplirPairRule for SubSettings, set clearTop false to prevent unexpected back - // navigation behavior. - ActivityEmbeddingRulesController.registerSubSettingsPairRuleIfNeeded(this /* context */, - false /* clearTop*/); + // Reroute request to SubSetting. + intent.setClass(this /* context */, SubSettings.class); + } else { + // Direct link case + final String intentUriString = intent.getStringExtra( + Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI); + if (TextUtils.isEmpty(intentUriString)) { + Log.e(TAG, "No EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI for deep link"); + finish(); + return; + } - // Reroute request to SubSetting. - intent.setClass(this /* context */, SubSettings.class) - .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - startActivity(intent); + try { + intent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException e) { + Log.e(TAG, "Failed to parse deep link intent: " + e); + finish(); + return; + } + } + + intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + + if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) { + startActivity(intent); + } else if (isFromSettingsIntelligence(callingActivity)) { + // Register SplitPairRule for SubSettings, set clearTop false to prevent unexpected back + // navigation behavior. + ActivityEmbeddingRulesController.registerSubSettingsPairRuleIfNeeded(this, + false /* clearTop */); + // TODO: pass menu key to homepage + intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } else { + // Two-pane case + intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(SettingsActivity.getTrampolineIntent(intent, highlightMenuKey)); + } // Done. finish(); } + private boolean isFromSettingsIntelligence(ComponentName callingActivity) { + return callingActivity != null && TextUtils.equals( + callingActivity.getPackageName(), + FeatureFactory.getFactory(this).getSearchFeatureProvider() + .getSettingsIntelligencePkgName(this)); + } } diff --git a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java index feb9510bff9..d6635a197c2 100644 --- a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java +++ b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java @@ -365,7 +365,6 @@ public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider { // The classname and intent information comes from the PreIndexData // This will be more clear when provider conversion is done at PreIndex time. raw.className = bundle.getTargetClass().getName(); - } rawList.addAll(providerRaws); } diff --git a/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java b/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java index 19a91f65d16..bf92bbdf679 100644 --- a/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java +++ b/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java @@ -16,7 +16,6 @@ package com.android.settings.widget; -import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; @@ -34,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.settings.Utils; import com.android.settings.activityembedding.ActivityEmbeddingUtils; +import com.android.settings.homepage.SettingsHomepageActivity; /** * Adapter for highlighting top level preferences @@ -54,7 +54,7 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt final int mIconColorHighlight; private final Context mContext; - private final Activity mActivity; + private final SettingsHomepageActivity mHomepageActivity; private final RecyclerView mRecyclerView; private final int mNormalBackgroundRes; private String mHighlightKey; @@ -63,13 +63,13 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt private boolean mHighlightNeeded; private boolean mScrolled; - public HighlightableTopLevelPreferenceAdapter(Activity activity, + public HighlightableTopLevelPreferenceAdapter(SettingsHomepageActivity homepageActivity, PreferenceGroup preferenceGroup, RecyclerView recyclerView, String key) { super(preferenceGroup); mRecyclerView = recyclerView; mHighlightKey = key; mContext = preferenceGroup.getContext(); - mActivity = activity; + mHomepageActivity = homepageActivity; final TypedValue outValue = new TypedValue(); mContext.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true /* resolveRefs */); @@ -115,7 +115,7 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt * A function can highlight a specific setting in recycler view. */ public void requestHighlight() { - if (mRecyclerView == null || TextUtils.isEmpty(mHighlightKey)) { + if (mRecyclerView == null) { return; } @@ -194,6 +194,11 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt return; } + if (mHomepageActivity.registerHomepageLoadedListenerIfNeeded( + () -> scrollToPositionIfNeeded(position))) { + return; + } + // Only when the recyclerView is loaded, it can be scrolled final View view = mRecyclerView.getChildAt(position); if (view == null) { @@ -236,6 +241,6 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt } private boolean isHighlightNeeded() { - return ActivityEmbeddingUtils.isTwoPaneResolution(mActivity); + return ActivityEmbeddingUtils.isTwoPaneResolution(mHomepageActivity); } } diff --git a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java index 444a8137889..5de57b6c95e 100644 --- a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java @@ -19,7 +19,6 @@ package com.android.settings.search; import static com.google.common.truth.Truth.assertThat; -import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Intent; @@ -29,6 +28,8 @@ import android.net.Uri; import android.provider.Settings; import android.widget.Toolbar; +import androidx.fragment.app.FragmentActivity; + import com.android.settings.R; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowUtils; @@ -46,13 +47,13 @@ import org.robolectric.shadows.ShadowPackageManager; public class SearchFeatureProviderImplTest { private SearchFeatureProviderImpl mProvider; - private Activity mActivity; + private FragmentActivity mActivity; private ShadowPackageManager mPackageManager; @Before public void setUp() { FakeFeatureFactory.setupForTest(); - mActivity = Robolectric.setupActivity(Activity.class); + mActivity = Robolectric.setupActivity(FragmentActivity.class); mProvider = new SearchFeatureProviderImpl(); mPackageManager = Shadows.shadowOf(mActivity.getPackageManager()); Settings.Global.putInt(mActivity.getContentResolver(),