/* * 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.core; import static junit.framework.Assert.fail; import android.content.Context; import android.content.res.Resources; import android.os.Bundle; import android.platform.test.annotations.Presubmit; 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.Log; import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.DatabaseIndexingUtils; import com.android.settings.search.Indexable; import com.android.settings.search.SearchIndexableRaw; import com.android.settings.search.SearchIndexableResources; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; 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 IGNORE_PREF_TYPES = Arrays.asList( "com.android.settingslib.widget.FooterPreference"); private static final List WHITELISTED_DUPLICATE_KEYS = Arrays.asList( "owner_info_settings", // Lock screen message in security - multiple xml files // contain this because security page is constructed by // combining small xml chunks. Eventually the page // should be formed as one single xml and this entry // should be removed. "dashboard_tile_placeholder", // This is the placeholder pref for injecting dynamic // tiles. // Dup keys from About Phone v2 experiment. "ims_reg_state", "bt_address", "device_model", "firmware_version", "regulatory_info", "manual", "legal_container", "device_feedback", "fcc_equipment_id", "sim_status", "build_number", "phone_number", "imei_info", "wifi_ip_address", "wifi_mac_address", "safety_info", // Dupe keys from data usage v2. "data_usage_screen", "cellular_data_usage", "data_usage_wifi_screen", "status_header", "billing_preference", "data_usage_cellular_screen", "wifi_data_usage", "data_usage_enable" ); 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. *

* So in this test we are checking preferences participating in search. *

* Note: Preference is not limited to just object. Everything in preference xml * should have a key. */ @Test @Presubmit public void allPreferencesShouldHaveUniqueKey() throws IOException, XmlPullParserException, Resources.NotFoundException { final Set uniqueKeys = new HashSet<>(); final Set nullKeyClasses = new HashSet<>(); final Set duplicatedKeys = new HashSet<>(); final SearchIndexableResources resources = FeatureFactory.getFactory(mContext).getSearchFeatureProvider() .getSearchIndexableResources(); for (Class clazz : resources.getProviderValues()) { verifyPreferenceKeys(uniqueKeys, duplicatedKeys, nullKeyClasses, clazz); } if (!nullKeyClasses.isEmpty()) { final StringBuilder nullKeyErrors = new StringBuilder() .append("Each preference/SearchIndexableData must have a key, ") .append("the following classes have null 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 verifyPreferenceKeys(Set uniqueKeys, Set duplicatedKeys, Set nullKeyClasses, Class clazz) throws IOException, XmlPullParserException, Resources.NotFoundException { if (clazz == null) { return; } final String className = clazz.getName(); final Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(clazz); final List rawsToIndex = provider.getRawDataToIndex(mContext, true); final List resourcesToIndex = provider.getXmlResourcesToIndex(mContext, true); verifyResources(className, resourcesToIndex, uniqueKeys, duplicatedKeys, nullKeyClasses); verifyRaws(className, rawsToIndex, uniqueKeys, duplicatedKeys, nullKeyClasses); } private void verifyResources(String className, List resourcesToIndex, Set uniqueKeys, Set duplicatedKeys, Set nullKeyClasses) throws IOException, XmlPullParserException, Resources.NotFoundException { if (resourcesToIndex == null) { Log.d(TAG, className + "is not providing SearchIndexableResource, skipping"); return; } for (SearchIndexableResource sir : resourcesToIndex) { final List metadata = PreferenceXmlParserUtils.extractMetadata(mContext, sir.xmlResId, MetadataFlag.FLAG_INCLUDE_PREF_SCREEN | MetadataFlag.FLAG_NEED_KEY | MetadataFlag.FLAG_NEED_PREF_TYPE); for (Bundle bundle : metadata) { final String type = bundle.getString(PreferenceXmlParserUtils.METADATA_PREF_TYPE); if (IGNORE_PREF_TYPES.contains(type)) { continue; } final String key = bundle.getString(PreferenceXmlParserUtils.METADATA_KEY); if (TextUtils.isEmpty(key)) { Log.e(TAG, "Every preference must have an key; found null key" + " in " + className); nullKeyClasses.add(className); continue; } if (uniqueKeys.contains(key) && !WHITELISTED_DUPLICATE_KEYS.contains(key)) { Log.e(TAG, "Every preference key must unique; found " + " in " + className + " / " + key); duplicatedKeys.add(key); } uniqueKeys.add(key); } } } private void verifyRaws(String className, List rawsToIndex, Set uniqueKeys, Set duplicatedKeys, Set nullKeyClasses) { if (rawsToIndex == null) { Log.d(TAG, className + "is not providing SearchIndexableRaw, skipping"); return; } for (SearchIndexableRaw raw : rawsToIndex) { if (TextUtils.isEmpty(raw.key)) { Log.e(TAG, "Every SearchIndexableRaw must have an key; found null key" + " in " + className); nullKeyClasses.add(className); continue; } if (uniqueKeys.contains(raw.key) && !WHITELISTED_DUPLICATE_KEYS.contains(raw.key)) { Log.e(TAG, "Every SearchIndexableRaw key must unique; found " + raw.key + " in " + className); duplicatedKeys.add(raw.key); } } } }