1 /*
2  * Copyright 2020 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 package com.android.server.blob;
17 
18 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
22 import static com.android.server.blob.BlobStoreConfig.DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.mockito.ArgumentMatchers.anyBoolean;
27 import static org.mockito.ArgumentMatchers.anyLong;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.verify;
30 
31 import android.app.blob.BlobHandle;
32 import android.content.Context;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.Message;
36 import android.os.UserHandle;
37 import android.platform.test.annotations.Presubmit;
38 import android.util.LongSparseArray;
39 
40 import androidx.test.InstrumentationRegistry;
41 import androidx.test.filters.SmallTest;
42 import androidx.test.runner.AndroidJUnit4;
43 
44 import com.android.server.blob.BlobStoreManagerService.Injector;
45 
46 import org.junit.After;
47 import org.junit.Before;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 import org.mockito.Mock;
51 import org.mockito.MockitoSession;
52 import org.mockito.quality.Strictness;
53 
54 import java.io.File;
55 
56 @RunWith(AndroidJUnit4.class)
57 @SmallTest
58 @Presubmit
59 public class BlobStoreManagerServiceTest {
60     private Context mContext;
61     private Handler mHandler;
62     private BlobStoreManagerService mService;
63 
64     private MockitoSession mMockitoSession;
65 
66     @Mock
67     private File mBlobsDir;
68 
69     private LongSparseArray<BlobStoreSession> mUserSessions;
70 
71     private static final String TEST_PKG1 = "com.example1";
72     private static final String TEST_PKG2 = "com.example2";
73     private static final String TEST_PKG3 = "com.example3";
74 
75     private static final int TEST_UID1 = 10001;
76     private static final int TEST_UID2 = 10002;
77     private static final int TEST_UID3 = 10003;
78 
79     @Before
setUp()80     public void setUp() {
81         // Share classloader to allow package private access.
82         System.setProperty("dexmaker.share_classloader", "true");
83 
84         mMockitoSession = mockitoSession()
85                 .initMocks(this)
86                 .strictness(Strictness.LENIENT)
87                 .mockStatic(BlobStoreConfig.class)
88                 .startMocking();
89 
90         doReturn(mBlobsDir).when(() -> BlobStoreConfig.getBlobsDir());
91         doReturn(true).when(mBlobsDir).exists();
92         doReturn(new File[0]).when(mBlobsDir).listFiles();
93         doReturn(true).when(() -> BlobStoreConfig.hasLeaseWaitTimeElapsed(anyLong()));
94         doCallRealMethod().when(() -> BlobStoreConfig.hasSessionExpired(anyLong()));
95 
96         mContext = InstrumentationRegistry.getTargetContext();
97         mHandler = new TestHandler(Looper.getMainLooper());
98         mService = new BlobStoreManagerService(mContext, new TestInjector());
99         mUserSessions = new LongSparseArray<>();
100 
101         mService.addUserSessionsForTest(mUserSessions, UserHandle.myUserId());
102     }
103 
104     @After
tearDown()105     public void tearDown() {
106         if (mMockitoSession != null) {
107             mMockitoSession.finishMocking();
108         }
109     }
110 
111     @Test
testHandlePackageRemoved()112     public void testHandlePackageRemoved() throws Exception {
113         // Setup sessions
114         final File sessionFile1 = mock(File.class);
115         final long sessionId1 = 11;
116         final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
117                 sessionId1, sessionFile1);
118         mUserSessions.append(sessionId1, session1);
119 
120         final File sessionFile2 = mock(File.class);
121         final long sessionId2 = 25;
122         final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
123                 sessionId2, sessionFile2);
124         mUserSessions.append(sessionId2, session2);
125 
126         final File sessionFile3 = mock(File.class);
127         final long sessionId3 = 37;
128         final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
129                 sessionId3, sessionFile3);
130         mUserSessions.append(sessionId3, session3);
131 
132         final File sessionFile4 = mock(File.class);
133         final long sessionId4 = 48;
134         final BlobStoreSession session4 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
135                 sessionId4, sessionFile4);
136         mUserSessions.append(sessionId4, session4);
137 
138         // Setup blobs
139         final long blobId1 = 978;
140         final File blobFile1 = mock(File.class);
141         final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
142                 "label1", System.currentTimeMillis() + 10000, "tag1");
143         final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobId1, blobFile1,
144                 blobHandle1, true /* hasLeases */);
145         doReturn(true).when(blobMetadata1).isACommitter(TEST_PKG1, TEST_UID1);
146         addBlob(blobHandle1, blobMetadata1);
147 
148         final long blobId2 = 347;
149         final File blobFile2 = mock(File.class);
150         final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
151                 "label2", System.currentTimeMillis() + 20000, "tag2");
152         final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobId2, blobFile2,
153                 blobHandle2, false /* hasLeases */);
154         doReturn(false).when(blobMetadata2).isACommitter(TEST_PKG1, TEST_UID1);
155         addBlob(blobHandle2, blobMetadata2);
156 
157         final long blobId3 = 49875;
158         final File blobFile3 = mock(File.class);
159         final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
160                 "label3", System.currentTimeMillis() - 1000, "tag3");
161         final BlobMetadata blobMetadata3 = createBlobMetadataMock(blobId3, blobFile3,
162                 blobHandle3, true /* hasLeases */);
163         doReturn(true).when(blobMetadata3).isACommitter(TEST_PKG1, TEST_UID1);
164         addBlob(blobHandle3, blobMetadata3);
165 
166         mService.addActiveIdsForTest(sessionId1, sessionId2, sessionId3, sessionId4,
167                 blobId1, blobId2, blobId3);
168 
169         // Invoke test method
170         mService.handlePackageRemoved(TEST_PKG1, TEST_UID1);
171 
172         // Verify sessions are removed
173         verify(session1).destroy();
174         verify(session2, never()).destroy();
175         verify(session3, never()).destroy();
176         verify(session4).destroy();
177 
178         assertThat(mUserSessions.size()).isEqualTo(2);
179         assertThat(mUserSessions.get(sessionId1)).isNull();
180         assertThat(mUserSessions.get(sessionId2)).isNotNull();
181         assertThat(mUserSessions.get(sessionId3)).isNotNull();
182         assertThat(mUserSessions.get(sessionId4)).isNull();
183 
184         // Verify blobs are removed
185         verify(blobMetadata1).removeCommitter(TEST_PKG1, TEST_UID1);
186         verify(blobMetadata1).removeLeasee(TEST_PKG1, TEST_UID1);
187         verify(blobMetadata2, never()).removeCommitter(TEST_PKG1, TEST_UID1);
188         verify(blobMetadata2).removeLeasee(TEST_PKG1, TEST_UID1);
189         verify(blobMetadata3).removeCommitter(TEST_PKG1, TEST_UID1);
190         verify(blobMetadata3).removeLeasee(TEST_PKG1, TEST_UID1);
191 
192         verify(blobMetadata1, never()).destroy();
193         verify(blobMetadata2).destroy();
194         verify(blobMetadata3).destroy();
195 
196         assertThat(mService.getBlobsCountForTest()).isEqualTo(1);
197         assertThat(mService.getBlobForTest(blobHandle1)).isNotNull();
198         assertThat(mService.getBlobForTest(blobHandle2)).isNull();
199         assertThat(mService.getBlobForTest(blobHandle3)).isNull();
200 
201         assertThat(mService.getActiveIdsForTest()).containsExactly(
202                 sessionId2, sessionId3, blobId1);
203         assertThat(mService.getKnownIdsForTest()).containsExactly(
204                 sessionId1, sessionId2, sessionId3, sessionId4, blobId1, blobId2, blobId3);
205     }
206 
207     @Test
testHandleIdleMaintenance_deleteUnknownBlobs()208     public void testHandleIdleMaintenance_deleteUnknownBlobs() throws Exception {
209         // Setup blob files
210         final long testId1 = 286;
211         final File file1 = mock(File.class);
212         doReturn(String.valueOf(testId1)).when(file1).getName();
213         final long testId2 = 349;
214         final File file2 = mock(File.class);
215         doReturn(String.valueOf(testId2)).when(file2).getName();
216         final long testId3 = 7355;
217         final File file3 = mock(File.class);
218         doReturn(String.valueOf(testId3)).when(file3).getName();
219 
220         doReturn(new File[] {file1, file2, file3}).when(mBlobsDir).listFiles();
221         mService.addActiveIdsForTest(testId1, testId3);
222 
223         // Invoke test method
224         mService.handleIdleMaintenanceLocked();
225 
226         // Verify unknown blobs are deleted
227         verify(file1, never()).delete();
228         verify(file2).delete();
229         verify(file3, never()).delete();
230     }
231 
232     @Test
testHandleIdleMaintenance_deleteStaleSessions()233     public void testHandleIdleMaintenance_deleteStaleSessions() throws Exception {
234         // Setup sessions
235         final File sessionFile1 = mock(File.class);
236         doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MS + 1000)
237                 .when(sessionFile1).lastModified();
238         final long sessionId1 = 342;
239         final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
240                 "label1", System.currentTimeMillis() - 1000, "tag1");
241         final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
242                 sessionId1, sessionFile1, blobHandle1);
243         mUserSessions.append(sessionId1, session1);
244 
245         final File sessionFile2 = mock(File.class);
246         doReturn(System.currentTimeMillis() - 20000)
247                 .when(sessionFile2).lastModified();
248         final long sessionId2 = 4597;
249         final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
250                 "label2", System.currentTimeMillis() + 20000, "tag2");
251         final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
252                 sessionId2, sessionFile2, blobHandle2);
253         mUserSessions.append(sessionId2, session2);
254 
255         final File sessionFile3 = mock(File.class);
256         doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MS - 2000)
257                 .when(sessionFile3).lastModified();
258         final long sessionId3 = 9484;
259         final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
260                 "label3", System.currentTimeMillis() + 30000, "tag3");
261         final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
262                 sessionId3, sessionFile3, blobHandle3);
263         mUserSessions.append(sessionId3, session3);
264 
265         mService.addActiveIdsForTest(sessionId1, sessionId2, sessionId3);
266 
267         // Invoke test method
268         mService.handleIdleMaintenanceLocked();
269 
270         // Verify stale sessions are removed
271         verify(session1).destroy();
272         verify(session2, never()).destroy();
273         verify(session3).destroy();
274 
275         assertThat(mUserSessions.size()).isEqualTo(1);
276         assertThat(mUserSessions.get(sessionId2)).isNotNull();
277 
278         assertThat(mService.getActiveIdsForTest()).containsExactly(sessionId2);
279         assertThat(mService.getKnownIdsForTest()).containsExactly(
280                 sessionId1, sessionId2, sessionId3);
281     }
282 
283     @Test
testHandleIdleMaintenance_deleteStaleBlobs()284     public void testHandleIdleMaintenance_deleteStaleBlobs() throws Exception {
285         // Setup blobs
286         final long blobId1 = 3489;
287         final File blobFile1 = mock(File.class);
288         final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
289                 "label1", System.currentTimeMillis() - 2000, "tag1");
290         final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobId1, blobFile1, blobHandle1,
291                 true /* hasLeases */);
292         addBlob(blobHandle1, blobMetadata1);
293 
294         final long blobId2 = 78974;
295         final File blobFile2 = mock(File.class);
296         final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
297                 "label2", System.currentTimeMillis() + 30000, "tag2");
298         final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobId2, blobFile2, blobHandle2,
299                 true /* hasLeases */);
300         addBlob(blobHandle2, blobMetadata2);
301 
302         final long blobId3 = 97;
303         final File blobFile3 = mock(File.class);
304         final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
305                 "label3", System.currentTimeMillis() + 4400000, "tag3");
306         final BlobMetadata blobMetadata3 = createBlobMetadataMock(blobId3, blobFile3, blobHandle3,
307                 false /* hasLeases */);
308         addBlob(blobHandle3, blobMetadata3);
309 
310         mService.addActiveIdsForTest(blobId1, blobId2, blobId3);
311 
312         // Invoke test method
313         mService.handleIdleMaintenanceLocked();
314 
315         // Verify stale blobs are removed
316         verify(blobMetadata1).destroy();
317         verify(blobMetadata2, never()).destroy();
318         verify(blobMetadata3).destroy();
319 
320         assertThat(mService.getBlobsCountForTest()).isEqualTo(1);
321         assertThat(mService.getBlobForTest(blobHandle2)).isNotNull();
322 
323         assertThat(mService.getActiveIdsForTest()).containsExactly(blobId2);
324         assertThat(mService.getKnownIdsForTest()).containsExactly(blobId1, blobId2, blobId3);
325     }
326 
327     @Test
testGetTotalUsageBytes()328     public void testGetTotalUsageBytes() throws Exception {
329         // Setup blobs
330         final BlobMetadata blobMetadata1 = mock(BlobMetadata.class);
331         final long size1 = 4567;
332         doReturn(size1).when(blobMetadata1).getSize();
333         doReturn(true).when(blobMetadata1).isALeasee(TEST_PKG1, TEST_UID1);
334         doReturn(true).when(blobMetadata1).isALeasee(TEST_PKG2, TEST_UID2);
335         addBlob(mock(BlobHandle.class), blobMetadata1);
336 
337         final BlobMetadata blobMetadata2 = mock(BlobMetadata.class);
338         final long size2 = 89475;
339         doReturn(size2).when(blobMetadata2).getSize();
340         doReturn(false).when(blobMetadata2).isALeasee(TEST_PKG1, TEST_UID1);
341         doReturn(true).when(blobMetadata2).isALeasee(TEST_PKG2, TEST_UID2);
342         addBlob(mock(BlobHandle.class), blobMetadata2);
343 
344         final BlobMetadata blobMetadata3 = mock(BlobMetadata.class);
345         final long size3 = 328732;
346         doReturn(size3).when(blobMetadata3).getSize();
347         doReturn(true).when(blobMetadata3).isALeasee(TEST_PKG1, TEST_UID1);
348         doReturn(false).when(blobMetadata3).isALeasee(TEST_PKG2, TEST_UID2);
349         addBlob(mock(BlobHandle.class), blobMetadata3);
350 
351         // Verify usage is calculated correctly
352         assertThat(mService.getTotalUsageBytesLocked(TEST_UID1, TEST_PKG1))
353                 .isEqualTo(size1 + size3);
354         assertThat(mService.getTotalUsageBytesLocked(TEST_UID2, TEST_PKG2))
355                 .isEqualTo(size1 + size2);
356     }
357 
createBlobStoreSessionMock(String ownerPackageName, int ownerUid, long sessionId, File sessionFile)358     private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
359             long sessionId, File sessionFile) {
360         return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile,
361                 mock(BlobHandle.class));
362     }
createBlobStoreSessionMock(String ownerPackageName, int ownerUid, long sessionId, File sessionFile, BlobHandle blobHandle)363     private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
364             long sessionId, File sessionFile, BlobHandle blobHandle) {
365         final BlobStoreSession session = mock(BlobStoreSession.class);
366         doReturn(ownerPackageName).when(session).getOwnerPackageName();
367         doReturn(ownerUid).when(session).getOwnerUid();
368         doReturn(sessionId).when(session).getSessionId();
369         doReturn(sessionFile).when(session).getSessionFile();
370         doReturn(blobHandle).when(session).getBlobHandle();
371         doCallRealMethod().when(session).isExpired();
372         return session;
373     }
374 
createBlobMetadataMock(long blobId, File blobFile, BlobHandle blobHandle, boolean hasValidLeases)375     private BlobMetadata createBlobMetadataMock(long blobId, File blobFile,
376             BlobHandle blobHandle, boolean hasValidLeases) {
377         final BlobMetadata blobMetadata = mock(BlobMetadata.class);
378         doReturn(blobId).when(blobMetadata).getBlobId();
379         doReturn(blobFile).when(blobMetadata).getBlobFile();
380         doReturn(hasValidLeases).when(blobMetadata).hasValidLeases();
381         doReturn(blobHandle).when(blobMetadata).getBlobHandle();
382         doCallRealMethod().when(blobMetadata).shouldBeDeleted(anyBoolean());
383         doReturn(true).when(blobMetadata).hasLeaseWaitTimeElapsedForAll();
384         return blobMetadata;
385     }
386 
addBlob(BlobHandle blobHandle, BlobMetadata blobMetadata)387     private void addBlob(BlobHandle blobHandle, BlobMetadata blobMetadata) {
388         doReturn(blobHandle).when(blobMetadata).getBlobHandle();
389         mService.addBlobLocked(blobMetadata);
390     }
391 
392     private class TestHandler extends Handler {
TestHandler(Looper looper)393         TestHandler(Looper looper) {
394             super(looper);
395         }
396 
397         @Override
dispatchMessage(Message msg)398         public void dispatchMessage(Message msg) {
399             // Ignore all messages
400         }
401     }
402 
403     private class TestInjector extends Injector {
404         @Override
initializeMessageHandler()405         public Handler initializeMessageHandler() {
406             return mHandler;
407         }
408 
409         @Override
getBackgroundHandler()410         public Handler getBackgroundHandler() {
411             return mHandler;
412         }
413     }
414 }
415