diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java
index 4df4d509538..e068b2f0851 100644
--- a/src/com/android/settings/slices/SettingsSliceProvider.java
+++ b/src/com/android/settings/slices/SettingsSliceProvider.java
@@ -25,13 +25,40 @@ import android.graphics.drawable.Icon;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.support.annotation.VisibleForTesting;
+import android.util.Log;
import com.android.settings.R;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.WeakHashMap;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceProvider;
import androidx.app.slice.builders.ListBuilder;
+/**
+ * A {@link SliceProvider} for Settings to enabled inline results in system apps.
+ *
+ *
{@link SettingsSliceProvider} accepts a {@link Uri} with {@link #SLICE_AUTHORITY} and a
+ * {@code String} key based on the setting intended to be changed. This provider builds a
+ * {@link Slice} and responds to Slice actions through the database defined by
+ * {@link SlicesDatabaseHelper}, whose data is written by {@link SlicesIndexer}.
+ *
+ *
When a {@link Slice} is requested, we start loading {@link SliceData} in the background and
+ * return an stub {@link Slice} with the correct {@link Uri} immediately. In the background, the
+ * data corresponding to the key in the {@link Uri} is read by {@link SlicesDatabaseAccessor}, and
+ * the entire row is converted into a {@link SliceData}. Once complete, it is stored in
+ * {@link #mSliceDataCache}, and then an update sent via the Slice framework to the Slice.
+ * The {@link Slice} displayed by the Slice-presenter will re-query this Slice-provider and find
+ * the {@link SliceData} cached to build the full {@link Slice}.
+ *
+ *
When an action is taken on that {@link Slice}, we receive the action in
+ * {@link SliceBroadcastReceiver}, and use the
+ * {@link com.android.settings.core.BasePreferenceController} indexed as
+ * {@link SlicesDatabaseHelper.IndexColumns#CONTROLLER} to manipulate the setting.
+ */
public class SettingsSliceProvider extends SliceProvider {
private static final String TAG = "SettingsSliceProvider";
@@ -52,6 +79,9 @@ public class SettingsSliceProvider extends SliceProvider {
@VisibleForTesting
SlicesDatabaseAccessor mSlicesDatabaseAccessor;
+ @VisibleForTesting
+ Map mSliceDataCache;
+
public static Uri getUri(String path) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
@@ -62,6 +92,7 @@ public class SettingsSliceProvider extends SliceProvider {
@Override
public boolean onCreateSliceProvider() {
mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext());
+ mSliceDataCache = new WeakHashMap<>();
return true;
}
@@ -75,10 +106,41 @@ public class SettingsSliceProvider extends SliceProvider {
return createWifiSlice(sliceUri);
}
- return getHoldingSlice(sliceUri);
+ SliceData cachedSliceData = mSliceDataCache.get(sliceUri);
+ if (cachedSliceData == null) {
+ loadSliceInBackground(sliceUri);
+ return getSliceStub(sliceUri);
+ }
+
+ // Remove the SliceData from the cache after it has been used to prevent a memory-leak.
+ mSliceDataCache.remove(sliceUri);
+ return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData);
}
- private Slice getHoldingSlice(Uri uri) {
+ @VisibleForTesting
+ void loadSlice(Uri uri) {
+ long startBuildTime = System.currentTimeMillis();
+
+ SliceData sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri);
+ mSliceDataCache.put(uri, sliceData);
+ getContext().getContentResolver().notifyChange(uri, null /* content observer */);
+
+ Log.d(TAG, "Built slice (" + uri + ") in: " +
+ (System.currentTimeMillis() - startBuildTime));
+ }
+
+ @VisibleForTesting
+ void loadSliceInBackground(Uri uri) {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ loadSlice(uri);
+ });
+ }
+
+ /**
+ * @return an empty {@link Slice} with {@param uri} to be used as a stub while the real
+ * {@link SliceData} is loaded from {@link SlicesDatabaseHelper.Tables#TABLE_SLICES_INDEX}.
+ */
+ private Slice getSliceStub(Uri uri) {
return new ListBuilder(getContext(), uri).build();
}
diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
index 2af15e2d3fb..340d04bd401 100644
--- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
+++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
@@ -19,6 +19,8 @@ package com.android.settings.slices;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import android.content.ContentValues;
@@ -38,15 +40,22 @@ import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import java.util.HashMap;
+
import androidx.app.slice.Slice;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SettingsSliceProviderTest {
- private final String fakeTitle = "title";
- private final String KEY = "key";
-
+ private final String KEY = "KEY";
+ private final String TITLE = "title";
+ private final String SUMMARY = "summary";
+ private final String SCREEN_TITLE = "screen title";
+ private final String FRAGMENT_NAME = "fragment name";
+ private final int ICON = 1234; // I declare a thumb war
+ private final Uri URI = Uri.parse("content://com.android.settings.slices/test");
+ private final String PREF_CONTROLLER = FakeToggleController.class.getName();
private Context mContext;
private SettingsSliceProvider mProvider;
private SQLiteDatabase mDb;
@@ -55,6 +64,7 @@ public class SettingsSliceProviderTest {
public void setUp() {
mContext = spy(RuntimeEnvironment.application);
mProvider = spy(new SettingsSliceProvider());
+ mProvider.mSliceDataCache = new HashMap<>();
mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase();
SlicesDatabaseHelper.getInstance(mContext).setIndexedState();
}
@@ -82,10 +92,39 @@ public class SettingsSliceProviderTest {
assertThat(uri.getLastPathSegment()).isEqualTo(KEY);
}
+ @Test
+ public void testLoadSlice_returnsSliceFromAccessor() {
+ ContentResolver mockResolver = mock(ContentResolver.class);
+ doReturn(mockResolver).when(mContext).getContentResolver();
+ doReturn(mContext).when(mProvider).getContext();
+ mProvider.mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(mContext);
+ insertSpecialCase(KEY);
+ Uri uri = SettingsSliceProvider.getUri(KEY);
+
+ mProvider.loadSlice(uri);
+ SliceData data = mProvider.mSliceDataCache.get(uri);
+
+ assertThat(data.getKey()).isEqualTo(KEY);
+ assertThat(data.getTitle()).isEqualTo(TITLE);
+ }
+
+ @Test
+ public void testLoadSlice_cachedEntryRemovedOnBuild() {
+ doReturn(mContext).when(mProvider).getContext();
+ SliceData data = getDummyData();
+ mProvider.mSliceDataCache.put(data.getUri(), data);
+ mProvider.onBindSlice(data.getUri());
+ insertSpecialCase(data.getKey());
+
+ SliceData cachedData = mProvider.mSliceDataCache.get(data.getUri());
+
+ assertThat(cachedData).isNull();
+ }
+
private void insertSpecialCase(String key) {
ContentValues values = new ContentValues();
values.put(SlicesDatabaseHelper.IndexColumns.KEY, key);
- values.put(SlicesDatabaseHelper.IndexColumns.TITLE, fakeTitle);
+ values.put(SlicesDatabaseHelper.IndexColumns.TITLE, TITLE);
values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, "s");
values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, "s");
values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234);
@@ -94,4 +133,17 @@ public class SettingsSliceProviderTest {
mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values);
}
+
+ private SliceData getDummyData() {
+ return new SliceData.Builder()
+ .setKey(KEY)
+ .setTitle(TITLE)
+ .setSummary(SUMMARY)
+ .setScreenTitle(SCREEN_TITLE)
+ .setIcon(ICON)
+ .setFragmentName(FRAGMENT_NAME)
+ .setUri(URI)
+ .setPreferenceControllerClassName(PREF_CONTROLLER)
+ .build();
+ }
}
\ No newline at end of file