From 0cb918a1deee84b05766b8354732261a354532bb Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Wed, 10 Jan 2018 11:40:28 -0800 Subject: [PATCH] Asynchronously load SliceData from SliceProvider The slices paradigm is to return a slice as soon as possible whith whatever information is currently available. Then, load the longer information in the background and broadcast on the Uri when the changes are ready. This CL incorprates a cache system for SettingsSliceProvider to load that data in the background to solve the issues of multiple simultaneous callers. Bug: 67996923 Test: robotests Change-Id: I0e3f9984181e1c989fed139707cdb27956cf6de6 --- .../slices/SettingsSliceProvider.java | 66 ++++++++++++++++++- .../slices/SettingsSliceProviderTest.java | 60 +++++++++++++++-- 2 files changed, 120 insertions(+), 6 deletions(-) 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