| /* |
| * 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.search; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Matchers.anyBoolean; |
| import static org.mockito.Matchers.anyInt; |
| import static org.mockito.Matchers.anyList; |
| import static org.mockito.Matchers.anyString; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.verify; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.os.Build; |
| import android.provider.SearchIndexableData; |
| import android.util.ArrayMap; |
| |
| import com.android.settings.search.indexing.PreIndexData; |
| import com.android.settings.testutils.DatabaseTestUtils; |
| import com.android.settings.testutils.FakeFeatureFactory; |
| import com.android.settings.testutils.SettingsRobolectricTestRunner; |
| import com.android.settings.testutils.shadow.ShadowRunnableAsyncTask; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Config; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| @RunWith(SettingsRobolectricTestRunner.class) |
| @Config(shadows = ShadowRunnableAsyncTask.class) |
| public class DatabaseIndexingManagerTest { |
| |
| private final String localeStr = "en_US"; |
| |
| private final int rank = 8; |
| private final String title = "title\u2011title"; |
| private final String updatedTitle = "title-title"; |
| private final String normalizedTitle = "titletitle"; |
| private final String summaryOn = "summary\u2011on"; |
| private final String updatedSummaryOn = "summary-on"; |
| private final String normalizedSummaryOn = "summaryon"; |
| private final String summaryOff = "summary\u2011off"; |
| private final String entries = "entries"; |
| private final String keywords = "keywords, keywordss, keywordsss"; |
| private final String spaceDelimittedKeywords = "keywords keywordss keywordsss"; |
| private final String screenTitle = "screen title"; |
| private final String className = "class name"; |
| private final int iconResId = 0xff; |
| private final String action = "action"; |
| private final String targetPackage = "target package"; |
| private final String targetClass = "target class"; |
| private final String packageName = "package name"; |
| private final String key = "key"; |
| private final int userId = -1; |
| private final boolean enabled = true; |
| |
| private final String TITLE_ONE = "title one"; |
| private final String TITLE_TWO = "title two"; |
| private final String KEY_ONE = "key one"; |
| private final String KEY_TWO = "key two"; |
| |
| private Context mContext; |
| |
| private DatabaseIndexingManager mManager; |
| private SQLiteDatabase mDb; |
| |
| private final List<ResolveInfo> FAKE_PROVIDER_LIST = new ArrayList<>(); |
| |
| @Mock |
| private PackageManager mPackageManager; |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| mContext = spy(RuntimeEnvironment.application); |
| mManager = spy(new DatabaseIndexingManager(mContext)); |
| mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); |
| |
| doReturn(mPackageManager).when(mContext).getPackageManager(); |
| doReturn(FAKE_PROVIDER_LIST).when(mPackageManager) |
| .queryIntentContentProviders(any(Intent.class), anyInt()); |
| FakeFeatureFactory.setupForTest(); |
| } |
| |
| @After |
| public void cleanUp() { |
| DatabaseTestUtils.clearDb(mContext); |
| } |
| |
| @Test |
| public void testDatabaseSchema() { |
| Cursor dbCursor = mDb.query("prefs_index", null, null, null, null, null, null); |
| List<String> columnNames = new ArrayList<>(Arrays.asList(dbCursor.getColumnNames())); |
| // Note that docid is not included. |
| List<String> expColumnNames = Arrays.asList( |
| "locale", |
| "data_rank", |
| "data_title", |
| "data_title_normalized", |
| "data_summary_on", |
| "data_summary_on_normalized", |
| "data_summary_off", |
| "data_summary_off_normalized", |
| "data_entries", |
| "data_keywords", |
| "class_name", |
| "screen_title", |
| "intent_action", |
| "intent_target_package", |
| "intent_target_class", |
| "icon", |
| "enabled", |
| "data_key_reference", |
| "user_id", |
| "payload_type", |
| "payload" |
| ); |
| // Prevent database schema regressions |
| assertThat(columnNames).containsAllIn(expColumnNames); |
| } |
| |
| // Test new public indexing flow |
| |
| @Test |
| public void testPerformIndexing_fullIndex_getsDataFromProviders() { |
| SearchIndexableRaw rawData = getFakeRaw(); |
| PreIndexData data = getPreIndexData(rawData); |
| doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); |
| doReturn(true).when(mManager) |
| .isFullIndex(any(Context.class), anyString(), anyString(), anyString()); |
| |
| mManager.performIndexing(); |
| |
| verify(mManager).updateDatabase(data, true /* isFullIndex */); |
| } |
| |
| @Test |
| public void testPerformIndexing_fullIndex_databaseDropped() { |
| // Initialize the Manager and force rebuild |
| DatabaseIndexingManager manager = |
| spy(new DatabaseIndexingManager(mContext)); |
| doReturn(false).when(mManager) |
| .isFullIndex(any(Context.class), anyString(), anyString(), anyString()); |
| |
| // Insert data point which will be dropped |
| insertSpecialCase("Ceci n'est pas un pipe", true, "oui oui mon ami"); |
| |
| manager.performIndexing(); |
| |
| // Assert that the Old Title is no longer in the database, since it was dropped |
| final Cursor oldCursor = mDb.rawQuery("SELECT * FROM prefs_index", null); |
| |
| assertThat(oldCursor.getCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void testPerformIndexing_isfullIndex() { |
| SearchIndexableRaw rawData = getFakeRaw(); |
| PreIndexData data = getPreIndexData(rawData); |
| doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); |
| doReturn(true).when(mManager) |
| .isFullIndex(any(Context.class), anyString(), anyString(), anyString()); |
| |
| mManager.performIndexing(); |
| |
| verify(mManager).updateDatabase(data, true /* isFullIndex */); |
| } |
| |
| @Test |
| public void testPerformIndexing_onOta_buildNumberIsCached() { |
| mManager.performIndexing(); |
| |
| assertThat(IndexDatabaseHelper.isBuildIndexed(mContext, Build.FINGERPRINT)).isTrue(); |
| } |
| |
| @Test |
| public void testLocaleUpdated_afterIndexing_localeNotAdded() { |
| PreIndexData emptydata = new PreIndexData(); |
| mManager.updateDatabase(emptydata, true /* isFullIndex */); |
| |
| assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isFalse(); |
| } |
| |
| @Test |
| public void testLocaleUpdated_afterFullIndexing_localeAdded() { |
| mManager.performIndexing(); |
| |
| assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isTrue(); |
| } |
| |
| @Test |
| public void testUpdateDatabase_newEligibleData_addedToDatabase() { |
| // Test that addDataToDatabase is called when dataToUpdate is non-empty |
| PreIndexData indexData = new PreIndexData(); |
| indexData.dataToUpdate.add(getFakeRaw()); |
| mManager.updateDatabase(indexData, true /* isFullIndex */); |
| |
| Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index", null); |
| cursor.moveToPosition(0); |
| |
| // Locale |
| assertThat(cursor.getString(0)).isEqualTo(localeStr); |
| // Data Title |
| assertThat(cursor.getString(2)).isEqualTo(updatedTitle); |
| // Normalized Title |
| assertThat(cursor.getString(3)).isEqualTo(normalizedTitle); |
| // Summary On |
| assertThat(cursor.getString(4)).isEqualTo(updatedSummaryOn); |
| // Summary On Normalized |
| assertThat(cursor.getString(5)).isEqualTo(normalizedSummaryOn); |
| // Entries |
| assertThat(cursor.getString(8)).isEqualTo(entries); |
| // Keywords |
| assertThat(cursor.getString(9)).isEqualTo(spaceDelimittedKeywords); |
| // Screen Title |
| assertThat(cursor.getString(10)).isEqualTo(screenTitle); |
| // Class Name |
| assertThat(cursor.getString(11)).isEqualTo(className); |
| // Icon |
| assertThat(cursor.getInt(12)).isEqualTo(iconResId); |
| // Intent Action |
| assertThat(cursor.getString(13)).isEqualTo(action); |
| // Target Package |
| assertThat(cursor.getString(14)).isEqualTo(targetPackage); |
| // Target Class |
| assertThat(cursor.getString(15)).isEqualTo(targetClass); |
| // Enabled |
| assertThat(cursor.getInt(16) == 1).isEqualTo(enabled); |
| // Data ref key |
| assertThat(cursor.getString(17)).isNotNull(); |
| // User Id |
| assertThat(cursor.getInt(18)).isEqualTo(userId); |
| // Payload Type - default is 0 |
| assertThat(cursor.getInt(19)).isEqualTo(0); |
| // Payload |
| byte[] payload = cursor.getBlob(20); |
| ResultPayload unmarshalledPayload = ResultPayloadUtils.unmarshall(payload, |
| ResultPayload.CREATOR); |
| assertThat(unmarshalledPayload).isInstanceOf(ResultPayload.class); |
| } |
| |
| @Test |
| public void testUpdateDataInDatabase_enabledResultsAreNonIndexable_becomeDisabled() { |
| // Both results are enabled, and then TITLE_ONE gets disabled. |
| final boolean enabled = true; |
| insertSpecialCase(TITLE_ONE, enabled, KEY_ONE); |
| insertSpecialCase(TITLE_TWO, enabled, KEY_TWO); |
| Map<String, Set<String>> niks = new ArrayMap<>(); |
| Set<String> keys = new HashSet<>(); |
| keys.add(KEY_ONE); |
| niks.put(targetPackage, keys); |
| |
| mManager.updateDataInDatabase(mDb, niks); |
| |
| Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 0", null); |
| cursor.moveToPosition(0); |
| |
| assertThat(cursor.getString(2)).isEqualTo(TITLE_ONE); |
| } |
| |
| @Test |
| public void testUpdateDataInDatabase_disabledResultsAreIndexable_becomeEnabled() { |
| // Both results are initially disabled, and then TITLE_TWO gets enabled. |
| final boolean enabled = false; |
| insertSpecialCase(TITLE_ONE, enabled, KEY_ONE); |
| insertSpecialCase(TITLE_TWO, enabled, KEY_TWO); |
| Map<String, Set<String>> niks = new ArrayMap<>(); |
| Set<String> keys = new HashSet<>(); |
| keys.add(KEY_ONE); |
| niks.put(targetPackage, keys); |
| |
| mManager.updateDataInDatabase(mDb, niks); |
| |
| Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 1", null); |
| cursor.moveToPosition(0); |
| |
| assertThat(cursor.getString(2)).isEqualTo(TITLE_TWO); |
| } |
| |
| @Test |
| public void testEmptyNonIndexableKeys_emptyDataKeyResources_addedToDatabase() { |
| insertSpecialCase(TITLE_ONE, true /* enabled */, null /* dataReferenceKey */); |
| PreIndexData emptydata = new PreIndexData(); |
| mManager.updateDatabase(emptydata, false /* needsReindexing */); |
| |
| Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 1", null); |
| cursor.moveToPosition(0); |
| assertThat(cursor.getCount()).isEqualTo(1); |
| assertThat(cursor.getString(2)).isEqualTo(TITLE_ONE); |
| } |
| |
| // Util functions |
| |
| private SearchIndexableRaw getFakeRaw() { |
| return getFakeRaw(localeStr); |
| } |
| |
| private SearchIndexableRaw getFakeRaw(String localeStr) { |
| SearchIndexableRaw data = new SearchIndexableRaw(mContext); |
| data.locale = new Locale(localeStr); |
| data.rank = rank; |
| data.title = title; |
| data.summaryOn = summaryOn; |
| data.summaryOff = summaryOff; |
| data.entries = entries; |
| data.keywords = keywords; |
| data.screenTitle = screenTitle; |
| data.className = className; |
| data.packageName = packageName; |
| data.iconResId = iconResId; |
| data.intentAction = action; |
| data.intentTargetPackage = targetPackage; |
| data.intentTargetClass = targetClass; |
| data.key = key; |
| data.userId = userId; |
| data.enabled = enabled; |
| return data; |
| } |
| |
| private void insertSpecialCase(String specialCase, boolean enabled, String key) { |
| ContentValues values = new ContentValues(); |
| values.put(IndexDatabaseHelper.IndexColumns.DOCID, specialCase.hashCode()); |
| values.put(IndexDatabaseHelper.IndexColumns.LOCALE, localeStr); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, 1); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_ENTRIES, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.CLASS_NAME, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.SCREEN_TITLE, "Moves"); |
| values.put(IndexDatabaseHelper.IndexColumns.INTENT_ACTION, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE, targetPackage); |
| values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.ICON, ""); |
| values.put(IndexDatabaseHelper.IndexColumns.ENABLED, enabled); |
| values.put(IndexDatabaseHelper.IndexColumns.DATA_KEY_REF, key); |
| values.put(IndexDatabaseHelper.IndexColumns.USER_ID, 0); |
| values.put(IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE, 0); |
| values.put(IndexDatabaseHelper.IndexColumns.PAYLOAD, (String) null); |
| |
| mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); |
| } |
| |
| private PreIndexData getPreIndexData(SearchIndexableData fakeData) { |
| PreIndexData data = new PreIndexData(); |
| data.dataToUpdate.add(fakeData); |
| return data; |
| } |
| } |