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
This commit is contained in:
Matthew Fritze
2018-01-10 11:40:28 -08:00
parent 8fe96d100a
commit 0cb918a1de
2 changed files with 120 additions and 6 deletions

View File

@@ -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.
*
* <p>{@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}.
*
* <p>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}.
*
* <p>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<Uri, SliceData> 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);
}
private Slice getHoldingSlice(Uri uri) {
// Remove the SliceData from the cache after it has been used to prevent a memory-leak.
mSliceDataCache.remove(sliceUri);
return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData);
}
@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();
}

View File

@@ -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();
}
}