OSDN Git Service

Add Slices Data object and DB Contract
authorMatthew Fritze <mfritze@google.com>
Mon, 11 Dec 2017 23:01:11 +0000 (15:01 -0800)
committerMatthew Fritze <mfritze@google.com>
Wed, 13 Dec 2017 18:59:47 +0000 (10:59 -0800)
Add in a Data object used to represent one row
in a new SQLite database used for building Slices.

The database has the following schema:
- Key
- Title
- Subtitle
- Screentitle
- Icon
- Fragment
- Controller

The key is the preference key.
Title, subtitle and Icon are for UI info.
Screentitle and fragment are for the intent.
Controller is used to get dynamic ui info (like summary),
and to take actions on the slice (like a toggle).

The actual indexing and a Slice will be handled in a subsquent CL,
but a prototype can be found here: ag/3324435

Test: robotests
Bug: 67996923
Change-Id: Id91deb58a3ab89ce1dab5a3f34cdb9ade6263aa8

AndroidManifest.xml
src/com/android/settings/search/DatabaseIndexingUtils.java
src/com/android/settings/slices/SettingsSliceProvider.java [moved from src/com/android/settings/SettingsSliceProvider.java with 67% similarity]
src/com/android/settings/slices/SliceBroadcastReceiver.java [moved from src/com/android/settings/SliceBroadcastReceiver.java with 85% similarity]
src/com/android/settings/slices/SliceData.java [new file with mode: 0644]
src/com/android/settings/slices/SlicesDatabaseHelper.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/SearchIndexProviderCodeInspector.java
tests/robotests/src/com/android/settings/slices/SliceDataTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java [new file with mode: 0644]

index 6ccb8d1..0418d51 100644 (file)
             </intent-filter>
         </activity>
 
-        <provider android:name=".SettingsSliceProvider"
+        <provider android:name=".slices.SettingsSliceProvider"
                   android:authorities="com.android.settings.slices"
                   android:exported="true">
         </provider>
 
         <receiver
-            android:name=".SliceBroadcastReceiver" >
+            android:name=".slices.SliceBroadcastReceiver" >
             <intent-filter>
                 <action android:name="com.android.settings.slice.action.WIFI_CHANGED"/>
             </intent-filter>
