| /* |
| * 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<String> IGNORE_PREF_TYPES = Arrays.asList( |
| "com.android.settingslib.widget.FooterPreference"); |
| |
| private static final List<String> 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. |
| * <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 |
| @Presubmit |
| 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<>(); |
| 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<String> uniqueKeys, Set<String> duplicatedKeys, |
| Set<String> 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<SearchIndexableRaw> rawsToIndex = provider.getRawDataToIndex(mContext, true); |
| final List<SearchIndexableResource> resourcesToIndex = |
| provider.getXmlResourcesToIndex(mContext, true); |
| verifyResources(className, resourcesToIndex, uniqueKeys, duplicatedKeys, nullKeyClasses); |
| verifyRaws(className, rawsToIndex, uniqueKeys, duplicatedKeys, nullKeyClasses); |
| } |
| |
| private void verifyResources(String className, List<SearchIndexableResource> resourcesToIndex, |
| Set<String> uniqueKeys, Set<String> duplicatedKeys, Set<String> 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<Bundle> 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<SearchIndexableRaw> rawsToIndex, |
| Set<String> uniqueKeys, Set<String> duplicatedKeys, Set<String> 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); |
| } |
| } |
| } |
| } |