diff --git a/aconfig/settings_flag_declarations.aconfig b/aconfig/settings_flag_declarations.aconfig
index 0007bbffb75..983d4b3e6d9 100644
--- a/aconfig/settings_flag_declarations.aconfig
+++ b/aconfig/settings_flag_declarations.aconfig
@@ -56,3 +56,10 @@ flag {
description: "This flag controls the About phone > Legal information page migration"
bug: "323791114"
}
+
+flag {
+ name: "updated_suggestion_card_aosp"
+ namespace: "android_settings"
+ description: "Use updated suggestion card(s) in AOSP Settings"
+ bug: "323258154"
+}
diff --git a/res/drawable/suggestion_icon_background.xml b/res/drawable/suggestion_icon_background.xml
new file mode 100644
index 00000000000..a6dee4836cf
--- /dev/null
+++ b/res/drawable/suggestion_icon_background.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/suggestion_tile.xml b/res/layout/suggestion_tile.xml
new file mode 100644
index 00000000000..fcb0d4f78a1
--- /dev/null
+++ b/res/layout/suggestion_tile.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 689262fa3e8..2a9b4334e29 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -297,6 +297,7 @@
4dp
14dp
16dp
+ 18dp
16dp
@*android:dimen/config_dialogCornerRadius
12dp
diff --git a/res/values/styles.xml b/res/values/styles.xml
index c02378e3f7c..70d3670cebb 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -1017,4 +1017,22 @@
- @null
+
+
+
+
+
+
diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java
index fe97759e225..6c4d1ad0255 100644
--- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java
+++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
/** Interface should be implemented if you have added new suggestions */
@@ -46,6 +47,20 @@ public interface SuggestionFeatureProvider {
/**
* Returns the class of {@link Fragment} that supports contextual suggestion.
+ *
+ * @deprecated - use {@link SuggestionFeatureProvider#getSuggestionFragment()} instead.
*/
- Class extends Fragment> getContextualSuggestionFragment();
+ @Deprecated
+ @Nullable
+ default Class extends Fragment> getContextualSuggestionFragment() {
+ return null;
+ }
+
+ /**
+ * Returns the class of {@link Fragment} that provides the UI for Suggestions.
+ */
+ @Nullable
+ default Class extends Fragment> getSuggestionFragment() {
+ return getContextualSuggestionFragment();
+ }
}
diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java
index 066901015d4..5e8e3740904 100644
--- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java
+++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java
@@ -22,12 +22,14 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.android.settings.Settings.NightDisplaySuggestionActivity;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollSuggestionActivity;
import com.android.settings.biometrics.fingerprint.FingerprintSuggestionActivity;
import com.android.settings.display.NightDisplayPreferenceController;
+import com.android.settings.flags.Flags;
import com.android.settings.notification.zen.ZenOnboardingActivity;
import com.android.settings.notification.zen.ZenSuggestionActivity;
import com.android.settings.password.ScreenLockSuggestionActivity;
@@ -81,8 +83,13 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider
return context.getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE);
}
+ @Nullable
@Override
- public Class extends Fragment> getContextualSuggestionFragment() {
- return null;
+ public Class extends Fragment> getSuggestionFragment() {
+ if (Flags.updatedSuggestionCardAosp()) {
+ return SuggestionFragment.class;
+ } else {
+ return null;
+ }
}
}
diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt b/src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt
new file mode 100644
index 00000000000..760ddc9e505
--- /dev/null
+++ b/src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.dashboard.suggestions
+
+import android.app.ActivityOptions
+import android.app.PendingIntent
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.os.Bundle
+import android.os.SystemClock
+import android.service.settings.suggestions.Suggestion
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.settings.core.InstrumentedFragment
+import com.android.settings.homepage.SettingsHomepageActivity
+import com.android.settings.homepage.SplitLayoutListener
+import com.android.settings.overlay.FeatureFactory
+import com.android.settings.R
+import com.android.settingslib.suggestions.SuggestionController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+private const val SUGGESTIONS = "suggestions"
+private const val TAG = "ContextualSuggestFrag"
+private const val FLAG_IS_DISMISSIBLE = 1 shl 2
+
+/**
+ * Fragment to control display and interaction logic for [Suggestion]s
+ */
+class SuggestionFragment : InstrumentedFragment(),
+ SplitLayoutListener, SuggestionController.ServiceConnectionListener {
+
+ private val scope = CoroutineScope(Job() + Dispatchers.Main)
+ private lateinit var suggestionController: SuggestionController
+ private lateinit var suggestionTile: View
+ private var icon: ImageView? = null
+ private var iconFrame: View? = null
+ private var title: TextView? = null
+ private var summary: TextView? = null
+ private var dismiss: ImageView? = null
+ private var iconVisible = true
+ private var startTime: Long = 0
+ private var suggestionsRestored = false
+ private var splitLayoutSupported = false
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ val component = FeatureFactory.featureFactory
+ .suggestionFeatureProvider
+ .suggestionServiceComponent
+ suggestionController = SuggestionController(context, component, this)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ suggestionTile = inflater.inflate(R.layout.suggestion_tile, container, true)
+ icon = suggestionTile.findViewById(android.R.id.icon)
+ iconFrame = suggestionTile.findViewById(android.R.id.icon_frame)
+ title = suggestionTile.findViewById(android.R.id.title)
+ summary = suggestionTile.findViewById(android.R.id.summary)
+ dismiss = suggestionTile.findViewById(android.R.id.closeButton)
+ if (!iconVisible) {
+ onSplitLayoutChanged(false)
+ }
+ // Restore the suggestion and skip reloading
+ if (savedInstanceState != null) {
+ Log.d(TAG, "Restoring suggestions")
+ savedInstanceState.getParcelableArrayList(
+ SUGGESTIONS,
+ Suggestion::class.java
+ )?.let { suggestions ->
+ suggestionsRestored = true
+ startTime = SystemClock.uptimeMillis()
+ updateState(suggestions)
+ }
+ }
+
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putParcelableArrayList(SUGGESTIONS, currentSuggestions)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ suggestionController.start()
+ }
+
+ override fun onStop() {
+ suggestionController.stop()
+ super.onStop()
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.SETTINGS_HOMEPAGE
+ }
+
+ override fun setSplitLayoutSupported(supported: Boolean) {
+ splitLayoutSupported = supported
+ }
+
+ override fun onSplitLayoutChanged(isRegularLayout: Boolean) {
+ iconVisible = isRegularLayout
+ if (splitLayoutSupported) {
+ iconFrame?.visibility = if (iconVisible) View.VISIBLE else View.GONE
+ }
+ }
+
+ override fun onServiceConnected() {
+ loadSuggestions()
+ }
+
+ override fun onServiceDisconnected() {
+ // no-op
+ }
+
+ private fun loadSuggestions() {
+ if (suggestionsRestored) {
+ // Skip first suggestion loading when restored
+ suggestionsRestored = false
+ return
+ }
+
+ startTime = SystemClock.uptimeMillis()
+ scope.launch(Dispatchers.IO) {
+ Log.d(TAG, "Start loading suggestions")
+ val suggestions = suggestionController.suggestions
+ Log.d(TAG, "Loaded suggestions: ${suggestions?.size}")
+ withContext(Dispatchers.Main) {
+ updateState(suggestions)
+ }
+ }
+ }
+
+ private fun updateState(suggestions: List?) {
+ currentSuggestions.clear()
+ if (suggestions.isNullOrEmpty()) {
+ Log.d(TAG, "Remove suggestions")
+ showSuggestionTile(false)
+ return
+ }
+ currentSuggestions.addAll(suggestions)
+
+ // Only take top suggestion; we assume this is the highest rank.
+ val suggestion = suggestions.first()
+ icon?.setImageIcon(suggestion.icon)
+ suggestion.title?.let {
+ title?.text = it
+ } ?: run {
+ Log.d(TAG, "No suggestion title, removing")
+ showSuggestionTile(false)
+ return
+ }
+ val suggestionSummary = suggestion.summary
+ if (suggestionSummary.isNullOrEmpty()) {
+ summary?.visibility = View.GONE
+ } else {
+ summary?.visibility = View.VISIBLE
+ summary?.text = suggestionSummary
+ }
+ if (suggestion.flags and FLAG_IS_DISMISSIBLE != 0) {
+ dismiss?.let { dismissView ->
+ dismissView.visibility = View.VISIBLE
+ dismissView.setOnClickListener {
+ scope.launch(Dispatchers.IO) {
+ suggestionController.dismissSuggestions(suggestion)
+ }
+ if (suggestions.size > 1) {
+ dismissView.visibility = View.GONE
+ updateState(suggestions.subList(1, suggestions.size))
+ } else {
+ currentSuggestions.clear()
+ suggestionTile.visibility = View.GONE
+ }
+ }
+ }
+ }
+ suggestionTile.setOnClickListener {
+ // Notify service that suggestion is being launched. Note that the service does not
+ // actually start the suggestion on our behalf, instead simply logging metrics.
+ scope.launch(Dispatchers.IO) {
+ suggestionController.launchSuggestion(suggestion)
+ }
+ currentSuggestions.clear()
+ try {
+ val options = ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ )
+ suggestion.pendingIntent.send(options.toBundle())
+ } catch (e: PendingIntent.CanceledException) {
+ Log.e(TAG, "Failed to start suggestion ${suggestion.title}", e)
+ }
+ }
+ showSuggestionTile(true)
+ }
+
+ private fun showSuggestionTile(show: Boolean) {
+ val totalTime = SystemClock.uptimeMillis() - startTime
+ Log.d(TAG, "Total loading time: $totalTime ms")
+ mMetricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW,
+ totalTime.toInt()
+ )
+ (activity as? SettingsHomepageActivity)?.showHomepageWithSuggestion(show)
+ }
+
+ private companion object {
+ val currentSuggestions = arrayListOf()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java
index 7a2f52a8674..4b9f6a8da9d 100644
--- a/src/com/android/settings/homepage/SettingsHomepageActivity.java
+++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java
@@ -276,7 +276,8 @@ public class SettingsHomepageActivity extends FragmentActivity implements
final boolean scrollNeeded = mIsEmbeddingActivityEnabled
&& !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);
showSuggestionFragment(scrollNeeded);
- if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {
+ if (!Flags.updatedSuggestionCardAosp()
+ && FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {
showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);
((FrameLayout) findViewById(R.id.main_content))
.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
@@ -477,7 +478,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements
private void showSuggestionFragment(boolean scrollNeeded) {
final Class extends Fragment> fragmentClass = FeatureFactory.getFeatureFactory()
- .getSuggestionFeatureProvider().getContextualSuggestionFragment();
+ .getSuggestionFeatureProvider().getSuggestionFragment();
if (fragmentClass == null) {
return;
}
diff --git a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java
index 0c8af12db29..6d2193cb760 100644
--- a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java
@@ -25,12 +25,19 @@ import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.settings.suggestions.Suggestion;
+import androidx.fragment.app.Fragment;
+
+import com.android.settings.flags.Flags;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowSecureSettings;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -43,6 +50,9 @@ import org.robolectric.annotation.Config;
@Config(shadows = ShadowSecureSettings.class)
public class SuggestionFeatureProviderImplTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private Context mContext;
@Mock
@@ -94,4 +104,20 @@ public class SuggestionFeatureProviderImplTest {
when(mActivityManager.isLowRamDevice()).thenReturn(false);
assertThat(mProvider.isSuggestionEnabled(mContext)).isTrue();
}
+
+ @DisableFlags(Flags.FLAG_UPDATED_SUGGESTION_CARD_AOSP)
+ @Test
+ public void getSuggestionFragment_withFlagDisabled_shouldReturnNull() {
+ Class extends Fragment> fragment = mProvider.getSuggestionFragment();
+
+ assertThat(fragment).isNull();
+ }
+
+ @EnableFlags(Flags.FLAG_UPDATED_SUGGESTION_CARD_AOSP)
+ @Test
+ public void getSuggestionFragment_withFlagEnabled_shouldReturnFragment() {
+ Class extends Fragment> fragment = mProvider.getSuggestionFragment();
+
+ assertThat(fragment).isEqualTo(SuggestionFragment.class);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java
index 31ac9446612..2cf93e4fb3b 100644
--- a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java
+++ b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java
@@ -444,7 +444,7 @@ public class SettingsHomepageActivityTest {
public static class ShadowSuggestionFeatureProviderImpl {
@Implementation
- public Class extends Fragment> getContextualSuggestionFragment() {
+ public Class extends Fragment> getSuggestionFragment() {
return Fragment.class;
}
}