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:
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user