1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.settings.search; 19 20 import static com.google.common.truth.Truth.assertThat; 21 import static org.mockito.Matchers.any; 22 import static org.mockito.Matchers.anyBoolean; 23 import static org.mockito.Matchers.anyInt; 24 import static org.mockito.Matchers.anyList; 25 import static org.mockito.Matchers.anyString; 26 import static org.mockito.Mockito.doReturn; 27 import static org.mockito.Mockito.spy; 28 import static org.mockito.Mockito.verify; 29 30 import android.content.ContentValues; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ResolveInfo; 35 import android.database.Cursor; 36 import android.database.sqlite.SQLiteDatabase; 37 import android.os.Build; 38 import android.provider.SearchIndexableData; 39 import android.util.ArrayMap; 40 41 import com.android.settings.search.indexing.PreIndexData; 42 import com.android.settings.testutils.DatabaseTestUtils; 43 import com.android.settings.testutils.FakeFeatureFactory; 44 import com.android.settings.testutils.SettingsRobolectricTestRunner; 45 import com.android.settings.testutils.shadow.ShadowRunnableAsyncTask; 46 47 import org.junit.After; 48 import org.junit.Before; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 import org.mockito.Mock; 52 import org.mockito.MockitoAnnotations; 53 import org.robolectric.RuntimeEnvironment; 54 import org.robolectric.annotation.Config; 55 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.HashSet; 59 import java.util.List; 60 import java.util.Locale; 61 import java.util.Map; 62 import java.util.Set; 63 64 @RunWith(SettingsRobolectricTestRunner.class) 65 @Config(shadows = ShadowRunnableAsyncTask.class) 66 public class DatabaseIndexingManagerTest { 67 68 private final String localeStr = "en_US"; 69 70 private final int rank = 8; 71 private final String title = "title\u2011title"; 72 private final String updatedTitle = "title-title"; 73 private final String normalizedTitle = "titletitle"; 74 private final String summaryOn = "summary\u2011on"; 75 private final String updatedSummaryOn = "summary-on"; 76 private final String normalizedSummaryOn = "summaryon"; 77 private final String summaryOff = "summary\u2011off"; 78 private final String entries = "entries"; 79 private final String keywords = "keywords, keywordss, keywordsss"; 80 private final String spaceDelimittedKeywords = "keywords keywordss keywordsss"; 81 private final String screenTitle = "screen title"; 82 private final String className = "class name"; 83 private final int iconResId = 0xff; 84 private final String action = "action"; 85 private final String targetPackage = "target package"; 86 private final String targetClass = "target class"; 87 private final String packageName = "package name"; 88 private final String key = "key"; 89 private final int userId = -1; 90 private final boolean enabled = true; 91 92 private final String TITLE_ONE = "title one"; 93 private final String TITLE_TWO = "title two"; 94 private final String KEY_ONE = "key one"; 95 private final String KEY_TWO = "key two"; 96 97 private Context mContext; 98 99 private DatabaseIndexingManager mManager; 100 private SQLiteDatabase mDb; 101 102 private final List<ResolveInfo> FAKE_PROVIDER_LIST = new ArrayList<>(); 103 104 @Mock 105 private PackageManager mPackageManager; 106 107 @Before 108 public void setUp() { 109 MockitoAnnotations.initMocks(this); 110 mContext = spy(RuntimeEnvironment.application); 111 mManager = spy(new DatabaseIndexingManager(mContext)); 112 mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); 113 114 doReturn(mPackageManager).when(mContext).getPackageManager(); 115 doReturn(FAKE_PROVIDER_LIST).when(mPackageManager) 116 .queryIntentContentProviders(any(Intent.class), anyInt()); 117 FakeFeatureFactory.setupForTest(); 118 } 119 120 @After 121 public void cleanUp() { 122 DatabaseTestUtils.clearDb(mContext); 123 } 124 125 @Test 126 public void testDatabaseSchema() { 127 Cursor dbCursor = mDb.query("prefs_index", null, null, null, null, null, null); 128 List<String> columnNames = new ArrayList<>(Arrays.asList(dbCursor.getColumnNames())); 129 // Note that docid is not included. 130 List<String> expColumnNames = Arrays.asList( 131 "locale", 132 "data_rank", 133 "data_title", 134 "data_title_normalized", 135 "data_summary_on", 136 "data_summary_on_normalized", 137 "data_summary_off", 138 "data_summary_off_normalized", 139 "data_entries", 140 "data_keywords", 141 "class_name", 142 "screen_title", 143 "intent_action", 144 "intent_target_package", 145 "intent_target_class", 146 "icon", 147 "enabled", 148 "data_key_reference", 149 "user_id", 150 "payload_type", 151 "payload" 152 ); 153 // Prevent database schema regressions 154 assertThat(columnNames).containsAllIn(expColumnNames); 155 } 156 157 // Test new public indexing flow 158 159 @Test 160 public void testPerformIndexing_fullIndex_getsDataFromProviders() { 161 SearchIndexableRaw rawData = getFakeRaw(); 162 PreIndexData data = getPreIndexData(rawData); 163 doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); 164 doReturn(true).when(mManager) 165 .isFullIndex(any(Context.class), anyString(), anyString(), anyString()); 166 167 mManager.performIndexing(); 168 169 verify(mManager).updateDatabase(data, true /* isFullIndex */); 170 } 171 172 @Test 173 public void testPerformIndexing_fullIndex_databaseDropped() { 174 // Initialize the Manager and force rebuild 175 DatabaseIndexingManager manager = 176 spy(new DatabaseIndexingManager(mContext)); 177 doReturn(false).when(mManager) 178 .isFullIndex(any(Context.class), anyString(), anyString(), anyString()); 179 180 // Insert data point which will be dropped 181 insertSpecialCase("Ceci n'est pas un pipe", true, "oui oui mon ami"); 182 183 manager.performIndexing(); 184 185 // Assert that the Old Title is no longer in the database, since it was dropped 186 final Cursor oldCursor = mDb.rawQuery("SELECT * FROM prefs_index", null); 187 188 assertThat(oldCursor.getCount()).isEqualTo(0); 189 } 190 191 @Test 192 public void testPerformIndexing_isfullIndex() { 193 SearchIndexableRaw rawData = getFakeRaw(); 194 PreIndexData data = getPreIndexData(rawData); 195 doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); 196 doReturn(true).when(mManager) 197 .isFullIndex(any(Context.class), anyString(), anyString(), anyString()); 198 199 mManager.performIndexing(); 200 201 verify(mManager).updateDatabase(data, true /* isFullIndex */); 202 } 203 204 @Test 205 public void testPerformIndexing_onOta_buildNumberIsCached() { 206 mManager.performIndexing(); 207 208 assertThat(IndexDatabaseHelper.isBuildIndexed(mContext, Build.FINGERPRINT)).isTrue(); 209 } 210 211 @Test 212 public void testLocaleUpdated_afterIndexing_localeNotAdded() { 213 PreIndexData emptydata = new PreIndexData(); 214 mManager.updateDatabase(emptydata, true /* isFullIndex */); 215 216 assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isFalse(); 217 } 218 219 @Test 220 public void testLocaleUpdated_afterFullIndexing_localeAdded() { 221 mManager.performIndexing(); 222 223 assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isTrue(); 224 } 225 226 @Test 227 public void testUpdateDatabase_newEligibleData_addedToDatabase() { 228 // Test that addDataToDatabase is called when dataToUpdate is non-empty 229 PreIndexData indexData = new PreIndexData(); 230 indexData.dataToUpdate.add(getFakeRaw()); 231 mManager.updateDatabase(indexData, true /* isFullIndex */); 232 233 Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index", null); 234 cursor.moveToPosition(0); 235 236 // Locale 237 assertThat(cursor.getString(0)).isEqualTo(localeStr); 238 // Data Title 239 assertThat(cursor.getString(2)).isEqualTo(updatedTitle); 240 // Normalized Title 241 assertThat(cursor.getString(3)).isEqualTo(normalizedTitle); 242 // Summary On 243 assertThat(cursor.getString(4)).isEqualTo(updatedSummaryOn); 244 // Summary On Normalized 245 assertThat(cursor.getString(5)).isEqualTo(normalizedSummaryOn); 246 // Entries 247 assertThat(cursor.getString(8)).isEqualTo(entries); 248 // Keywords 249 assertThat(cursor.getString(9)).isEqualTo(spaceDelimittedKeywords); 250 // Screen Title 251 assertThat(cursor.getString(10)).isEqualTo(screenTitle); 252 // Class Name 253 assertThat(cursor.getString(11)).isEqualTo(className); 254 // Icon 255 assertThat(cursor.getInt(12)).isEqualTo(iconResId); 256 // Intent Action 257 assertThat(cursor.getString(13)).isEqualTo(action); 258 // Target Package 259 assertThat(cursor.getString(14)).isEqualTo(targetPackage); 260 // Target Class 261 assertThat(cursor.getString(15)).isEqualTo(targetClass); 262 // Enabled 263 assertThat(cursor.getInt(16) == 1).isEqualTo(enabled); 264 // Data ref key 265 assertThat(cursor.getString(17)).isNotNull(); 266 // User Id 267 assertThat(cursor.getInt(18)).isEqualTo(userId); 268 // Payload Type - default is 0 269 assertThat(cursor.getInt(19)).isEqualTo(0); 270 // Payload 271 byte[] payload = cursor.getBlob(20); 272 ResultPayload unmarshalledPayload = ResultPayloadUtils.unmarshall(payload, 273 ResultPayload.CREATOR); 274 assertThat(unmarshalledPayload).isInstanceOf(ResultPayload.class); 275 } 276 277 @Test 278 public void testUpdateDataInDatabase_enabledResultsAreNonIndexable_becomeDisabled() { 279 // Both results are enabled, and then TITLE_ONE gets disabled. 280 final boolean enabled = true; 281 insertSpecialCase(TITLE_ONE, enabled, KEY_ONE); 282 insertSpecialCase(TITLE_TWO, enabled, KEY_TWO); 283 Map<String, Set<String>> niks = new ArrayMap<>(); 284 Set<String> keys = new HashSet<>(); 285 keys.add(KEY_ONE); 286 niks.put(targetPackage, keys); 287 288 mManager.updateDataInDatabase(mDb, niks); 289 290 Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 0", null); 291 cursor.moveToPosition(0); 292 293 assertThat(cursor.getString(2)).isEqualTo(TITLE_ONE); 294 } 295 296 @Test 297 public void testUpdateDataInDatabase_disabledResultsAreIndexable_becomeEnabled() { 298 // Both results are initially disabled, and then TITLE_TWO gets enabled. 299 final boolean enabled = false; 300 insertSpecialCase(TITLE_ONE, enabled, KEY_ONE); 301 insertSpecialCase(TITLE_TWO, enabled, KEY_TWO); 302 Map<String, Set<String>> niks = new ArrayMap<>(); 303 Set<String> keys = new HashSet<>(); 304 keys.add(KEY_ONE); 305 niks.put(targetPackage, keys); 306 307 mManager.updateDataInDatabase(mDb, niks); 308 309 Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 1", null); 310 cursor.moveToPosition(0); 311 312 assertThat(cursor.getString(2)).isEqualTo(TITLE_TWO); 313 } 314 315 @Test 316 public void testEmptyNonIndexableKeys_emptyDataKeyResources_addedToDatabase() { 317 insertSpecialCase(TITLE_ONE, true /* enabled */, null /* dataReferenceKey */); 318 PreIndexData emptydata = new PreIndexData(); 319 mManager.updateDatabase(emptydata, false /* needsReindexing */); 320 321 Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 1", null); 322 cursor.moveToPosition(0); 323 assertThat(cursor.getCount()).isEqualTo(1); 324 assertThat(cursor.getString(2)).isEqualTo(TITLE_ONE); 325 } 326 327 // Util functions 328 329 private SearchIndexableRaw getFakeRaw() { 330 return getFakeRaw(localeStr); 331 } 332 333 private SearchIndexableRaw getFakeRaw(String localeStr) { 334 SearchIndexableRaw data = new SearchIndexableRaw(mContext); 335 data.locale = new Locale(localeStr); 336 data.rank = rank; 337 data.title = title; 338 data.summaryOn = summaryOn; 339 data.summaryOff = summaryOff; 340 data.entries = entries; 341 data.keywords = keywords; 342 data.screenTitle = screenTitle; 343 data.className = className; 344 data.packageName = packageName; 345 data.iconResId = iconResId; 346 data.intentAction = action; 347 data.intentTargetPackage = targetPackage; 348 data.intentTargetClass = targetClass; 349 data.key = key; 350 data.userId = userId; 351 data.enabled = enabled; 352 return data; 353 } 354 355 private void insertSpecialCase(String specialCase, boolean enabled, String key) { 356 ContentValues values = new ContentValues(); 357 values.put(IndexDatabaseHelper.IndexColumns.DOCID, specialCase.hashCode()); 358 values.put(IndexDatabaseHelper.IndexColumns.LOCALE, localeStr); 359 values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, 1); 360 values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase); 361 values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED, ""); 362 values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON, ""); 363 values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED, ""); 364 values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF, ""); 365 values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, ""); 366 values.put(IndexDatabaseHelper.IndexColumns.DATA_ENTRIES, ""); 367 values.put(IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS, ""); 368 values.put(IndexDatabaseHelper.IndexColumns.CLASS_NAME, ""); 369 values.put(IndexDatabaseHelper.IndexColumns.SCREEN_TITLE, "Moves"); 370 values.put(IndexDatabaseHelper.IndexColumns.INTENT_ACTION, ""); 371 values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE, targetPackage); 372 values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS, ""); 373 values.put(IndexDatabaseHelper.IndexColumns.ICON, ""); 374 values.put(IndexDatabaseHelper.IndexColumns.ENABLED, enabled); 375 values.put(IndexDatabaseHelper.IndexColumns.DATA_KEY_REF, key); 376 values.put(IndexDatabaseHelper.IndexColumns.USER_ID, 0); 377 values.put(IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE, 0); 378 values.put(IndexDatabaseHelper.IndexColumns.PAYLOAD, (String) null); 379 380 mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); 381 } 382 383 private PreIndexData getPreIndexData(SearchIndexableData fakeData) { 384 PreIndexData data = new PreIndexData(); 385 data.dataToUpdate.add(fakeData); 386 return data; 387 } 388 } 389