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
diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java
index 437fc86..b9d5045 100644
--- a/src/com/android/settings/search/SearchFeatureProvider.java
+++ b/src/com/android/settings/search/SearchFeatureProvider.java
@@ -28,6 +28,7 @@
 
 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 @@
             } 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 08ea7c6..7136182 100644
--- a/src/com/android/settings/slices/SettingsSliceProvider.java
+++ b/src/com/android/settings/slices/SettingsSliceProvider.java
@@ -24,6 +24,7 @@
 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.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 @@
 
     @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 b6f2ab9..970b72a 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.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 @@
                 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 0000000..3663e89
--- /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 f83676a..f72add7 100644
--- a/src/com/android/settings/slices/SliceData.java
+++ b/src/com/android/settings/slices/SliceData.java
@@ -18,7 +18,6 @@
 
 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 @@
             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 0000000..4fca63a
--- /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 18f8cc9..448d8f1 100644
--- a/src/com/android/settings/slices/SlicesDatabaseHelper.java
+++ b/src/com/android/settings/slices/SlicesDatabaseHelper.java
@@ -104,7 +104,7 @@
 
     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 cbf1b75..e5bba61 100644
--- a/src/com/android/settings/slices/SlicesFeatureProvider.java
+++ b/src/com/android/settings/slices/SlicesFeatureProvider.java
@@ -13,5 +13,15 @@
 
     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 34ef884..8e5bc06 100644
--- a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java
+++ b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java
@@ -15,7 +15,7 @@
     @Override
     public SlicesIndexer getSliceIndexer(Context context) {
         if (mSlicesIndexer == null) {
-            mSlicesIndexer = new SlicesIndexer(context.getApplicationContext());
+            mSlicesIndexer = new SlicesIndexer(context);
         }
         return mSlicesIndexer;
     }
@@ -29,9 +29,14 @@
     }
 
     @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 0297f3f..a92388a 100644
--- a/src/com/android/settings/slices/SlicesIndexer.java
+++ b/src/com/android/settings/slices/SlicesIndexer.java
@@ -20,6 +20,7 @@
 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 @@
  */
 class SlicesIndexer implements Runnable {
 
-    private static final String TAG = "SlicesIndexingManager";
+    private static final String TAG = "SlicesIndexer";
 
     private Context mContext;
 
@@ -48,18 +49,27 @@
     }
 
     /**
-     * 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 @@
             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 0000000..1b08e35
--- /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 0000000..2af15e2
--- /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 0000000..efd1cc5
--- /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<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);
+    }
+}
\ 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 0000000..106e6fe
--- /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 0000000..f22e85f
--- /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();
+    }
+}