index 207d09f..94ec650 100644 (file)
@@ -43,7 +43,7 @@ public class DatabaseIndexingUtils {
 
     private static final String TAG = "IndexingUtil";
 
-    private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
+    public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
             "SEARCH_INDEX_DATA_PROVIDER";
 
     /**
  * limitations under the License
  */
 
-package com.android.settings;
+package com.android.settings.slices;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
-import android.app.slice.SliceProvider;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
-import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 
+import com.android.settings.R;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceProvider;
+import androidx.app.slice.builders.ListBuilder;
+
 public class SettingsSliceProvider extends SliceProvider {
     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";
-    // TODO -- Associate slice URI with search result instead of separate hardcoded thing
-    public static final String[] WIFI_SEARCH_TERMS = {"wi-fi", "wifi", "internet"};
 
+    // TODO -- Associate slice URI with search result instead of separate hardcoded thing
     public static Uri getUri(String path) {
         return new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_CONTENT)
@@ -44,7 +47,7 @@ public class SettingsSliceProvider extends SliceProvider {
     }
 
     @Override
-    public boolean onCreate() {
+    public boolean onCreateSliceProvider() {
         return true;
     }
 
@@ -53,15 +56,15 @@ public class SettingsSliceProvider extends SliceProvider {
         String path = sliceUri.getPath();
         switch (path) {
             case "/" + PATH_WIFI:
-                return createWifi(sliceUri);
-
+                return createWifiSlice(sliceUri);
         }
         throw new IllegalArgumentException("Unrecognized slice uri: " + sliceUri);
     }
 
-    private Slice createWifi(Uri uri) {
+
+    // TODO (b/70622039) remove this when the proper wifi slice is enabled.
+    private Slice createWifiSlice(Uri sliceUri) {
         // Get wifi state
-        String[] toggleHints;
         WifiManager wifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
         int wifiState = wifiManager.getWifiState();
         boolean wifiEnabled = false;
@@ -74,7 +77,6 @@ public class SettingsSliceProvider extends SliceProvider {
             case WifiManager.WIFI_STATE_ENABLED:
             case WifiManager.WIFI_STATE_ENABLING:
                 state = wifiManager.getConnectionInfo().getSSID();
-                WifiInfo.removeDoubleQuotes(state);
                 wifiEnabled = true;
                 break;
             case WifiManager.WIFI_STATE_UNKNOWN:
@@ -82,28 +84,17 @@ public class SettingsSliceProvider extends SliceProvider {
                 state = ""; // just don't show anything?
                 break;
         }
-        if (wifiEnabled) {
-            toggleHints = new String[] {Slice.HINT_TOGGLE, Slice.HINT_SELECTED};
-        } else {
-            toggleHints = new String[] {Slice.HINT_TOGGLE};
-        }
-        // Construct the slice
-        Slice.Builder b = new Slice.Builder(uri);
-        b.addSubSlice(new Slice.Builder(b)
-                .addAction(getIntent("android.settings.WIFI_SETTINGS"),
-                        new Slice.Builder(b)
-                                .addText(getContext().getString(R.string.wifi_settings), null)
-                                .addText(state, null)
-                                .addIcon(Icon.createWithResource(getContext(),
-                                        R.drawable.ic_settings_wireless), null, Slice.HINT_HIDDEN)
-                                .addHints(Slice.HINT_TITLE)
-                                .build())
-                .addAction(getBroadcastIntent(ACTION_WIFI_CHANGED),
-                        new Slice.Builder(b)
-                                .addHints(toggleHints)
-                                .build())
-                .build());
-        return b.build();
+
+        boolean finalWifiEnabled = wifiEnabled;
+        return new ListBuilder(sliceUri)
+                .setColor(R.color.material_blue_500)
+                .add(b -> b
+                        .setTitle(getContext().getString(R.string.wifi_settings))
+                        .setTitleItem(Icon.createWithResource(getContext(), R.drawable.wifi_signal))
+                        .setSubtitle(state)
+                        .addToggle(getBroadcastIntent(ACTION_WIFI_CHANGED), finalWifiEnabled)
+                        .setContentIntent(getIntent(Intent.ACTION_MAIN)))
+                .build();
     }
 
     private PendingIntent getIntent(String action) {
@@ -14,9 +14,9 @@
  * limitations under the License
  */
 
-package com.android.settings;
+package com.android.settings.slices;
 
-import static com.android.settings.SettingsSliceProvider.ACTION_WIFI_CHANGED;
+import static com.android.settings.slices.SettingsSliceProvider.ACTION_WIFI_CHANGED;
 
 import android.app.slice.Slice;
 import android.content.BroadcastReceiver;
@@ -42,8 +42,8 @@ public class SliceBroadcastReceiver extends BroadcastReceiver {
                 // Wait a bit for wifi to update (TODO: is there a better way to do this?)
                 Handler h = new Handler();
                 h.postDelayed(() -> {
-                        Uri uri = SettingsSliceProvider.getUri(SettingsSliceProvider.PATH_WIFI);
-                        context.getContentResolver().notifyChange(uri, null);
+                    Uri uri = SettingsSliceProvider.getUri(SettingsSliceProvider.PATH_WIFI);
+                    context.getContentResolver().notifyChange(uri, null);
                 }, 1000);
                 break;
         }
diff --git a/src/com/android/settings/slices/SliceData.java b/src/com/android/settings/slices/SliceData.java
new file mode 100644 (file)
index 0000000..528f23c
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * 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 android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * TODO (b/67996923) Add SlicesIndexingManager
+ * Data class representing a slice stored by {@link SlicesIndexingManager}.
+ * Note that {@link #key} is treated as a primary key for this class and determines equality.
+ */
+public class SliceData {
+
+    private final String key;
+
+    private final String title;
+
+    private final String summary;
+
+    private final String screenTitle;
+
+    private final int iconResource;
+
+    private final String fragmentClassName;
+
+    private final Uri uri;
+
+    private final String preferenceController;
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public String getSummary() {
+        return summary;
+    }
+
+    public String getScreenTitle() {
+        return screenTitle;
+    }
+
+    public int getIconResource() {
+        return iconResource;
+    }
+
+    public String getFragmentClassName() {
+        return fragmentClassName;
+    }
+
+    public Uri getUri() {
+        return uri;
+    }
+
+    public String getPreferenceController() {
+        return preferenceController;
+    }
+
+    private SliceData(Builder builder) {
+        key = builder.mKey;
+        title = builder.mTitle;
+        summary = builder.mSummary;
+        screenTitle = builder.mScreenTitle;
+        iconResource = builder.mIconResource;
+        fragmentClassName = builder.mFragmentClassName;
+        uri = builder.mUri;
+        preferenceController = builder.mPrefControllerClassName;
+    }
+
+    @Override
+    public int hashCode() {
+        return key.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof SliceData)) {
+            return false;
+        }
+        SliceData newObject = (SliceData) obj;
+        return TextUtils.equals(key, newObject.key);
+    }
+
+    static class Builder {
+        private String mKey;
+
+        private String mTitle;
+
+        private String mSummary;
+
+        private String mScreenTitle;
+
+        private int mIconResource;
+
+        private String mFragmentClassName;
+
+        private Uri mUri;
+
+        private String mPrefControllerClassName;
+
+        public Builder setKey(String key) {
+            mKey = key;
+            return this;
+        }
+
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        public Builder setSummary(String summary) {
+            mSummary = summary;
+            return this;
+        }
+
+        public Builder setScreenTitle(String screenTitle) {
+            mScreenTitle = screenTitle;
+            return this;
+        }
+
+        public Builder setIcon(int iconResource) {
+            mIconResource = iconResource;
+            return this;
+        }
+
+        public Builder setPreferenceControllerClassName(String controllerClassName) {
+            mPrefControllerClassName = controllerClassName;
+            return this;
+        }
+
+        public Builder setFragmentName(String fragmentClassName) {
+            mFragmentClassName = fragmentClassName;
+            return this;
+        }
+
+        public Builder setUri(Uri uri) {
+            mUri = uri;
+            return this;
+        }
+
+        public SliceData build() {
+            if (TextUtils.isEmpty(mKey)) {
+                throw new IllegalStateException("Key cannot be empty");
+            }
+
+            if (TextUtils.isEmpty(mTitle)) {
+                throw new IllegalStateException("Title cannot be empty");
+            }
+
+            if (TextUtils.isEmpty(mFragmentClassName)) {
+                throw new IllegalStateException("Fragment Name cannot be empty");
+            }
+
+            if (TextUtils.isEmpty(mPrefControllerClassName)) {
+                throw new IllegalStateException("Preference Controller cannot be empty");
+            }
+
+            if (mUri == null) {
+                throw new IllegalStateException("Uri cannot be null");
+            }
+
+            return new SliceData(this);
+        }
+
+        public String getKey() {
+            return mKey;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/slices/SlicesDatabaseHelper.java b/src/com/android/settings/slices/SlicesDatabaseHelper.java
new file mode 100644 (file)
index 0000000..a74fc81
--- /dev/null
@@ -0,0 +1,122 @@
+package com.android.settings.slices;
+
+import android.content.Context;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Defines the schema for the Slices database.
+ */
+public class SlicesDatabaseHelper extends SQLiteOpenHelper {
+
+    private static final String TAG = "SlicesDatabaseHelper";
+
+    private static final String DATABASE_NAME = "slices_index.db";
+    private static final String SHARED_PREFS_TAG = "slices_shared_prefs";
+
+    private static final int DATABASE_VERSION = 1;
+
+    public interface Tables {
+        String TABLE_SLICES_INDEX = "slices_index";
+    }
+
+    public interface IndexColumns {
+        /**
+         * Primary key of the DB. Preference key from preference controllers.
+         */
+        String KEY = "key";
+
+        /**
+         * Title of the Setting.
+         */
+        String TITLE = "title";
+
+        /**
+         * Summary / Subtitle for the setting.
+         */
+        String SUBTITLE = "subtitle";
+
+        /**
+         * Title of the Setting screen on which the Setting lives.
+         */
+        String SCREENTITLE = "screentitle";
+
+        /**
+         * Resource ID for the icon of the setting. Should be 0 for no icon.
+         */
+        String ICON_RESOURCE = "icon";
+
+        /**
+         * Classname of the fragment name of the page that hosts the setting.
+         */
+        String FRAGMENT = "fragment";
+
+        /**
+         * Class name of the controller backing the setting. Must be a
+         * {@link com.android.settings.core.BasePreferenceController}.
+         */
+        String CONTROLLER = "controller";
+    }
+
+    private static final String CREATE_SLICES_TABLE =
+            "CREATE VIRTUAL TABLE " + Tables.TABLE_SLICES_INDEX + " USING fts4" +
+                    "(" +
+                    IndexColumns.KEY +
+                    ", " +
+                    IndexColumns.TITLE +
+                    ", " +
+                    IndexColumns.SUBTITLE +
+                    ", " +
+                    IndexColumns.SCREENTITLE +
+                    ", " +
+                    IndexColumns.ICON_RESOURCE +
+                    ", " +
+                    IndexColumns.FRAGMENT +
+                    ", " +
+                    IndexColumns.CONTROLLER +
+                    ");";
+
+    private final Context mContext;
+
+    public SlicesDatabaseHelper(Context context) {
+        super(context, DATABASE_NAME, null /* CursorFactor */, DATABASE_VERSION);
+        mContext = context;
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        createDatabases(db);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        if (oldVersion < DATABASE_VERSION) {
+            Log.d(TAG, "Reconstructing DB from " + oldVersion + "to " + newVersion);
+            reconstruct(db);
+        }
+    }
+
+    @VisibleForTesting
+    void reconstruct(SQLiteDatabase db) {
+        mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
+                .edit()
+                .clear()
+                .commit();
+        dropTables(db);
+        createDatabases(db);
+    }
+
+    private void createDatabases(SQLiteDatabase db) {
+        db.execSQL(CREATE_SLICES_TABLE);
+        Log.d(TAG, "Created databases");
+    }
+
+
+    private void dropTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SLICES_INDEX);
+    }
+}
\ No newline at end of file
index a8372d9..f84f9a2 100644 (file)
@@ -43,13 +43,13 @@ public class SearchIndexProviderCodeInspector extends CodeInspector {
             "SettingsPreferenceFragment should implement Indexable, but these do not:\n";
     private static final String NOT_CONTAINING_PROVIDER_OBJECT_ERROR =
             "Indexable should have public field "
-                    + DatabaseIndexingManager.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER
+                    + DatabaseIndexingUtils.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER
                     + " but these are not:\n";
     private static final String NOT_SHARING_PREF_CONTROLLERS_BETWEEN_FRAG_AND_PROVIDER =
             "DashboardFragment should share pref controllers with its SearchIndexProvider, but "
                     + " these are not: \n";
     private static final String NOT_IN_INDEXABLE_PROVIDER_REGISTRY =
-            "Class containing " + DatabaseIndexingManager.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER
+            "Class containing " + DatabaseIndexingUtils.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER
                     + " must be added to " + SearchIndexableResources.class.getName()
                     + " but these are not: \n";
     private static final String NOT_PROVIDING_VALID_RESOURCE_ERROR =
@@ -173,7 +173,7 @@ public class SearchIndexProviderCodeInspector extends CodeInspector {
     private boolean hasSearchIndexProvider(Class clazz) {
         try {
             final Field f = clazz.getField(
-                    DatabaseIndexingManager.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER);
+                    DatabaseIndexingUtils.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER);
             return f != null;
         } catch (NoClassDefFoundError e) {
             // Cannot find class def, ignore
diff --git a/tests/robotests/src/com/android/settings/slices/SliceDataTest.java b/tests/robotests/src/com/android/settings/slices/SliceDataTest.java
new file mode 100644 (file)
index 0000000..0e4acca
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SliceDataTest {
+
+    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 = "com.android.settings.slices.tester";
+
+    @Test
+    public void testBuilder_buildsMatchingObject() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData data = builder.build();
+
+        assertThat(data.getKey()).isEqualTo(KEY);
+        assertThat(data.getTitle()).isEqualTo(TITLE);
+        assertThat(data.getSummary()).isEqualTo(SUMMARY);
+        assertThat(data.getScreenTitle()).isEqualTo(SCREEN_TITLE);
+        assertThat(data.getIconResource()).isEqualTo(ICON);
+        assertThat(data.getFragmentClassName()).isEqualTo(FRAGMENT_NAME);
+        assertThat(data.getUri()).isEqualTo(URI);
+        assertThat(data.getPreferenceController()).isEqualTo(PREF_CONTROLLER);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBuilder_noKey_throwsIllegalStateException() {
+        new SliceData.Builder()
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER)
+                .build();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBuilder_noTitle_throwsIllegalStateException() {
+        new SliceData.Builder()
+                .setKey(KEY)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER)
+                .build();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBuilder_noFragment_throwsIllegalStateException() {
+        new SliceData.Builder()
+                .setKey(KEY)
+                .setFragmentName(FRAGMENT_NAME)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER)
+                .build();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBuilder_noUri_throwsIllegalStateException() {
+        new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setPreferenceControllerClassName(PREF_CONTROLLER)
+                .build();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBuilder_noPrefController_throwsIllegalStateException() {
+        new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setUri(URI)
+                .setFragmentName(FRAGMENT_NAME)
+                .build();
+    }
+
+    @Test
+    public void testBuilder_noSubtitle_buildsMatchingObject() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData data = builder.build();
+
+        assertThat(data.getKey()).isEqualTo(KEY);
+        assertThat(data.getTitle()).isEqualTo(TITLE);
+        assertThat(data.getSummary()).isNull();
+        assertThat(data.getScreenTitle()).isEqualTo(SCREEN_TITLE);
+        assertThat(data.getIconResource()).isEqualTo(ICON);
+        assertThat(data.getFragmentClassName()).isEqualTo(FRAGMENT_NAME);
+        assertThat(data.getUri()).isEqualTo(URI);
+        assertThat(data.getPreferenceController()).isEqualTo(PREF_CONTROLLER);
+    }
+
+    @Test
+    public void testBuilder_noScreenTitle_buildsMatchingObject() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData data = builder.build();
+
+        assertThat(data.getKey()).isEqualTo(KEY);
+        assertThat(data.getTitle()).isEqualTo(TITLE);
+        assertThat(data.getSummary()).isEqualTo(SUMMARY);
+        assertThat(data.getScreenTitle()).isNull();
+        assertThat(data.getIconResource()).isEqualTo(ICON);
+        assertThat(data.getFragmentClassName()).isEqualTo(FRAGMENT_NAME);
+        assertThat(data.getUri()).isEqualTo(URI);
+        assertThat(data.getPreferenceController()).isEqualTo(PREF_CONTROLLER);
+    }
+
+    @Test
+    public void testBuilder_noIcon_buildsMatchingObject() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData data = builder.build();
+
+        assertThat(data.getKey()).isEqualTo(KEY);
+        assertThat(data.getTitle()).isEqualTo(TITLE);
+        assertThat(data.getSummary()).isEqualTo(SUMMARY);
+        assertThat(data.getScreenTitle()).isEqualTo(SCREEN_TITLE);
+        assertThat(data.getIconResource()).isEqualTo(0);
+        assertThat(data.getFragmentClassName()).isEqualTo(FRAGMENT_NAME);
+        assertThat(data.getUri()).isEqualTo(URI);
+        assertThat(data.getPreferenceController()).isEqualTo(PREF_CONTROLLER);
+    }
+
+    @Test
+    public void testEquality_identicalObjects() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData dataOne = builder.build();
+        SliceData dataTwo = builder.build();
+
+        assertThat(dataOne.hashCode()).isEqualTo(dataTwo.hashCode());
+        assertThat(dataOne).isEqualTo(dataTwo);
+    }
+
+    @Test
+    public void testEquality_matchingKey_EqualObjects() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData dataOne = builder.build();
+
+        builder.setTitle(TITLE + " diff")
+                .setSummary(SUMMARY + " diff")
+                .setScreenTitle(SCREEN_TITLE + " diff")
+                .setIcon(ICON + 1)
+                .setFragmentName(FRAGMENT_NAME + " diff")
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER + " diff");
+
+        SliceData dataTwo = builder.build();
+
+        assertThat(dataOne.hashCode()).isEqualTo(dataTwo.hashCode());
+        assertThat(dataOne).isEqualTo(dataTwo);
+    }
+
+    @Test
+    public void testEquality_differentKey_differentObjects() {
+        SliceData.Builder builder = new SliceData.Builder()
+                .setKey(KEY)
+                .setTitle(TITLE)
+                .setSummary(SUMMARY)
+                .setScreenTitle(SCREEN_TITLE)
+                .setIcon(ICON)
+                .setFragmentName(FRAGMENT_NAME)
+                .setUri(URI)
+                .setPreferenceControllerClassName(PREF_CONTROLLER);
+
+        SliceData dataOne = builder.build();
+
+        builder.setKey("not key");
+        SliceData dataTwo = builder.build();
+
+        assertThat(dataOne.hashCode()).isNotEqualTo(dataTwo.hashCode());
+        assertThat(dataOne).isNotEqualTo(dataTwo);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java
new file mode 100644 (file)
index 0000000..a4020dd
--- /dev/null
@@ -0,0 +1,87 @@
+package com.android.settings.slices;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import com.android.settings.TestConfig;
+import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns;
+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 SlicesDatabaseHelperTest {
+
+    private Context mContext;
+    private SlicesDatabaseHelper mSlicesDatabaseHelper;
+    private SQLiteDatabase mDatabase;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mSlicesDatabaseHelper = new SlicesDatabaseHelper(mContext);
+        mDatabase = mSlicesDatabaseHelper.getWritableDatabase();
+    }
+
+    @After
+    public void cleanUp() {
+        DatabaseTestUtils.clearDb(mContext);
+    }
+
+    @Test
+    public void testDatabaseSchema() {
+        Cursor cursor = mDatabase.rawQuery("SELECT * FROM slices_index", null);
+        String[] columnNames = cursor.getColumnNames();
+
+        String[] expectedNames = new String[]{
+                IndexColumns.KEY,
+                IndexColumns.TITLE,
+                IndexColumns.SUBTITLE,
+                IndexColumns.SCREENTITLE,
+                IndexColumns.ICON_RESOURCE,
+                IndexColumns.FRAGMENT,
+                IndexColumns.CONTROLLER
+        };
+
+        assertThat(columnNames).isEqualTo(expectedNames);
+    }
+
+    @Test
+    public void testUpgrade_dropsOldData() {
+        ContentValues dummyValues = getDummyRow();
+
+        mDatabase.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, dummyValues);
+        Cursor baseline = mDatabase.rawQuery("SELECT * FROM slices_index", null);
+        assertThat(baseline.getCount()).isEqualTo(1);
+
+        mSlicesDatabaseHelper.onUpgrade(mDatabase, 0, 1);
+
+        Cursor newCursor = mDatabase.rawQuery("SELECT * FROM slices_index", null);
+        assertThat(newCursor.getCount()).isEqualTo(0);
+    }
+
+    private ContentValues getDummyRow() {
+        ContentValues values;
+
+        values = new ContentValues();
+        values.put(IndexColumns.KEY, "key");
+        values.put(IndexColumns.TITLE, "title");
+        values.put(IndexColumns.SUBTITLE, "subtitle");
+        values.put(IndexColumns.ICON_RESOURCE, 99);
+        values.put(IndexColumns.FRAGMENT, "fragmentClassName");
+        values.put(IndexColumns.CONTROLLER, "preferenceController");
+
+        return values;
+    }
+}