Build slice from indexed data in SliceProvider

Connect the SliceIndexing data to the SliceProvider,
such that a query to SliceProvider can build a Slice
via the indexed data from SlicesIndexingManager.

We take the key from the Uri supplied to the SettingSliceProvider
and find a potential matching row in the indexed data. The
matched data is then used to Build a slice for the caller.

Bug: 67996923
Test: robotests
Change-Id: If51bfd1a05c3f3817ae720554f95a98fc7b002e1
This commit is contained in:
Matthew Fritze
2017-12-15 10:48:40 -08:00
parent 87f7a1be1e
commit 8c96843fe3
15 changed files with 858 additions and 14 deletions

View File

@@ -28,6 +28,7 @@ import android.widget.Toolbar;
import com.android.settings.core.FeatureFlags; import com.android.settings.core.FeatureFlags;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.overlay.FeatureFactory;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -185,6 +186,9 @@ public interface SearchFeatureProvider {
} else { } else {
intent = new Intent(activity, SearchActivity.class); intent = new Intent(activity, SearchActivity.class);
} }
FeatureFactory.getFactory(
activity.getApplicationContext()).getSlicesFeatureProvider()
.indexSliceDataAsync(activity.getApplicationContext());
activity.startActivityForResult(intent, 0 /* requestCode */); activity.startActivityForResult(intent, 0 /* requestCode */);
}); });
} }

View File

