diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java index 437fc86a165..b9d5045b452 100644 --- a/src/com/android/settings/search/SearchFeatureProvider.java +++ b/src/com/android/settings/search/SearchFeatureProvider.java @@ -28,6 +28,7 @@ import android.widget.Toolbar; import com.android.settings.core.FeatureFlags; import com.android.settings.dashboard.SiteMapManager; +import com.android.settings.overlay.FeatureFactory; import java.util.List; import java.util.concurrent.ExecutorService; @@ -185,6 +186,9 @@ public interface SearchFeatureProvider { } else { intent = new Intent(activity, SearchActivity.class); } + FeatureFactory.getFactory( + activity.getApplicationContext()).getSlicesFeatureProvider() + .indexSliceDataAsync(activity.getApplicationContext()); activity.startActivityForResult(intent, 0 /* requestCode */); }); } diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 08ea7c6fdcb..7136182dd51 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.graphics.drawable.Icon; import android.net.Uri; import android.net.wifi.WifiManager; +import android.support.annotation.VisibleForTesting; import com.android.settings.R; @@ -32,13 +33,25 @@ import androidx.app.slice.SliceProvider; import androidx.app.slice.builders.ListBuilder; public class SettingsSliceProvider extends SliceProvider { + + private static final String TAG = "SettingsSliceProvider"; + public static final String SLICE_AUTHORITY = "com.android.settings.slices"; public static final String PATH_WIFI = "wifi"; public static final String ACTION_WIFI_CHANGED = "com.android.settings.slice.action.WIFI_CHANGED"; + public static final String ACTION_TOGGLE_CHANGED = + "com.android.settings.slice.action.TOGGLE_CHANGED"; + + public static final String EXTRA_SLICE_KEY = "com.android.settings.slice.extra.key"; + // TODO -- Associate slice URI with search result instead of separate hardcoded thing + + @VisibleForTesting + SlicesDatabaseAccessor mSlicesDatabaseAccessor; + public static Uri getUri(String path) { return new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) @@ -48,19 +61,26 @@ public class SettingsSliceProvider extends SliceProvider { @Override public boolean onCreateSliceProvider() { + mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext()); return true; } @Override public Slice onBindSlice(Uri sliceUri) { String path = sliceUri.getPath(); + // If adding a new Slice, do not directly match Slice URIs. + // Use {@link SlicesDatabaseAccessor}. switch (path) { case "/" + PATH_WIFI: return createWifiSlice(sliceUri); } - throw new IllegalArgumentException("Unrecognized slice uri: " + sliceUri); + + return getHoldingSlice(sliceUri); } + private Slice getHoldingSlice(Uri uri) { + return new ListBuilder(uri).build(); + } // TODO (b/70622039) remove this when the proper wifi slice is enabled. private Slice createWifiSlice(Uri sliceUri) { diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index b6f2ab945ca..970b72a9caf 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -16,7 +16,9 @@ package com.android.settings.slices; +import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_WIFI_CHANGED; +import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; import android.app.slice.Slice; import android.content.BroadcastReceiver; @@ -25,19 +27,34 @@ import android.content.Intent; import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Handler; +import android.text.TextUtils; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.TogglePreferenceController; /** * Responds to actions performed on slices and notifies slices of updates in state changes. */ public class SliceBroadcastReceiver extends BroadcastReceiver { + private static String TAG = "SettSliceBroadcastRec"; + + /** + * TODO (b/) move wifi action into generalized case. + */ @Override - public void onReceive(Context context, Intent i) { - String action = i.getAction(); + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String key = intent.getStringExtra(EXTRA_SLICE_KEY); + switch (action) { + case ACTION_TOGGLE_CHANGED: + handleToggleAction(context, key); + break; case ACTION_WIFI_CHANGED: WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - boolean newState = i.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, wm.isWifiEnabled()); + boolean newState = intent.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, + wm.isWifiEnabled()); wm.setWifiEnabled(newState); // Wait a bit for wifi to update (TODO: is there a better way to do this?) Handler h = new Handler(); @@ -48,4 +65,28 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { break; } } + + private void handleToggleAction(Context context, String key) { + if (TextUtils.isEmpty(key)) { + throw new IllegalStateException("No key passed to Intent for toggle controller"); + } + + BasePreferenceController controller = getBasePreferenceController(context, key); + + if (!(controller instanceof TogglePreferenceController)) { + throw new IllegalStateException("Toggle action passed for a non-toggle key: " + key); + } + + // TODO post context.getContentResolver().notifyChanged(uri, null) in the Toggle controller + // so that it's automatically broadcast to any slice. + TogglePreferenceController toggleController = (TogglePreferenceController) controller; + boolean currentValue = toggleController.isChecked(); + toggleController.setChecked(!currentValue); + } + + private BasePreferenceController getBasePreferenceController(Context context, String key) { + final SlicesDatabaseAccessor accessor = new SlicesDatabaseAccessor(context); + final SliceData sliceData = accessor.getSliceDataFromKey(key); + return SliceBuilderUtils.getPreferenceController(context, sliceData); + } } diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java new file mode 100644 index 00000000000..3663e897217 --- /dev/null +++ b/src/com/android/settings/slices/SliceBuilderUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.settings.slices; + +import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.text.TextUtils; + +import com.android.settings.SubSettings; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.TogglePreferenceController; +import com.android.settings.search.DatabaseIndexingUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import androidx.app.slice.Slice; +import androidx.app.slice.builders.ListBuilder; +import androidx.app.slice.builders.ListBuilder.RowBuilder; + +/** + * Utility class to build Slices objects and Preference Controllers based on the Database managed + * by {@link SlicesDatabaseHelper} + */ +public class SliceBuilderUtils { + + private static final String TAG = "SliceBuilder"; + + /** + * Build a Slice from {@link SliceData}. + * + * @return a {@link Slice} based on the data provided by {@param sliceData}. + * Will build an {@link Intent} based Slice unless the Preference Controller name in + * {@param sliceData} is an inline controller. + */ + public static Slice buildSlice(Context context, SliceData sliceData) { + final PendingIntent contentIntent = getContentIntent(context, sliceData); + final Icon icon = Icon.createWithResource(context, sliceData.getIconResource()); + String summaryText = sliceData.getSummary(); + String subtitleText = TextUtils.isEmpty(summaryText) + ? sliceData.getScreenTitle() + : summaryText; + + RowBuilder builder = new RowBuilder(sliceData.getUri()) + .setTitle(sliceData.getTitle()) + .setTitleItem(icon) + .setSubtitle(subtitleText) + .setContentIntent(contentIntent); + + BasePreferenceController controller = getPreferenceController(context, sliceData); + + // TODO (b/71640747) Respect setting availability. + // TODO (b/71640678) Add dynamic summary text. + + if (controller instanceof TogglePreferenceController) { + addToggleAction(context, builder, ((TogglePreferenceController) controller).isChecked(), + sliceData.getKey()); + } + + return new ListBuilder(sliceData.getUri()) + .addRow(builder) + .build(); + } + + /** + * Looks at the {@link SliceData#preferenceController} from {@param sliceData} and attempts to + * build a {@link BasePreferenceController}. + */ + public static BasePreferenceController getPreferenceController(Context context, + SliceData sliceData) { + // TODO check for context-only controller first. + try { + Class clazz = Class.forName(sliceData.getPreferenceController()); + Constructor preferenceConstructor = clazz.getConstructor(Context.class, + String.class); + return (BasePreferenceController) preferenceConstructor.newInstance( + new Object[]{context, sliceData.getKey()}); + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | + IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException( + "Invalid preference controller: " + sliceData.getPreferenceController()); + } + } + + private static void addToggleAction(Context context, RowBuilder builder, boolean isChecked, + String key) { + PendingIntent actionIntent = getActionIntent(context, + SettingsSliceProvider.ACTION_TOGGLE_CHANGED, key); + builder.addToggle(actionIntent, isChecked); + } + + private static PendingIntent getActionIntent(Context context, String action, String key) { + Intent intent = new Intent(action); + intent.setClass(context, SliceBroadcastReceiver.class); + intent.putExtra(EXTRA_SLICE_KEY, key); + return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + private static PendingIntent getContentIntent(Context context, SliceData sliceData) { + Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, + sliceData.getFragmentClassName(), sliceData.getKey(), sliceData.getScreenTitle(), + 0 /* TODO */); + intent.setClassName("com.android.settings", SubSettings.class.getName()); + return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */); + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SliceData.java b/src/com/android/settings/slices/SliceData.java index f83676af832..f72add75828 100644 --- a/src/com/android/settings/slices/SliceData.java +++ b/src/com/android/settings/slices/SliceData.java @@ -18,7 +18,6 @@ package com.android.settings.slices; import android.net.Uri; import android.text.TextUtils; - /** * Data class representing a slice stored by {@link SlicesIndexer}. * Note that {@link #key} is treated as a primary key for this class and determines equality. @@ -179,5 +178,4 @@ public class SliceData { return mKey; } } - } \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesDatabaseAccessor.java b/src/com/android/settings/slices/SlicesDatabaseAccessor.java new file mode 100644 index 00000000000..4fca63ad5fb --- /dev/null +++ b/src/com/android/settings/slices/SlicesDatabaseAccessor.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.settings.slices; + +import static com.android.settings.slices.SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import android.content.Context; +import android.os.Binder; + +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; + +import androidx.app.slice.Slice; + +/** + * Class used to map a {@link Uri} from {@link SettingsSliceProvider} to a Slice. + */ +public class SlicesDatabaseAccessor { + + public static final String[] SELECT_COLUMNS = { + IndexColumns.KEY, + IndexColumns.TITLE, + IndexColumns.SUMMARY, + IndexColumns.SCREENTITLE, + IndexColumns.ICON_RESOURCE, + IndexColumns.FRAGMENT, + IndexColumns.CONTROLLER, + }; + + Context mContext; + + public SlicesDatabaseAccessor(Context context) { + mContext = context; + } + + /** + * Query the slices database and return a {@link SliceData} object corresponding to the row + * matching the key provided by the {@param uri}. Additionally adds the {@param uri} to the + * {@link SliceData} object so the {@link Slice} can bind to the {@link Uri}. + * Used when building a {@link Slice}. + */ + public SliceData getSliceDataFromUri(Uri uri) { + String key = uri.getLastPathSegment(); + Cursor cursor = getIndexedSliceData(key); + return buildSliceData(cursor, uri); + } + + /** + * Query the slices database and return a {@link SliceData} object corresponding to the row + * matching the {@param key}. + * Used when handling the action of the {@link Slice}. + */ + public SliceData getSliceDataFromKey(String key) { + Cursor cursor = getIndexedSliceData(key); + return buildSliceData(cursor, null /* uri */); + } + + private Cursor getIndexedSliceData(String path) { + verifyIndexing(); + + final String whereClause = buildWhereClause(); + final SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); + final SQLiteDatabase database = helper.getReadableDatabase(); + final String[] selection = new String[]{path}; + + Cursor resultCursor = database.query(TABLE_SLICES_INDEX, SELECT_COLUMNS, whereClause, + selection, null /* groupBy */, null /* having */, null /* orderBy */); + + int numResults = resultCursor.getCount(); + + if (numResults == 0) { + throw new IllegalStateException("Invalid Slices key from path: " + path); + } + + if (numResults > 1) { + throw new IllegalStateException( + "Should not match more than 1 slice with path: " + path); + } + + resultCursor.moveToFirst(); + return resultCursor; + } + + private String buildWhereClause() { + return new StringBuilder(IndexColumns.KEY) + .append(" = ?") + .toString(); + } + + private SliceData buildSliceData(Cursor cursor, Uri uri) { + final String key = cursor.getString(cursor.getColumnIndex(IndexColumns.KEY)); + final String title = cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE)); + final String summary = cursor.getString(cursor.getColumnIndex(IndexColumns.SUMMARY)); + final String screenTitle = cursor.getString( + cursor.getColumnIndex(IndexColumns.SCREENTITLE)); + final int iconResource = cursor.getInt(cursor.getColumnIndex(IndexColumns.ICON_RESOURCE)); + final String fragmentClassName = cursor.getString( + cursor.getColumnIndex(IndexColumns.FRAGMENT)); + final String controllerClassName = cursor.getString( + cursor.getColumnIndex(IndexColumns.CONTROLLER)); + + return new SliceData.Builder() + .setKey(key) + .setTitle(title) + .setSummary(summary) + .setScreenTitle(screenTitle) + .setIcon(iconResource) + .setFragmentName(fragmentClassName) + .setPreferenceControllerClassName(controllerClassName) + .setUri(uri) + .build(); + } + + private void verifyIndexing() { + final long uidToken = Binder.clearCallingIdentity(); + try { + FeatureFactory.getFactory( + mContext).getSlicesFeatureProvider().indexSliceData(mContext); + } finally { + Binder.restoreCallingIdentity(uidToken); + } + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesDatabaseHelper.java b/src/com/android/settings/slices/SlicesDatabaseHelper.java index 18f8cc91107..448d8f17ba5 100644 --- a/src/com/android/settings/slices/SlicesDatabaseHelper.java +++ b/src/com/android/settings/slices/SlicesDatabaseHelper.java @@ -104,7 +104,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { public static synchronized SlicesDatabaseHelper getInstance(Context context) { if (sSingleton == null) { - sSingleton = new SlicesDatabaseHelper(context); + sSingleton = new SlicesDatabaseHelper(context.getApplicationContext()); } return sSingleton; } diff --git a/src/com/android/settings/slices/SlicesFeatureProvider.java b/src/com/android/settings/slices/SlicesFeatureProvider.java index cbf1b75ed1b..e5bba617e48 100644 --- a/src/com/android/settings/slices/SlicesFeatureProvider.java +++ b/src/com/android/settings/slices/SlicesFeatureProvider.java @@ -13,5 +13,15 @@ public interface SlicesFeatureProvider { SliceDataConverter getSliceDataConverter(Context context); + /** + * Asynchronous call to index the data used to build Slices. + * If the data is already indexed, the data will not change. + */ + void indexSliceDataAsync(Context context); + + /** + * Indexes the data used to build Slices. + * If the data is already indexed, the data will not change. + */ void indexSliceData(Context context); } \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java index 34ef884f641..8e5bc067150 100644 --- a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java +++ b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java @@ -15,7 +15,7 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { @Override public SlicesIndexer getSliceIndexer(Context context) { if (mSlicesIndexer == null) { - mSlicesIndexer = new SlicesIndexer(context.getApplicationContext()); + mSlicesIndexer = new SlicesIndexer(context); } return mSlicesIndexer; } @@ -29,9 +29,14 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { } @Override - public void indexSliceData(Context context) { - // TODO (b/67996923) add indexing time log + public void indexSliceDataAsync(Context context) { SlicesIndexer indexer = getSliceIndexer(context); ThreadUtils.postOnBackgroundThread(indexer); } -} + + @Override + public void indexSliceData(Context context) { + SlicesIndexer indexer = getSliceIndexer(context); + indexer.indexSliceData(); + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesIndexer.java b/src/com/android/settings/slices/SlicesIndexer.java index 0297f3fa557..a92388aa752 100644 --- a/src/com/android/settings/slices/SlicesIndexer.java +++ b/src/com/android/settings/slices/SlicesIndexer.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.VisibleForTesting; +import android.util.Log; import com.android.settings.dashboard.DashboardFragment; @@ -36,7 +37,7 @@ import java.util.List; */ class SlicesIndexer implements Runnable { - private static final String TAG = "SlicesIndexingManager"; + private static final String TAG = "SlicesIndexer"; private Context mContext; @@ -48,18 +49,27 @@ class SlicesIndexer implements Runnable { } /** - * Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a - * SQLite database. + * Asynchronously index slice data from {@link #indexSliceData()}. */ @Override public void run() { + indexSliceData(); + } + + /** + * Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a + * SQLite database + */ + protected void indexSliceData() { if (mHelper.isSliceDataIndexed()) { + Log.d(TAG, "Slices already indexed - returning."); return; } SQLiteDatabase database = mHelper.getWritableDatabase(); try { + long startTime = System.currentTimeMillis(); database.beginTransaction(); mHelper.reconstruct(mHelper.getWritableDatabase()); @@ -67,6 +77,10 @@ class SlicesIndexer implements Runnable { insertSliceData(database, indexData); mHelper.setIndexedState(); + + // TODO (b/71503044) Log indexing time. + Log.d(TAG, + "Indexing slices database took: " + (System.currentTimeMillis() - startTime)); database.setTransactionSuccessful(); } finally { database.endTransaction(); diff --git a/tests/robotests/src/com/android/settings/slices/FakeToggleController.java b/tests/robotests/src/com/android/settings/slices/FakeToggleController.java new file mode 100644 index 00000000000..1b08e3566aa --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/FakeToggleController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.TogglePreferenceController; + +public class FakeToggleController extends TogglePreferenceController { + + private String settingKey = "toggle_key"; + + private final int ON = 1; + private final int OFF = 0; + + public FakeToggleController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public boolean isChecked() { + return Settings.System.getInt(mContext.getContentResolver(), + settingKey, OFF) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.System.putInt(mContext.getContentResolver(), settingKey, + isChecked ? ON : OFF); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java new file mode 100644 index 00000000000..2af15e2d3fb --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.ContentValues; +import android.content.Context; +import android.content.ContentResolver; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.DatabaseTestUtils; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +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 Context mContext; + private SettingsSliceProvider mProvider; + private SQLiteDatabase mDb; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + mProvider = spy(new SettingsSliceProvider()); + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + SlicesDatabaseHelper.getInstance(mContext).setIndexedState(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + } + + @Test + public void testInitialSliceReturned_emmptySlice() { + Uri uri = SettingsSliceProvider.getUri(KEY); + Slice slice = mProvider.onBindSlice(uri); + + assertThat(slice.getUri()).isEqualTo(uri); + assertThat(slice.getItems()).isEmpty(); + } + + @Test + public void testUriBuilder_returnsValidSliceUri() { + Uri uri = SettingsSliceProvider.getUri(KEY); + + assertThat(uri.getScheme()).isEqualTo(ContentResolver.SCHEME_CONTENT); + assertThat(uri.getAuthority()).isEqualTo(SettingsSliceProvider.SLICE_AUTHORITY); + assertThat(uri.getLastPathSegment()).isEqualTo(KEY); + } + + 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.SUMMARY, "s"); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, "s"); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, "test"); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, "test"); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java new file mode 100644 index 00000000000..efd1cc5e263 --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.sqlite.SQLiteDatabase; + +import com.android.settings.TestConfig; +import com.android.settings.search.FakeIndexProvider; +import com.android.settings.search.SearchIndexableResources; +import com.android.settings.testutils.DatabaseTestUtils; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.HashSet; +import java.util.Set; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SliceBroadcastReceiverTest { + + private final String fakeTitle = "title"; + private final String fakeSummary = "summary"; + private final String fakeScreenTitle = "screen_title"; + private final int fakeIcon = 1234; + private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); + private final String fakeControllerName = FakeToggleController.class.getName(); + + private Context mContext; + private SQLiteDatabase mDb; + private SliceBroadcastReceiver mReceiver; + + private Set mProviderClassesCopy; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + mReceiver = new SliceBroadcastReceiver(); + mProviderClassesCopy = new HashSet<>(SearchIndexableResources.providerValues()); + SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); + helper.setIndexedState(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + SearchIndexableResources.providerValues().clear(); + SearchIndexableResources.providerValues().addAll(mProviderClassesCopy); + } + + @Test + public void testOnReceive_toggleChanged() { + String key = "key"; + SearchIndexableResources.providerValues().clear(); + insertSpecialCase(key); + // Turn on toggle setting + FakeToggleController fakeToggleController = new FakeToggleController(mContext, key); + fakeToggleController.setChecked(true); + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, key); + + assertThat(fakeToggleController.isChecked()).isTrue(); + + // Toggle setting + mReceiver.onReceive(mContext, intent); + + assertThat(fakeToggleController.isChecked()).isFalse(); + } + + @Test(expected = IllegalStateException.class) + public void testOnReceive_noExtra_illegalSatetException() { + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + mReceiver.onReceive(mContext, intent); + } + + @Test(expected = IllegalStateException.class) + public void testOnReceive_emptyKey_throwsIllegalStateException() { + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, (String) null); + mReceiver.onReceive(mContext, intent); + } + + 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.SUMMARY, fakeSummary); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, fakeScreenTitle); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, fakeIcon); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, fakeFragmentClassName); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, fakeControllerName); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java new file mode 100644 index 00000000000..106e6fe4324 --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.search.FakeIndexProvider; +import com.android.settings.testutils.DatabaseTestUtils; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SlicesDatabaseAccessorTest { + + private final String fakeTitle = "title"; + private final String fakeSummary = "summary"; + private final String fakeScreenTitle = "screen_title"; + private final int fakeIcon = 1234; + private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); + private final String fakeControllerName = FakePreferenceController.class.getName(); + + private Context mContext; + private SQLiteDatabase mDb; + private SlicesDatabaseAccessor mAccessor; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mAccessor = spy(new SlicesDatabaseAccessor(mContext)); + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + SlicesDatabaseHelper.getInstance(mContext).setIndexedState(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + } + + @Test + public void testGetSliceDataFromKey_validKey_validSliceReturned() { + String key = "key"; + insertSpecialCase(key); + + SliceData data = mAccessor.getSliceDataFromKey(key); + + assertThat(data.getKey()).isEqualTo(key); + assertThat(data.getTitle()).isEqualTo(fakeTitle); + assertThat(data.getSummary()).isEqualTo(fakeSummary); + assertThat(data.getScreenTitle()).isEqualTo(fakeScreenTitle); + assertThat(data.getIconResource()).isEqualTo(fakeIcon); + assertThat(data.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(data.getUri()).isNull(); + assertThat(data.getPreferenceController()).isEqualTo(fakeControllerName); + } + + @Test(expected = IllegalStateException.class) + public void testGetSliceDataFromKey_invalidKey_errorThrown() { + String key = "key"; + + mAccessor.getSliceDataFromKey(key); + } + + @Test + public void testGetSliceFromUri_validUri_validSliceReturned() { + String key = "key"; + insertSpecialCase(key); + Uri uri = SettingsSliceProvider.getUri(key); + + SliceData data = mAccessor.getSliceDataFromUri(uri); + + assertThat(data.getKey()).isEqualTo(key); + assertThat(data.getTitle()).isEqualTo(fakeTitle); + assertThat(data.getSummary()).isEqualTo(fakeSummary); + assertThat(data.getScreenTitle()).isEqualTo(fakeScreenTitle); + assertThat(data.getIconResource()).isEqualTo(fakeIcon); + assertThat(data.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(data.getUri()).isEqualTo(uri); + assertThat(data.getPreferenceController()).isEqualTo(fakeControllerName); + } + + @Test(expected = IllegalStateException.class) + public void testGetSliceFromUri_invalidUri_errorThrown() { + Uri uri = SettingsSliceProvider.getUri("durr"); + + mAccessor.getSliceDataFromUri(uri); + } + + 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.SUMMARY, fakeSummary); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, fakeScreenTitle); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, fakeIcon); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, fakeFragmentClassName); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, fakeControllerName); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java new file mode 100644 index 00000000000..f22e85ffdff --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.settings.slices; + +import static com.android.settings.TestConfig.SDK_VERSION; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import androidx.app.slice.Slice; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = SDK_VERSION) +public class SlicesDatabaseUtilsTest { + + 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; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + } + + @Test + public void testBuildSlice_returnsMatchingSlice() { + Slice slice = SliceBuilderUtils.buildSlice(mContext, getDummyData()); + + assertThat(slice).isNotNull(); // TODO improve test for Slice content + } + + @Test + public void testGetPreferenceController_buildsMatchingController() { + BasePreferenceController controller = SliceBuilderUtils.getPreferenceController(mContext, + getDummyData()); + + assertThat(controller).isInstanceOf(FakeToggleController.class); + } + + 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(); + } +}