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