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 com.android.server.rollback;
18 
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertFalse;
25 import static org.junit.Assert.assertTrue;
26 import static org.mockito.Mockito.spy;
27 import static org.mockito.Mockito.when;
28 
29 import android.content.Context;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.VersionedPackage;
33 import android.content.rollback.PackageRollbackInfo;
34 import android.content.rollback.RollbackInfo;
35 import android.content.rollback.RollbackManager;
36 import android.util.Log;
37 import android.util.Xml;
38 
39 import androidx.test.runner.AndroidJUnit4;
40 
41 import com.android.dx.mockito.inline.extended.ExtendedMockito;
42 import com.android.server.PackageWatchdog;
43 import com.android.server.SystemConfig;
44 
45 import org.junit.After;
46 import org.junit.Before;
47 import org.junit.Rule;
48 import org.junit.Test;
49 import org.junit.rules.TemporaryFolder;
50 import org.junit.runner.RunWith;
51 import org.mockito.Answers;
52 import org.mockito.Mock;
53 import org.mockito.MockitoSession;
54 import org.mockito.quality.Strictness;
55 import org.mockito.stubbing.Answer;
56 import org.xmlpull.v1.XmlPullParser;
57 
58 import java.io.BufferedWriter;
59 import java.io.File;
60 import java.io.FileWriter;
61 import java.io.IOException;
62 import java.util.List;
63 import java.util.Scanner;
64 
65 
66 @RunWith(AndroidJUnit4.class)
67 public class RollbackPackageHealthObserverTest {
68     @Mock
69     private Context mMockContext;
70     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
71     private PackageWatchdog mMockPackageWatchdog;
72     @Mock
73     RollbackManager mRollbackManager;
74     @Mock
75     RollbackInfo mRollbackInfo;
76     @Mock
77     PackageRollbackInfo mPackageRollbackInfo;
78     @Mock
79     PackageManager mMockPackageManager;
80 
81     private MockitoSession mSession;
82     private static final String APP_A = "com.package.a";
83     private static final String APP_B = "com.package.b";
84     private static final long VERSION_CODE = 1L;
85     private static final String LOG_TAG = "RollbackPackageHealthObserverTest";
86 
87     private SystemConfig mSysConfig;
88 
89     @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
90 
91     @Before
setup()92     public void setup() {
93         mSysConfig = new SystemConfigTestClass();
94 
95         mSession = ExtendedMockito.mockitoSession()
96                 .initMocks(this)
97                 .strictness(Strictness.LENIENT)
98                 .spyStatic(PackageWatchdog.class)
99                 .startMocking();
100 
101         // Mock PackageWatchdog
102         doAnswer((Answer<PackageWatchdog>) invocationOnMock -> mMockPackageWatchdog)
103                 .when(() -> PackageWatchdog.getInstance(mMockContext));
104 
105     }
106 
107     @After
tearDown()108     public void tearDown() throws Exception {
109         mSession.finishMocking();
110     }
111 
112     /**
113      * Subclass of SystemConfig without running the constructor.
114      */
115     private class SystemConfigTestClass extends SystemConfig {
SystemConfigTestClass()116         SystemConfigTestClass() {
117             super(false);
118         }
119     }
120 
121     @Test
testHealthCheckLevels()122     public void testHealthCheckLevels() {
123         RollbackPackageHealthObserver observer =
124                 spy(new RollbackPackageHealthObserver(mMockContext));
125         VersionedPackage testFailedPackage = new VersionedPackage(APP_A, VERSION_CODE);
126         VersionedPackage secondFailedPackage = new VersionedPackage(APP_B, VERSION_CODE);
127 
128         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
129 
130         // Crashes with no rollbacks available
131         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
132                 observer.onHealthCheckFailed(null,
133                         PackageWatchdog.FAILURE_REASON_NATIVE_CRASH, 1));
134         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
135                 observer.onHealthCheckFailed(null,
136                         PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
137 
138         // Make the rollbacks available
139         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(mRollbackInfo));
140         when(mRollbackInfo.getPackages()).thenReturn(List.of(mPackageRollbackInfo));
141         when(mPackageRollbackInfo.getVersionRolledBackFrom()).thenReturn(testFailedPackage);
142 
143         // native crash
144         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
145                 observer.onHealthCheckFailed(null,
146                         PackageWatchdog.FAILURE_REASON_NATIVE_CRASH, 1));
147         // non-native crash for the package
148         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
149                 observer.onHealthCheckFailed(testFailedPackage,
150                         PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
151         // non-native crash for a different package
152         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
153                 observer.onHealthCheckFailed(secondFailedPackage,
154                         PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
155         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
156                 observer.onHealthCheckFailed(secondFailedPackage,
157                         PackageWatchdog.FAILURE_REASON_APP_CRASH, 2));
158         // Subsequent crashes when rollbacks have completed
159         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of());
160         assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
161                 observer.onHealthCheckFailed(testFailedPackage,
162                         PackageWatchdog.FAILURE_REASON_APP_CRASH, 3));
163     }
164 
165     @Test
testIsPersistent()166     public void testIsPersistent() {
167         RollbackPackageHealthObserver observer =
168                 spy(new RollbackPackageHealthObserver(mMockContext));
169         assertTrue(observer.isPersistent());
170     }
171 
172     @Test
testMayObservePackage_withoutAnyRollback()173     public void testMayObservePackage_withoutAnyRollback() {
174         RollbackPackageHealthObserver observer =
175                 spy(new RollbackPackageHealthObserver(mMockContext));
176         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
177         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of());
178         assertFalse(observer.mayObservePackage(APP_A));
179     }
180 
181     @Test
testMayObservePackage_forPersistentApp()182     public void testMayObservePackage_forPersistentApp()
183             throws PackageManager.NameNotFoundException {
184         RollbackPackageHealthObserver observer =
185                 spy(new RollbackPackageHealthObserver(mMockContext));
186         ApplicationInfo info = new ApplicationInfo();
187         info.flags = ApplicationInfo.FLAG_PERSISTENT | ApplicationInfo.FLAG_SYSTEM;
188         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
189         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(mRollbackInfo));
190         when(mRollbackInfo.getPackages()).thenReturn(List.of(mPackageRollbackInfo));
191         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
192         when(mMockPackageManager.getApplicationInfo(APP_A, 0)).thenReturn(info);
193         assertTrue(observer.mayObservePackage(APP_A));
194     }
195 
196     @Test
testMayObservePackage_forNonPersistentApp()197     public void testMayObservePackage_forNonPersistentApp()
198             throws PackageManager.NameNotFoundException {
199         RollbackPackageHealthObserver observer =
200                 spy(new RollbackPackageHealthObserver(mMockContext));
201         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
202         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(mRollbackInfo));
203         when(mRollbackInfo.getPackages()).thenReturn(List.of(mPackageRollbackInfo));
204         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
205         when(mMockPackageManager.getApplicationInfo(APP_A, 0))
206                 .thenThrow(new PackageManager.NameNotFoundException());
207         assertFalse(observer.mayObservePackage(APP_A));
208     }
209 
210     /**
211      * Test that isAutomaticRollbackDenied works correctly when packages that are not
212      * denied are sent.
213      */
214     @Test
isRollbackAllowedTest_false()215     public void isRollbackAllowedTest_false() throws IOException {
216         final String contents =
217                 "<config>\n"
218                 + "    <automatic-rollback-denylisted-app package=\"com.android.vending\" />\n"
219                 + "</config>";
220         final File folder = createTempSubfolder("folder");
221         createTempFile(folder, "automatic-rollback-denylisted-app.xml", contents);
222 
223         readPermissions(folder, /* Grant all permission flags */ ~0);
224 
225         assertThat(RollbackPackageHealthObserver.isAutomaticRollbackDenied(mSysConfig,
226                 new VersionedPackage("com.test.package", 1))).isEqualTo(false);
227     }
228 
229     /**
230      * Test that isAutomaticRollbackDenied works correctly when packages that are
231      * denied are sent.
232      */
233     @Test
isRollbackAllowedTest_true()234     public void isRollbackAllowedTest_true() throws IOException {
235         final String contents =
236                 "<config>\n"
237                 + "    <automatic-rollback-denylisted-app package=\"com.android.vending\" />\n"
238                 + "</config>";
239         final File folder = createTempSubfolder("folder");
240         createTempFile(folder, "automatic-rollback-denylisted-app.xml", contents);
241 
242         readPermissions(folder, /* Grant all permission flags */ ~0);
243 
244         assertThat(RollbackPackageHealthObserver.isAutomaticRollbackDenied(mSysConfig,
245                 new VersionedPackage("com.android.vending", 1))).isEqualTo(true);
246     }
247 
248     /**
249      * Test that isAutomaticRollbackDenied works correctly when no config is present
250      */
251     @Test
isRollbackAllowedTest_noConfig()252     public void isRollbackAllowedTest_noConfig() throws IOException {
253         final File folder = createTempSubfolder("folder");
254 
255         readPermissions(folder, /* Grant all permission flags */ ~0);
256 
257         assertThat(RollbackPackageHealthObserver.isAutomaticRollbackDenied(mSysConfig,
258                 new VersionedPackage("com.android.vending", 1))).isEqualTo(false);
259     }
260 
261     /**
262      * Creates folderName/fileName in the mTemporaryFolder and fills it with the contents.
263      *
264      * @param folder   pre-existing subdirectory of mTemporaryFolder to put the file
265      * @param fileName name of the file (e.g. filename.xml) to create
266      * @param contents contents to write to the file
267      * @return the newly created file
268      */
createTempFile(File folder, String fileName, String contents)269     private File createTempFile(File folder, String fileName, String contents)
270             throws IOException {
271         File file = new File(folder, fileName);
272         BufferedWriter bw = new BufferedWriter(new FileWriter(file));
273         bw.write(contents);
274         bw.close();
275 
276         // Print to logcat for test debugging.
277         Log.d(LOG_TAG, "Contents of file " + file.getAbsolutePath());
278         Scanner input = new Scanner(file);
279         while (input.hasNextLine()) {
280             Log.d(LOG_TAG, input.nextLine());
281         }
282 
283         return file;
284     }
285 
readPermissions(File libraryDir, int permissionFlag)286     private void readPermissions(File libraryDir, int permissionFlag) {
287         final XmlPullParser parser = Xml.newPullParser();
288         mSysConfig.readPermissions(parser, libraryDir, permissionFlag);
289     }
290 
291     /**
292      * Creates folderName/fileName in the mTemporaryFolder and fills it with the contents.
293      *
294      * @param folderName subdirectory of mTemporaryFolder to put the file, creating if needed
295      * @return the folder
296      */
createTempSubfolder(String folderName)297     private File createTempSubfolder(String folderName)
298             throws IOException {
299         File folder = new File(mTemporaryFolder.getRoot(), folderName);
300         folder.mkdirs();
301         return folder;
302     }
303 }
304