1 /* 2 * Copyright (C) 2023 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 package android.ext.services.common; 18 19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; 20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; 21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; 22 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 23 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow; 24 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; 25 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; 26 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; 27 28 import static com.google.common.truth.Truth.assertThat; 29 30 import static org.mockito.ArgumentMatchers.eq; 31 32 import android.content.Context; 33 import android.content.pm.PackageManager; 34 import android.database.sqlite.SQLiteDatabase; 35 36 import androidx.test.core.app.ApplicationProvider; 37 38 import com.android.dx.mockito.inline.extended.ExtendedMockito; 39 40 import com.google.common.truth.Expect; 41 42 import org.junit.After; 43 import org.junit.Before; 44 import org.junit.Rule; 45 import org.junit.Test; 46 import org.mockito.Mock; 47 import org.mockito.MockitoSession; 48 import org.mockito.Spy; 49 import org.mockito.quality.Strictness; 50 51 import java.io.File; 52 import java.io.FileWriter; 53 import java.io.IOException; 54 import java.nio.file.FileVisitResult; 55 import java.nio.file.Files; 56 import java.nio.file.Path; 57 import java.nio.file.SimpleFileVisitor; 58 import java.nio.file.attribute.BasicFileAttributes; 59 import java.util.Arrays; 60 import java.util.List; 61 62 public final class AdServicesFilesCleanupBootCompleteReceiverTest { 63 private static final String ADSERVICES_FILE_NAME = "adservices_file"; 64 private static final String NON_ADSERVICES_FILE_NAME = "some_other_file"; 65 private static final String NON_ADSERVICES_FILE_WITH_PREFIX_IN_NAME = 66 "some_file_with_adservices_in_name"; 67 private static final String ADSERVICES_FILE_NAME_MIXED_CASE = "AdServicesFileMixedCase.txt"; 68 private static final String NON_ADSERVICE_FILE_NAME_2 = "adservice_but_no_s.txt"; 69 70 // Update this list with the previous name every time the receiver is renamed 71 private static final List<String> PREVIOUSLY_USED_CLASS_NAMES = List.of(); 72 73 // TODO(b/297207132): Replace with AdServicesExtendedMockitoRule 74 private MockitoSession mMockitoSession; 75 76 @Spy 77 private final Context mContext = ApplicationProvider.getApplicationContext(); 78 79 @Spy 80 private AdServicesFilesCleanupBootCompleteReceiver mReceiver; 81 82 @Mock 83 private PackageManager mPackageManager; 84 85 @Rule 86 public final Expect expect = Expect.create(); 87 88 @Before setup()89 public void setup() { 90 mMockitoSession = ExtendedMockito.mockitoSession() 91 .initMocks(this) 92 .strictness(Strictness.WARN) 93 .startMocking(); 94 95 doReturn(mPackageManager).when(mContext).getPackageManager(); 96 doNothing().when(mReceiver).scheduleAppsearchDeleteJob(any()); 97 } 98 99 @After tearDown()100 public void tearDown() { 101 if (mMockitoSession != null) { 102 mMockitoSession.finishMocking(); 103 } 104 } 105 106 @Test testReceiverDoesNotReuseClassNames()107 public void testReceiverDoesNotReuseClassNames() { 108 assertThat(PREVIOUSLY_USED_CLASS_NAMES) 109 .doesNotContain(AdServicesFilesCleanupBootCompleteReceiver.class.getName()); 110 } 111 112 @Test testReceiverSkipsDeletionIfDisabled()113 public void testReceiverSkipsDeletionIfDisabled() { 114 mockReceiverEnabled(false); 115 116 mReceiver.onReceive(mContext, /* intent= */ null); 117 118 verify(mContext, never()).getDataDir(); 119 verify(mContext, never()).getPackageManager(); 120 verify(mReceiver, never()).scheduleAppsearchDeleteJob(any()); 121 } 122 123 @Test testReceiverDisablesItselfIfDeleteSuccessful()124 public void testReceiverDisablesItselfIfDeleteSuccessful() { 125 mockReceiverEnabled(true); 126 doNothing().when(mPackageManager).setComponentEnabledSetting(any(), anyInt(), anyInt()); 127 doReturn(true).when(mReceiver).deleteAdServicesFiles(any()); 128 129 mReceiver.onReceive(mContext, /* intent= */ null); 130 verify(mReceiver).scheduleAppsearchDeleteJob(any()); 131 verifyDisableComponentCalled(); 132 } 133 134 @Test testReceiverDisablesItselfIfDeleteUnsuccessful()135 public void testReceiverDisablesItselfIfDeleteUnsuccessful() { 136 mockReceiverEnabled(true); 137 doReturn(false).when(mReceiver).deleteAdServicesFiles(any()); 138 139 mReceiver.onReceive(mContext, /* intent= */ null); 140 verify(mReceiver).scheduleAppsearchDeleteJob(any()); 141 verifyDisableComponentCalled(); 142 } 143 144 @Test testReceiverDeletesAdServicesFiles()145 public void testReceiverDeletesAdServicesFiles() throws Exception { 146 List<String> adServicesNames = List.of(ADSERVICES_FILE_NAME, 147 ADSERVICES_FILE_NAME_MIXED_CASE); 148 List<String> nonAdServicesNames = List.of(NON_ADSERVICES_FILE_NAME, 149 NON_ADSERVICES_FILE_WITH_PREFIX_IN_NAME, NON_ADSERVICE_FILE_NAME_2); 150 151 try { 152 createFiles(adServicesNames); 153 createFiles(nonAdServicesNames); 154 createDatabases(adServicesNames); 155 createDatabases(nonAdServicesNames); 156 157 mReceiver.deleteAdServicesFiles(mContext.getDataDir()); 158 159 // Check if the appropriate files were deleted 160 String[] remainingFiles = mContext.getFilesDir().list(); 161 List<String> remainingFilesList = Arrays.asList(remainingFiles); 162 expect.that(remainingFilesList).containsNoneIn(adServicesNames); 163 expect.that(remainingFilesList).containsAtLeastElementsIn(nonAdServicesNames); 164 expectDatabasesExist(nonAdServicesNames); 165 expectDatabasesDoNotExist(adServicesNames); 166 } finally { 167 deleteFiles(adServicesNames); 168 deleteFiles(nonAdServicesNames); 169 deleteDatabases(adServicesNames); 170 deleteDatabases(nonAdServicesNames); 171 } 172 } 173 174 @Test testReceiverDeletesAdServicesDirectories()175 public void testReceiverDeletesAdServicesDirectories() throws Exception { 176 String dataRoot = "data_root"; 177 Path root = mContext.getFilesDir().toPath(); 178 179 try { 180 File file1 = createFile(root, dataRoot, "level_1.txt"); // Preserved 181 File file2 = createFile(root, dataRoot, "adservices_level_1.txt"); // Deleted 182 File file3 = createFile(root, dataRoot + "/non_adservices", 183 "level_2.txt"); // Preserved 184 File file4 = createFile(root, dataRoot + "/non_adservices", 185 "adservices_level_2.txt"); // Deleted 186 File file5 = createFile(root, dataRoot + "/non_adservices/adservices_nested", 187 "level_3.txt"); // Deleted 188 File file6 = createFile(root, dataRoot + "/non_adservices/adservices_nested", 189 "adservices.level_3.txt"); // Deleted 190 File file7 = createFile(root, dataRoot + "/non_adservices", 191 "AdServices_level_2.txt"); // Deleted 192 File file8 = createFile(root, dataRoot + "/adservices-data", 193 "level_2.txt"); // Deleted 194 File file9 = createFile(root, dataRoot + "/adservices-data/nested", 195 "level_3.txt"); // Deleted 196 File file10 = createFile(root, dataRoot + "/AdServices-data/nested", 197 "level_3_1.txt"); 198 199 mReceiver.deleteAdServicesFiles(mContext.getDataDir()); 200 201 expectFilesExist(file1, file3); 202 expectFilesDoNotExist(file2, file4, file5, file6, file7, file8, file9, file10); 203 } finally { 204 deletePathRecursively(root.resolve(dataRoot)); 205 } 206 } 207 208 @Test testReceiverHandlesSecurityException()209 public void testReceiverHandlesSecurityException() { 210 // Simulate a directory with three files, and the first one throws an exception on delete 211 File file1 = mock(File.class); 212 doReturn(ADSERVICES_FILE_NAME).when(file1).getName(); 213 doThrow(SecurityException.class).when(file1).delete(); 214 215 File file2 = mock(File.class); 216 doReturn(ADSERVICES_FILE_NAME_MIXED_CASE).when(file2).getName(); 217 218 File file3 = mock(File.class); 219 doReturn(NON_ADSERVICES_FILE_NAME).when(file3).getName(); 220 221 File dir = mock(File.class); 222 doReturn(true).when(dir).isDirectory(); 223 doReturn(new File[] { file1, file2, file3 }).when(dir).listFiles(); 224 225 // Execute the receiver 226 mReceiver.deleteAdServicesFiles(dir); 227 228 // Verify that deletion of both file1 and file2 was attempted, in spite of the exception 229 verify(file1).delete(); 230 verify(file2).delete(); 231 verify(file3, never()).delete(); 232 } 233 234 @Test testDeleteAdServicesFiles_invalidInput()235 public void testDeleteAdServicesFiles_invalidInput() { 236 // Null input 237 assertThat(mReceiver.deleteAdServicesFiles(null)).isTrue(); 238 239 // Not a directory 240 File file = mock(File.class); 241 assertThat(mReceiver.deleteAdServicesFiles(file)).isTrue(); 242 verify(file, never()).listFiles(); 243 244 // Throws an exception 245 File file2 = mock(File.class); 246 doThrow(SecurityException.class).when(file2).isDirectory(); 247 assertThat(mReceiver.deleteAdServicesFiles(file2)).isFalse(); 248 verify(file2, never()).listFiles(); 249 } 250 mockReceiverEnabled(boolean value)251 private void mockReceiverEnabled(boolean value) { 252 doReturn(value).when(mReceiver).isReceiverEnabled(); 253 } 254 verifyDisableComponentCalled()255 private void verifyDisableComponentCalled() { 256 verify(mPackageManager).setComponentEnabledSetting(any(), 257 eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), eq(0)); 258 } 259 expectFilesExist(File... files)260 private void expectFilesExist(File... files) { 261 for (File file: files) { 262 expect.withMessage("%s exists", file.getPath()).that(file.exists()).isTrue(); 263 } 264 } 265 expectFilesDoNotExist(File... files)266 private void expectFilesDoNotExist(File... files) { 267 for (File file: files) { 268 expect.withMessage("%s exists", file.getPath()).that(file.exists()).isFalse(); 269 } 270 } 271 expectDatabasesExist(List<String> databaseNames)272 private void expectDatabasesExist(List<String> databaseNames) { 273 for (String db: databaseNames) { 274 expect.withMessage("%s exists", db) 275 .that(mContext.getDatabasePath(db).exists()) 276 .isTrue(); 277 } 278 } 279 expectDatabasesDoNotExist(List<String> databaseNames)280 private void expectDatabasesDoNotExist(List<String> databaseNames) { 281 for (String db: databaseNames) { 282 expect.withMessage("%s exists", db) 283 .that(mContext.getDatabasePath(db).exists()) 284 .isFalse(); 285 } 286 } 287 createFiles(List<String> names)288 private void createFiles(List<String> names) throws Exception { 289 File dir = mContext.getFilesDir(); 290 for (String name : names) { 291 createFile(name, dir); 292 } 293 } 294 createDatabases(List<String> names)295 private void createDatabases(List<String> names) { 296 for (String name : names) { 297 try (SQLiteDatabase unused = mContext.openOrCreateDatabase(name, 0, null)) { 298 // Intentionally do nothing. 299 } 300 } 301 } 302 deleteFiles(List<String> names)303 private void deleteFiles(List<String> names) { 304 for (String name : names) { 305 File file = new File(mContext.getFilesDir(), name); 306 if (file.exists()) { 307 file.delete(); 308 } 309 } 310 } 311 deleteDatabases(List<String> names)312 private void deleteDatabases(List<String> names) { 313 for (String name : names) { 314 mContext.deleteDatabase(name); 315 } 316 } 317 createFile(String name, File directory)318 private File createFile(String name, File directory) throws Exception { 319 File file = new File(directory, name); 320 try (FileWriter writer = new FileWriter(file)) { 321 writer.append("test data"); 322 writer.flush(); 323 } 324 325 return file; 326 } 327 createFile(Path root, String path, String fileName)328 private File createFile(Path root, String path, String fileName) throws Exception { 329 Path dir = root.resolve(path); 330 Files.createDirectories(dir); 331 return createFile(fileName, dir.toFile()); 332 } 333 deletePathRecursively(Path path)334 private void deletePathRecursively(Path path) throws Exception { 335 Files.walkFileTree(path, new SimpleFileVisitor<>() { 336 @Override 337 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 338 throws IOException { 339 Files.delete(file); 340 return FileVisitResult.CONTINUE; 341 } 342 343 @Override 344 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 345 throws IOException { 346 Files.delete(dir); 347 return FileVisitResult.CONTINUE; 348 } 349 }); 350 } 351 } 352