Merge "Asynchronously load SliceData from SliceProvider"
This commit is contained in:
committed by
Android (Google) Code Review
commit
46a1cb747c
@@ -25,13 +25,40 @@ import android.graphics.drawable.Icon;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.android.settings.R;
|
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.Slice;
|
||||||
import androidx.app.slice.SliceProvider;
|
import androidx.app.slice.SliceProvider;
|
||||||
import androidx.app.slice.builders.ListBuilder;
|
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 {
|
public class SettingsSliceProvider extends SliceProvider {
|
||||||
|
|
||||||
private static final String TAG = "SettingsSliceProvider";
|
private static final String TAG = "SettingsSliceProvider";
|
||||||
@@ -52,6 +79,9 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
SlicesDatabaseAccessor mSlicesDatabaseAccessor;
|
SlicesDatabaseAccessor mSlicesDatabaseAccessor;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Map<Uri, SliceData> mSliceDataCache;
|
||||||
|
|
||||||
public static Uri getUri(String path) {
|
public static Uri getUri(String path) {
|
||||||
return new Uri.Builder()
|
return new Uri.Builder()
|
||||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||||
@@ -62,6 +92,7 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onCreateSliceProvider() {
|
public boolean onCreateSliceProvider() {
|
||||||
mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext());
|
mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext());
|
||||||
|
mSliceDataCache = new WeakHashMap<>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,10 +106,41 @@ public class SettingsSliceProvider extends SliceProvider {
|
|||||||
return createWifiSlice(sliceUri);
|
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();
|
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 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 static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@@ -38,15 +40,22 @@ import org.junit.runner.RunWith;
|
|||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
import androidx.app.slice.Slice;
|
import androidx.app.slice.Slice;
|
||||||
|
|
||||||
@RunWith(SettingsRobolectricTestRunner.class)
|
@RunWith(SettingsRobolectricTestRunner.class)
|
||||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
||||||
public class SettingsSliceProviderTest {
|
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 Context mContext;
|
||||||
private SettingsSliceProvider mProvider;
|
private SettingsSliceProvider mProvider;
|
||||||
private SQLiteDatabase mDb;
|
private SQLiteDatabase mDb;
|
||||||
@@ -55,6 +64,7 @@ public class SettingsSliceProviderTest {
|
|||||||
public void setUp() {
|
public void setUp() {
|
||||||
mContext = spy(RuntimeEnvironment.application);
|
mContext = spy(RuntimeEnvironment.application);
|
||||||
mProvider = spy(new SettingsSliceProvider());
|
mProvider = spy(new SettingsSliceProvider());
|
||||||
|
mProvider.mSliceDataCache = new HashMap<>();
|
||||||
mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase();
|
mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase();
|
||||||
SlicesDatabaseHelper.getInstance(mContext).setIndexedState();
|
SlicesDatabaseHelper.getInstance(mContext).setIndexedState();
|
||||||
}
|
}
|
||||||
@@ -82,10 +92,39 @@ public class SettingsSliceProviderTest {
|
|||||||
assertThat(uri.getLastPathSegment()).isEqualTo(KEY);
|
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) {
|
private void insertSpecialCase(String key) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(SlicesDatabaseHelper.IndexColumns.KEY, key);
|
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.SUMMARY, "s");
|
||||||
values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, "s");
|
values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, "s");
|
||||||
values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234);
|
values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234);
|
||||||
@@ -94,4 +133,17 @@ public class SettingsSliceProviderTest {
|
|||||||
|
|
||||||
mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values);
|
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