1 /*
2  * Copyright (C) 2016 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.documentsui;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static junit.framework.Assert.assertTrue;
22 import static junit.framework.Assert.fail;
23 
24 import static org.junit.Assert.assertEquals;
25 import static org.mockito.Mockito.when;
26 
27 import android.content.Intent;
28 import android.net.Uri;
29 import android.os.Parcelable;
30 import android.provider.DocumentsContract;
31 import android.provider.DocumentsContract.Path;
32 
33 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
34 import androidx.test.filters.MediumTest;
35 
36 import com.android.documentsui.base.DocumentStack;
37 import com.android.documentsui.base.EventListener;
38 import com.android.documentsui.base.RootInfo;
39 import com.android.documentsui.base.Shared;
40 import com.android.documentsui.base.State;
41 import com.android.documentsui.files.LauncherActivity;
42 import com.android.documentsui.sorting.SortDimension;
43 import com.android.documentsui.sorting.SortModel;
44 import com.android.documentsui.testing.DocumentStackAsserts;
45 import com.android.documentsui.testing.Roots;
46 import com.android.documentsui.testing.TestEnv;
47 import com.android.documentsui.testing.TestEventHandler;
48 import com.android.documentsui.testing.TestProvidersAccess;
49 import com.android.documentsui.testing.UserManagers;
50 import com.android.modules.utils.build.SdkLevel;
51 
52 import com.google.android.collect.Lists;
53 
54 import org.junit.Before;
55 import org.junit.Test;
56 import org.junit.runner.RunWith;
57 import org.junit.runners.Parameterized;
58 import org.junit.runners.Parameterized.Parameter;
59 import org.junit.runners.Parameterized.Parameters;
60 
61 import java.util.Arrays;
62 import java.util.concurrent.CountDownLatch;
63 import java.util.concurrent.TimeUnit;
64 
65 /**
66  * A unit test *for* AbstractActionHandler, not an abstract test baseclass.
67  */
68 @RunWith(Parameterized.class)
69 @MediumTest
70 public class AbstractActionHandlerTest {
71 
72     private final TestConfigStore mTestConfigStore = new TestConfigStore();
73     private TestActivity mActivity;
74     private TestEnv mEnv;
75     private AbstractActionHandler<TestActivity> mHandler;
76 
77     @Parameter(0)
78     public boolean isPrivateSpaceEnabled;
79 
80     /**
81      * Parametrize values for {@code isPrivateSpaceEnabled} to run all the tests twice once with
82      * private space flag enabled and once with it disabled.
83      */
84     @Parameters(name = "privateSpaceEnabled={0}")
data()85     public static Iterable<?> data() {
86         return Lists.newArrayList(true, false);
87     }
88 
89     @Before
setUp()90     public void setUp() {
91         mEnv = TestEnv.create();
92         mActivity = TestActivity.create(mEnv);
93         mActivity.userManager = UserManagers.create();
94         mEnv.state.configStore = mTestConfigStore;
95 
96         isPrivateSpaceEnabled = SdkLevel.isAtLeastS() && isPrivateSpaceEnabled;
97         if (isPrivateSpaceEnabled) {
98             mTestConfigStore.enablePrivateSpaceInPhotoPicker();
99             mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.USER_ID, true);
100         }
101         mHandler = new AbstractActionHandler<TestActivity>(
102                 mActivity,
103                 mEnv.state,
104                 mEnv.providers,
105                 mEnv.docs,
106                 mEnv.searchViewManager,
107                 mEnv::lookupExecutor,
108                 mEnv.injector) {
109 
110             @Override
111             public void openRoot(RootInfo root) {
112                 throw new UnsupportedOperationException();
113             }
114 
115             @Override
116             public boolean openItem(
117                     ItemDetails<String> doc, @ViewType int type, @ViewType int fallback) {
118                 throw new UnsupportedOperationException();
119             }
120 
121             @Override
122             public void initLocation(Intent intent) {
123                 throw new UnsupportedOperationException();
124             }
125 
126             @Override
127             protected void launchToDefaultLocation() {
128                 throw new UnsupportedOperationException();
129             }
130         };
131     }
132 
133     @Test
testOpenNewWindow()134     public void testOpenNewWindow() {
135         DocumentStack path = new DocumentStack(Roots.create("123"));
136         mHandler.openInNewWindow(path);
137 
138         Intent expected = LauncherActivity.createLaunchIntent(mActivity);
139         expected.putExtra(Shared.EXTRA_STACK, (Parcelable) path);
140         Intent actual = mActivity.startActivity.getLastValue();
141         assertEquals(expected.toString(), actual.toString());
142     }
143 
144     @Test
testOpensContainerDocuments_OpenFolderInSearch_JumpsToNewLocation()145     public void testOpensContainerDocuments_OpenFolderInSearch_JumpsToNewLocation()
146             throws Exception {
147         if (!mEnv.features.isLaunchToDocumentEnabled()) {
148             return;
149         }
150 
151         mEnv.populateStack();
152 
153         mEnv.searchViewManager.isSearching = true;
154         mEnv.docs.nextIsDocumentsUri = true;
155         mEnv.docs.nextPath = new Path(
156                 TestProvidersAccess.HOME.rootId,
157                 Arrays.asList(TestEnv.FOLDER_1.documentId, TestEnv.FOLDER_2.documentId));
158         mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2);
159 
160         mHandler.openContainerDocument(TestEnv.FOLDER_2);
161 
162         mEnv.beforeAsserts();
163 
164         assertEquals(mEnv.docs.nextPath.getPath().size(), mEnv.state.stack.size());
165         assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.pop());
166         assertEquals(TestEnv.FOLDER_1, mEnv.state.stack.pop());
167     }
168 
169 
170     @Test
testOpensContainerDocuments_ClickFolderInSearch_PushToRootDoc_NoFindPathSupport()171     public void testOpensContainerDocuments_ClickFolderInSearch_PushToRootDoc_NoFindPathSupport()
172             throws Exception {
173         mEnv.populateStack();
174 
175         mEnv.searchViewManager.isSearching = true;
176         mEnv.docs.nextIsDocumentsUri = true;
177         mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2);
178 
179         mHandler.openContainerDocument(TestEnv.FOLDER_2);
180 
181         mEnv.beforeAsserts();
182 
183         assertEquals(2, mEnv.state.stack.size());
184         assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.pop());
185         assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.pop());
186     }
187 
188     @Test
testOpensContainerDocuments_ClickArchiveInSearch_opensArchiveInArchiveProvider()189     public void testOpensContainerDocuments_ClickArchiveInSearch_opensArchiveInArchiveProvider()
190             throws Exception {
191         if (!mEnv.features.isLaunchToDocumentEnabled()) {
192             return;
193         }
194 
195         mEnv.populateStack();
196 
197         mEnv.searchViewManager.isSearching = true;
198         mEnv.docs.nextIsDocumentsUri = true;
199         mEnv.docs.nextPath = new Path(
200                 TestProvidersAccess.HOME.rootId,
201                 Arrays.asList(TestEnv.FOLDER_1.documentId, TestEnv.FOLDER_2.documentId,
202                         TestEnv.FILE_ARCHIVE.documentId));
203         mEnv.docs.nextDocuments = Arrays.asList(
204                 TestEnv.FOLDER_1, TestEnv.FOLDER_2, TestEnv.FILE_ARCHIVE);
205         mEnv.docs.nextDocument = TestEnv.FILE_IN_ARCHIVE;
206 
207         mHandler.openContainerDocument(TestEnv.FILE_ARCHIVE);
208 
209         mEnv.beforeAsserts();
210 
211         assertEquals(mEnv.docs.nextPath.getPath().size(), mEnv.state.stack.size());
212         assertEquals(TestEnv.FILE_IN_ARCHIVE, mEnv.state.stack.pop());
213         assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.pop());
214         assertEquals(TestEnv.FOLDER_1, mEnv.state.stack.pop());
215     }
216 
217     @Test
testOpensDocument_ExceptionIfAlreadyInStack()218     public void testOpensDocument_ExceptionIfAlreadyInStack() throws Exception {
219         mEnv.populateStack();
220         try {
221             mEnv.state.stack.push(TestEnv.FOLDER_0);
222             fail("Should have thrown IllegalArgumentException.");
223         } catch (IllegalArgumentException expected) {
224         }
225     }
226 
227     @Test
testLaunchToDocuments()228     public void testLaunchToDocuments() throws Exception {
229         if (!mEnv.features.isLaunchToDocumentEnabled()) {
230             return;
231         }
232 
233         mEnv.docs.nextIsDocumentsUri = true;
234         mEnv.docs.nextPath = new Path(
235                 TestProvidersAccess.HOME.rootId,
236                 Arrays.asList(
237                         TestEnv.FOLDER_0.documentId,
238                         TestEnv.FOLDER_1.documentId,
239                         TestEnv.FILE_GIF.documentId));
240         mEnv.docs.nextDocuments =
241                 Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
242 
243         mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
244         assertTrue(mHandler.launchToDocument(TestEnv.FILE_GIF.derivedUri));
245 
246         mEnv.beforeAsserts();
247 
248         DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestProvidersAccess.HOME,
249                 Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
250         mActivity.refreshCurrentRootAndDirectory.assertCalled();
251     }
252 
253     @Test
testLaunchToDocuments_convertsTreeUriToDocumentUri()254     public void testLaunchToDocuments_convertsTreeUriToDocumentUri() throws Exception {
255         if (!mEnv.features.isLaunchToDocumentEnabled()) {
256             return;
257         }
258 
259         mEnv.docs.nextIsDocumentsUri = true;
260         mEnv.docs.nextPath = new Path(
261                 TestProvidersAccess.HOME.rootId,
262                 Arrays.asList(
263                         TestEnv.FOLDER_0.documentId,
264                         TestEnv.FOLDER_1.documentId,
265                         TestEnv.FILE_GIF.documentId));
266         mEnv.docs.nextDocuments =
267                 Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
268 
269         final Uri treeBaseUri = DocumentsContract.buildTreeDocumentUri(
270                 TestProvidersAccess.HOME.authority, TestEnv.FOLDER_0.documentId);
271         final Uri treeDocUri = DocumentsContract.buildDocumentUriUsingTree(
272                 treeBaseUri, TestEnv.FILE_GIF.documentId);
273         assertTrue(mHandler.launchToDocument(treeDocUri));
274 
275         mEnv.beforeAsserts();
276 
277         DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestProvidersAccess.HOME,
278                 Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
279         mEnv.docs.lastUri.assertLastArgument(TestEnv.FILE_GIF.derivedUri);
280         mActivity.refreshCurrentRootAndDirectory.assertCalled();
281     }
282 
283     @Test
testLoadChildrenDocuments()284     public void testLoadChildrenDocuments() throws Exception {
285         mEnv.state.stack.changeRoot(TestProvidersAccess.HOME);
286         mEnv.state.stack.push(TestEnv.FOLDER_0);
287 
288         mEnv.state.sortModel.sortByUser(
289                 SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
290 
291         mEnv.mockProviders.get(TestProvidersAccess.HOME.authority)
292                 .setNextChildDocumentsReturns(TestEnv.FILE_APK, TestEnv.FILE_GIF);
293 
294         mHandler.loadDocumentsForCurrentStack();
295         CountDownLatch latch = new CountDownLatch(1);
296         mEnv.model.addUpdateListener(event -> latch.countDown());
297         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
298 
299         latch.await(1, TimeUnit.SECONDS);
300         assertEquals(2, mEnv.model.getItemCount());
301         String[] modelIds = mEnv.model.getModelIds();
302         assertEquals(TestEnv.FILE_APK, mEnv.model.getDocument(modelIds[0]));
303         assertEquals(TestEnv.FILE_GIF, mEnv.model.getDocument(modelIds[1]));
304     }
305 
306     @Test
testCrossProfileDocuments_success()307     public void testCrossProfileDocuments_success() throws Exception {
308         mEnv.state.action = State.ACTION_GET_CONTENT;
309         if (isPrivateSpaceEnabled) {
310             mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, true);
311         } else {
312             mEnv.state.canShareAcrossProfile = true;
313         }
314         mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
315         mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
316 
317         mEnv.state.sortModel.sortByUser(
318                 SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
319 
320         // Currently mock provider does not have cross profile concept, this will always return
321         // the supplied docs without checking for the user. But this should not be a problem for
322         // this test case.
323         mEnv.mockProviders.get(TestProvidersAccess.OtherUser.HOME.authority)
324                 .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG);
325 
326         mHandler.loadDocumentsForCurrentStack();
327         CountDownLatch latch = new CountDownLatch(1);
328         mEnv.model.addUpdateListener(event -> latch.countDown());
329         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
330 
331         latch.await(1, TimeUnit.SECONDS);
332         assertEquals(1, mEnv.model.getItemCount());
333         String[] modelIds = mEnv.model.getModelIds();
334         assertEquals(TestEnv.OtherUser.FILE_PNG, mEnv.model.getDocument(modelIds[0]));
335     }
336 
337     @Test
testLoadCrossProfileDoc_failsWithQuietModeException()338     public void testLoadCrossProfileDoc_failsWithQuietModeException() throws Exception {
339         mEnv.state.action = State.ACTION_GET_CONTENT;
340         if (isPrivateSpaceEnabled) {
341             mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, true);
342         } else {
343             mEnv.state.canShareAcrossProfile = true;
344         }
345         mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
346         mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
347         // Turn off the other user.
348         when(mActivity.userManager.isQuietModeEnabled(TestProvidersAccess.OtherUser.USER_HANDLE))
349                 .thenReturn(true);
350 
351         TestEventHandler<Model.Update> listener = new TestEventHandler<>();
352         mEnv.model.addUpdateListener(listener::accept);
353 
354         mHandler.loadDocumentsForCurrentStack();
355         CountDownLatch latch = new CountDownLatch(1);
356         mEnv.model.addUpdateListener(event -> latch.countDown());
357         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
358 
359         latch.await(1, TimeUnit.SECONDS);
360         assertThat(listener.getLastValue().getException())
361                 .isInstanceOf(CrossProfileQuietModeException.class);
362     }
363 
364     @Test
testLoadCrossProfileDoc_failsWithNoPermissionException()365     public void testLoadCrossProfileDoc_failsWithNoPermissionException() throws Exception {
366         mEnv.state.action = State.ACTION_GET_CONTENT;
367         mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
368         mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
369         // Disallow sharing across profile
370         mEnv.state.canShareAcrossProfile = false;
371 
372         TestEventHandler<Model.Update> listener = new TestEventHandler<>();
373         mEnv.model.addUpdateListener(listener::accept);
374 
375         mHandler.loadDocumentsForCurrentStack();
376         CountDownLatch latch = new CountDownLatch(1);
377         mEnv.model.addUpdateListener(event -> latch.countDown());
378         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
379 
380         latch.await(1, TimeUnit.SECONDS);
381         assertThat(listener.getLastValue().getException())
382                 .isInstanceOf(CrossProfileNoPermissionException.class);
383     }
384 
385     @Test
testLoadCrossProfileDoc_bothError_showNoPermissionException()386     public void testLoadCrossProfileDoc_bothError_showNoPermissionException() throws Exception {
387         mEnv.state.action = State.ACTION_GET_CONTENT;
388         mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
389         mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
390         // Disallow sharing
391         mEnv.state.canShareAcrossProfile = false;
392         // Turn off the other user.
393         when(mActivity.userManager.isQuietModeEnabled(TestProvidersAccess.OtherUser.USER_HANDLE))
394                 .thenReturn(true);
395 
396         TestEventHandler<Model.Update> listener = new TestEventHandler<>();
397         mEnv.model.addUpdateListener(listener::accept);
398 
399         mHandler.loadDocumentsForCurrentStack();
400         CountDownLatch latch = new CountDownLatch(1);
401         mEnv.model.addUpdateListener(event -> latch.countDown());
402         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
403 
404         latch.await(1, TimeUnit.SECONDS);
405         assertThat(listener.getLastValue().getException())
406                 .isInstanceOf(CrossProfileNoPermissionException.class);
407     }
408 
409     @Test
testCrossProfileDocuments_reloadSuccessAfterCrossProfileError()410     public void testCrossProfileDocuments_reloadSuccessAfterCrossProfileError() throws Exception {
411         mEnv.state.action = State.ACTION_GET_CONTENT;
412         mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
413         mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
414 
415         mEnv.state.sortModel.sortByUser(
416                 SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
417 
418         // Currently mock provider does not have cross profile concept, this will always return
419         // the supplied docs without checking for the user. But this should not be a problem for
420         // this test case.
421         mEnv.mockProviders.get(TestProvidersAccess.OtherUser.HOME.authority)
422                 .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG);
423 
424         // Disallow sharing across profile
425         if (isPrivateSpaceEnabled) {
426             mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, false);
427         } else {
428             mEnv.state.canShareAcrossProfile = false;
429         }
430 
431         TestEventHandler<Model.Update> listener = new TestEventHandler<>();
432         mEnv.model.addUpdateListener(listener::accept);
433 
434         mHandler.loadDocumentsForCurrentStack();
435         CountDownLatch latch1 = new CountDownLatch(1);
436         EventListener<Model.Update> updateEventListener1 = update -> latch1.countDown();
437         mEnv.model.addUpdateListener(updateEventListener1);
438         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
439         latch1.await(1, TimeUnit.SECONDS);
440         assertThat(listener.getLastValue().getException())
441                 .isInstanceOf(CrossProfileNoPermissionException.class);
442 
443         // Allow sharing across profile.
444         if (isPrivateSpaceEnabled) {
445             mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, true);
446         } else {
447             mEnv.state.canShareAcrossProfile = true;
448         }
449 
450         CountDownLatch latch2 = new CountDownLatch(1);
451         mEnv.model.addUpdateListener(update -> latch2.countDown());
452         mHandler.loadDocumentsForCurrentStack();
453         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
454 
455         latch2.await(1, TimeUnit.SECONDS);
456         assertEquals(1, mEnv.model.getItemCount());
457         String[] modelIds = mEnv.model.getModelIds();
458         assertEquals(TestEnv.OtherUser.FILE_PNG, mEnv.model.getDocument(modelIds[0]));
459     }
460 
461     @Test
testLoadChildrenDocuments_failsWithNonRecentsAndEmptyStack()462     public void testLoadChildrenDocuments_failsWithNonRecentsAndEmptyStack() throws Exception {
463         mEnv.state.stack.changeRoot(TestProvidersAccess.HOME);
464 
465         mEnv.mockProviders.get(TestProvidersAccess.HOME.authority)
466                 .setNextChildDocumentsReturns(TestEnv.FILE_APK, TestEnv.FILE_GIF);
467 
468         TestEventHandler<Model.Update> listener = new TestEventHandler<>();
469         mEnv.model.addUpdateListener(listener::accept);
470 
471         mHandler.loadDocumentsForCurrentStack();
472         CountDownLatch latch = new CountDownLatch(1);
473         mEnv.model.addUpdateListener(event -> latch.countDown());
474         mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
475 
476         latch.await(1, TimeUnit.SECONDS);
477         assertTrue(listener.getLastValue().hasException());
478     }
479 
480     @Test
testPreviewItem_throwException()481     public void testPreviewItem_throwException() throws Exception {
482         try {
483             mHandler.previewItem(null);
484             fail("Should have thrown UnsupportedOperationException.");
485         } catch (UnsupportedOperationException expected) {
486         }
487     }
488 }
489