@@ -24,6 +24,7 @@ import android.content.Intent;
import android.graphics.drawable.Icon; 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 com.android.settings.R; import com.android.settings.R;
@@ -32,13 +33,25 @@ import androidx.app.slice.SliceProvider;
import androidx.app.slice.builders.ListBuilder; import androidx.app.slice.builders.ListBuilder;
public class SettingsSliceProvider extends SliceProvider { 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 SLICE_AUTHORITY = "com.android.settings.slices";
public static final String PATH_WIFI = "wifi"; public static final String PATH_WIFI = "wifi";
public static final String ACTION_WIFI_CHANGED = public static final String ACTION_WIFI_CHANGED =
"com.android.settings.slice.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 // TODO -- Associate slice URI with search result instead of separate hardcoded thing
@VisibleForTesting
SlicesDatabaseAccessor mSlicesDatabaseAccessor;
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)
@@ -48,19 +61,26 @@ public class SettingsSliceProvider extends SliceProvider {
@Override @Override
public boolean onCreateSliceProvider() { public boolean onCreateSliceProvider() {
mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext());
return true; return true;
} }
@Override @Override
public Slice onBindSlice(Uri sliceUri) { public Slice onBindSlice(Uri sliceUri) {
String path = sliceUri.getPath(); String path = sliceUri.getPath();
// If adding a new Slice, do not directly match Slice URIs.
// Use {@link SlicesDatabaseAccessor}.
switch (path) { switch (path) {
case "/" + PATH_WIFI: case "/" + PATH_WIFI:
return createWifiSlice(sliceUri); 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. // TODO (b/70622039) remove this when the proper wifi slice is enabled.
private Slice createWifiSlice(Uri sliceUri) { private Slice createWifiSlice(Uri sliceUri) {

View File

@@ -16,7 +16,9 @@
package com.android.settings.slices; 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.ACTION_WIFI_CHANGED;
import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
import android.app.slice.Slice; import android.app.slice.Slice;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@@ -25,19 +27,34 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Handler; 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. * Responds to actions performed on slices and notifies slices of updates in state changes.
*/ */
public class SliceBroadcastReceiver extends BroadcastReceiver { public class SliceBroadcastReceiver extends BroadcastReceiver {
private static String TAG = "SettSliceBroadcastRec";
/**
* TODO (b/) move wifi action into generalized case.
*/
@Override @Override
public void onReceive(Context context, Intent i) { public void onReceive(Context context, Intent intent) {
String action = i.getAction(); String action = intent.getAction();
String key = intent.getStringExtra(EXTRA_SLICE_KEY);
switch (action) { switch (action) {
case ACTION_TOGGLE_CHANGED:
handleToggleAction(context, key);
break;
case ACTION_WIFI_CHANGED: case ACTION_WIFI_CHANGED:
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 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); wm.setWifiEnabled(newState);
// Wait a bit for wifi to update (TODO: is there a better way to do this?) // Wait a bit for wifi to update (TODO: is there a better way to do this?)
Handler h = new Handler(); Handler h = new Handler();
@@ -48,4 +65,28 @@ public class SliceBroadcastReceiver extends BroadcastReceiver {
break; 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);
}
} }

View File

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

View File

@@ -18,7 +18,6 @@ package com.android.settings.slices;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
/** /**
* Data class representing a slice stored by {@link SlicesIndexer}. * 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. * 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; return mKey;
} }
} }
} }

View File

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

View File

@@ -104,7 +104,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper {
public static synchronized SlicesDatabaseHelper getInstance(Context context) { public static synchronized SlicesDatabaseHelper getInstance(Context context) {
if (sSingleton == null) { if (sSingleton == null) {
sSingleton = new SlicesDatabaseHelper(context); sSingleton = new SlicesDatabaseHelper(context.getApplicationContext());
} }
return sSingleton; return sSingleton;
} }

View File

@@ -13,5 +13,15 @@ public interface SlicesFeatureProvider {
SliceDataConverter getSliceDataConverter(Context context); 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); void indexSliceData(Context context);
} }

View File

@@ -15,7 +15,7 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider {
@Override @Override
public SlicesIndexer getSliceIndexer(Context context) { public SlicesIndexer getSliceIndexer(Context context) {
if (mSlicesIndexer == null) { if (mSlicesIndexer == null) {
mSlicesIndexer = new SlicesIndexer(context.getApplicationContext()); mSlicesIndexer = new SlicesIndexer(context);
} }
return mSlicesIndexer; return mSlicesIndexer;
} }
@@ -29,9 +29,14 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider {
} }
@Override @Override
public void indexSliceData(Context context) { public void indexSliceDataAsync(Context context) {
// TODO (b/67996923) add indexing time log
SlicesIndexer indexer = getSliceIndexer(context); SlicesIndexer indexer = getSliceIndexer(context);
ThreadUtils.postOnBackgroundThread(indexer); ThreadUtils.postOnBackgroundThread(indexer);
} }
@Override
public void indexSliceData(Context context) {
SlicesIndexer indexer = getSliceIndexer(context);
indexer.indexSliceData();
}
} }

View File

@@ -20,6 +20,7 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.DashboardFragment;
@@ -36,7 +37,7 @@ import java.util.List;
*/ */
class SlicesIndexer implements Runnable { class SlicesIndexer implements Runnable {
private static final String TAG = "SlicesIndexingManager"; private static final String TAG = "SlicesIndexer";
private Context mContext; private Context mContext;
@@ -48,18 +49,27 @@ class SlicesIndexer implements Runnable {
} }
/** /**
* Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a * Asynchronously index slice data from {@link #indexSliceData()}.
* SQLite database.
*/ */
@Override @Override
public void run() { public void run() {
indexSliceData();
}
/**
* Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a
* SQLite database
*/
protected void indexSliceData() {
if (mHelper.isSliceDataIndexed()) { if (mHelper.isSliceDataIndexed()) {
Log.d(TAG, "Slices already indexed - returning.");
return; return;
} }
SQLiteDatabase database = mHelper.getWritableDatabase(); SQLiteDatabase database = mHelper.getWritableDatabase();
try { try {
long startTime = System.currentTimeMillis();
database.beginTransaction(); database.beginTransaction();
mHelper.reconstruct(mHelper.getWritableDatabase()); mHelper.reconstruct(mHelper.getWritableDatabase());
@@ -67,6 +77,10 @@ class SlicesIndexer implements Runnable {
insertSliceData(database, indexData); insertSliceData(database, indexData);
mHelper.setIndexedState(); mHelper.setIndexedState();
// TODO (b/71503044) Log indexing time.
Log.d(TAG,
"Indexing slices database took: " + (System.currentTimeMillis() - startTime));
database.setTransactionSuccessful(); database.setTransactionSuccessful();
} finally { } finally {
database.endTransaction(); database.endTransaction();

View File

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

View File

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

View File

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

View File

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

View File

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