Merge "Add a test to enforce unique id for preferences (in search)"
diff --git a/res/xml/pick_up_gesture_settings.xml b/res/xml/pick_up_gesture_settings.xml
index 0b4a1de..e1414cd 100644
--- a/res/xml/pick_up_gesture_settings.xml
+++ b/res/xml/pick_up_gesture_settings.xml
@@ -18,6 +18,7 @@
 <PreferenceScreen
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:key="gesture_pick_up_screen"
     android:title="@string/ambient_display_pickup_title">
 
     <com.android.settings.widget.VideoPreference
diff --git a/res/xml/tts_engine_picker.xml b/res/xml/tts_engine_picker.xml
index c0a464c..92bfede 100644
--- a/res/xml/tts_engine_picker.xml
+++ b/res/xml/tts_engine_picker.xml
@@ -15,6 +15,7 @@
 -->
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        android:key="tts_engine_picker_screen"
         android:title="@string/tts_engine_preference_title">
 
     <PreferenceCategory android:key="tts_engine_preference_category"/>
diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/search/SearchIndexableResources.java
index 7252e2d..89924bb 100644
--- a/src/com/android/settings/search/SearchIndexableResources.java
+++ b/src/com/android/settings/search/SearchIndexableResources.java
@@ -86,7 +86,6 @@
 import com.android.settings.users.UserSettings;
 import com.android.settings.wallpaper.WallpaperTypeSettings;
 import com.android.settings.wifi.ConfigureWifiSettings;
-import com.android.settings.wifi.SavedAccessPointsWifiSettings;
 import com.android.settings.wifi.WifiSettings;
 
 import java.util.Collection;
@@ -129,7 +128,6 @@
         addIndex(WifiSettings.class);
         addIndex(NetworkDashboardFragment.class);
         addIndex(ConfigureWifiSettings.class);
-        addIndex(SavedAccessPointsWifiSettings.class);
         addIndex(BluetoothSettings.class);
         addIndex(SimSettings.class);
         addIndex(DataUsageSummary.class);
diff --git a/tests/unit/src/com/android/settings/UniquePreferenceTest.java b/tests/unit/src/com/android/settings/UniquePreferenceTest.java
new file mode 100644
index 0000000..2236b94
--- /dev/null
+++ b/tests/unit/src/com/android/settings/UniquePreferenceTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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;
+
+import static junit.framework.Assert.fail;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.provider.SearchIndexableResource;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.settings.search.DatabaseIndexingUtils;
+import com.android.settings.search.Indexable;
+import com.android.settings.search.SearchIndexableResources;
+import com.android.settings.search.XmlParserUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class UniquePreferenceTest {
+
+    private static final String TAG = "UniquePreferenceTest";
+    private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList(
+            "Preference", "PreferenceCategory", "PreferenceScreen");
+
+    private Context mContext;
+
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    /**
+     * All preferences should have their unique key. It's especially important for many parts of
+     * Settings to work properly: we assume pref keys are unique in displaying, search ranking,\
+     * search result suppression, and many other areas.
+     * <p/>
+     * So in this test we are checking preferences participating in search.
+     * <p/>
+     * Note: Preference is not limited to just <Preference/> object. Everything in preference xml
+     * should have a key.
+     */
+    @Test
+    public void allPreferencesShouldHaveUniqueKey()
+            throws IOException, XmlPullParserException, Resources.NotFoundException {
+        final Set<String> uniqueKeys = new HashSet<>();
+        final Set<String> nullKeyClasses = new HashSet<>();
+        final Set<String> duplicatedKeys = new HashSet<>();
+        for (SearchIndexableResource sir : SearchIndexableResources.values()) {
+            verifyPreferenceIdInXml(uniqueKeys, duplicatedKeys, nullKeyClasses, sir);
+        }
+
+        if (!nullKeyClasses.isEmpty()) {
+            final StringBuilder nullKeyErrors = new StringBuilder()
+                    .append("Each preference must have a key, ")
+                    .append("the following classes have pref without keys:\n");
+            for (String c : nullKeyClasses) {
+                nullKeyErrors.append(c).append("\n");
+            }
+            fail(nullKeyErrors.toString());
+        }
+
+        if (!duplicatedKeys.isEmpty()) {
+            final StringBuilder dupeKeysError = new StringBuilder(
+                    "The following keys are not unique\n");
+            for (String c : duplicatedKeys) {
+                dupeKeysError.append(c).append("\n");
+            }
+            fail(dupeKeysError.toString());
+        }
+    }
+
+    private void verifyPreferenceIdInXml(Set<String> uniqueKeys, Set<String> duplicatedKeys,
+            Set<String> nullKeyClasses, SearchIndexableResource page)
+            throws IOException, XmlPullParserException, Resources.NotFoundException {
+        final Class<?> clazz = DatabaseIndexingUtils.getIndexableClass(page.className);
+
+        final Indexable.SearchIndexProvider provider =
+                DatabaseIndexingUtils.getSearchIndexProvider(clazz);
+        final List<SearchIndexableResource> resourcesToIndex =
+                provider.getXmlResourcesToIndex(mContext, true);
+        if (resourcesToIndex == null) {
+            Log.d(TAG, page.className + "is not providing SearchIndexableResource, skipping");
+            return;
+        }
+
+        for (SearchIndexableResource sir : resourcesToIndex) {
+            if (sir.xmlResId <= 0) {
+                Log.d(TAG, page.className + " doesn't have a valid xml to index.");
+                continue;
+            }
+            final XmlResourceParser parser = mContext.getResources().getXml(sir.xmlResId);
+
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+                // Parse next until start tag is found
+            }
+            final int outerDepth = parser.getDepth();
+
+            do {
+                if (type != XmlPullParser.START_TAG) {
+                    continue;
+                }
+                final String nodeName = parser.getName();
+                if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith("Preference")) {
+                    continue;
+                }
+                final AttributeSet attrs = Xml.asAttributeSet(parser);
+                final String key = XmlParserUtils.getDataKey(mContext, attrs);
+                if (TextUtils.isEmpty(key)) {
+                    Log.e(TAG, "Every preference must have an key; found null key"
+                            + " in " + page.className
+                            + " at " + parser.getPositionDescription());
+                    nullKeyClasses.add(page.className);
+                    continue;
+                }
+                if (uniqueKeys.contains(key)) {
+                    Log.e(TAG, "Every preference key must unique; found " + nodeName
+                            + " in " + page.className
+                            + " at " + parser.getPositionDescription());
+                    duplicatedKeys.add(key);
+                }
+                uniqueKeys.add(key);
+            } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth));
+        }
+    }
+}