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