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 android.providerui.cts;
18 
19 import static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.fail;
23 
24 import android.app.Activity;
25 import android.app.Instrumentation;
26 import android.app.UiAutomation;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.UriPermission;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ResolveInfo;
33 import android.content.res.AssetFileDescriptor;
34 import android.net.Uri;
35 import android.os.Environment;
36 import android.os.FileUtils;
37 import android.os.ParcelFileDescriptor;
38 import android.os.storage.StorageManager;
39 import android.os.storage.StorageVolume;
40 import android.os.UserManager;
41 import android.provider.MediaStore;
42 import android.providerui.cts.GetResultActivity.Result;
43 import android.support.test.uiautomator.By;
44 import android.support.test.uiautomator.BySelector;
45 import android.support.test.uiautomator.UiDevice;
46 import android.support.test.uiautomator.UiObject2;
47 import android.support.test.uiautomator.Until;
48 import android.system.Os;
49 import android.text.format.DateUtils;
50 import android.util.Log;
51 
52 import androidx.test.InstrumentationRegistry;
53 
54 import org.junit.After;
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 import org.junit.runners.Parameterized;
59 import org.junit.runners.Parameterized.Parameter;
60 import org.junit.runners.Parameterized.Parameters;
61 
62 import java.io.BufferedReader;
63 import java.io.File;
64 import java.io.FileInputStream;
65 import java.io.FileNotFoundException;
66 import java.io.FileOutputStream;
67 import java.io.IOException;
68 import java.io.InputStream;
69 import java.io.InputStreamReader;
70 import java.io.OutputStream;
71 import java.nio.charset.StandardCharsets;
72 
73 @RunWith(Parameterized.class)
74 public class MediaStoreUiTest {
75     private static final String TAG = "MediaStoreUiTest";
76 
77     private static final int REQUEST_CODE = 42;
78 
79     private Instrumentation mInstrumentation;
80     private Context mContext;
81     private UiDevice mDevice;
82     private GetResultActivity mActivity;
83 
84     private File mFile;
85     private Uri mMediaStoreUri;
86     private String mTargetPackageName;
87 
88     @Parameter(0)
89     public String mVolumeName;
90 
91     @Parameters
data()92     public static Iterable<? extends Object> data() {
93         return MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext());
94     }
95 
96     @Before
setUp()97     public void setUp() throws Exception {
98         mInstrumentation = InstrumentationRegistry.getInstrumentation();
99         mContext = InstrumentationRegistry.getTargetContext();
100         mDevice = UiDevice.getInstance(mInstrumentation);
101 
102         final Intent intent = new Intent(mContext, GetResultActivity.class);
103         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
104         mActivity = (GetResultActivity) mInstrumentation.startActivitySync(intent);
105         mInstrumentation.waitForIdleSync();
106         mActivity.clearResult();
107     }
108 
109     @After
tearDown()110     public void tearDown() throws Exception {
111         if (mFile != null) {
112             mFile.delete();
113         }
114 
115         final ContentResolver resolver = mActivity.getContentResolver();
116         for (UriPermission permission : resolver.getPersistedUriPermissions()) {
117             mActivity.revokeUriPermission(
118                     permission.getUri(),
119                     Intent.FLAG_GRANT_READ_URI_PERMISSION
120                         | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
121         }
122 
123         mActivity.finish();
124     }
125 
126     @Test
testGetDocumentUri()127     public void testGetDocumentUri() throws Exception {
128         if (!supportsHardware()) return;
129 
130         prepareFile();
131 
132         final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS);
133         assertNotNull(treeUri);
134 
135         final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
136         assertNotNull(docUri);
137 
138         final ContentResolver resolver = mActivity.getContentResolver();
139 
140         // Test reading
141         final byte[] expected = "TEST".getBytes();
142         final byte[] actual = new byte[4];
143         try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "r")) {
144             Os.read(fd.getFileDescriptor(), actual, 0, actual.length);
145             assertArrayEquals(expected, actual);
146         }
147 
148         // Test writing
149         try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "wt")) {
150             Os.write(fd.getFileDescriptor(), expected, 0, expected.length);
151         }
152     }
153 
154     @Test
testGetDocumentUri_ThrowsWithoutPermission()155     public void testGetDocumentUri_ThrowsWithoutPermission() throws Exception {
156         if (!supportsHardware()) return;
157 
158         prepareFile();
159 
160         try {
161             MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
162             fail("Expecting SecurityException.");
163         } catch (SecurityException e) {
164             // Expected
165         }
166     }
167 
168     @Test
testGetDocumentUri_Symmetry()169     public void testGetDocumentUri_Symmetry() throws Exception {
170         if (!supportsHardware()) return;
171 
172         prepareFile();
173 
174         final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS);
175         Log.v(TAG, "Tree " + treeUri);
176         assertNotNull(treeUri);
177 
178         final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri);
179         Log.v(TAG, "Document " + docUri);
180         assertNotNull(docUri);
181 
182         final Uri mediaUri = MediaStore.getMediaUri(mActivity, docUri);
183         Log.v(TAG, "Media " + mediaUri);
184         assertNotNull(mediaUri);
185 
186         assertEquals(mMediaStoreUri, mediaUri);
187     }
188 
supportsHardware()189     private boolean supportsHardware() {
190         final PackageManager pm = mContext.getPackageManager();
191         return !pm.hasSystemFeature("android.hardware.type.television")
192                 && !pm.hasSystemFeature("android.hardware.type.watch");
193     }
194 
prepareFile()195     private void prepareFile() throws Exception {
196         final File dir = new File(MediaStore.getVolumePath(mVolumeName),
197                 Environment.DIRECTORY_DOCUMENTS);
198         final File file = new File(dir, "cts" + System.nanoTime() + ".txt");
199 
200         mFile = stageFile(R.raw.text, file);
201         mMediaStoreUri = MediaStore.scanFile(mContext, mFile);
202 
203         Log.v(TAG, "Staged " + mFile + " as " + mMediaStoreUri);
204     }
205 
acquireAccess(File file, String directoryName)206     private Uri acquireAccess(File file, String directoryName) {
207         StorageManager storageManager =
208                 (StorageManager) mActivity.getSystemService(Context.STORAGE_SERVICE);
209 
210         // Request access from DocumentsUI
211         final StorageVolume volume = storageManager.getStorageVolume(file);
212         final Intent intent = volume.createOpenDocumentTreeIntent();
213         mActivity.startActivityForResult(intent, REQUEST_CODE);
214 
215         if (mTargetPackageName == null) {
216             mTargetPackageName = getTargetPackageName(mActivity);
217         }
218 
219         // Granting the access
220         BySelector buttonPanelSelector = By.pkg(mTargetPackageName)
221                 .res(mTargetPackageName + ":id/container_save");
222         mDevice.wait(Until.hasObject(buttonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS);
223         final UiObject2 buttonPanel = mDevice.findObject(buttonPanelSelector);
224         final UiObject2 allowButton = buttonPanel.findObject(By.res("android:id/button1"));
225         allowButton.click();
226         mDevice.waitForIdle();
227 
228         // Granting the access by click "allow" in confirm dialog
229         final BySelector dialogButtonPanelSelector = By.pkg(mTargetPackageName)
230                 .res(mTargetPackageName + ":id/buttonPanel");
231         mDevice.wait(Until.hasObject(dialogButtonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS);
232         final UiObject2 positiveButton = mDevice.findObject(dialogButtonPanelSelector)
233                 .findObject(By.res("android:id/button1"));
234         positiveButton.click();
235         mDevice.waitForIdle();
236 
237         // Check granting result and take persistent permission
238         final Result result = mActivity.getResult();
239         assertEquals(Activity.RESULT_OK, result.resultCode);
240 
241         final Intent resultIntent = result.data;
242         final Uri resultUri = resultIntent.getData();
243         final int flags = resultIntent.getFlags()
244                 & (Intent.FLAG_GRANT_READ_URI_PERMISSION
245                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
246         mActivity.getContentResolver().takePersistableUriPermission(resultUri, flags);
247         return resultUri;
248     }
249 
getTargetPackageName(Context context)250     private static String getTargetPackageName(Context context) {
251         final PackageManager pm = context.getPackageManager();
252 
253         final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
254         intent.addCategory(Intent.CATEGORY_OPENABLE);
255         intent.setType("*/*");
256         final ResolveInfo ri = pm.resolveActivity(intent, 0);
257         return ri.activityInfo.packageName;
258     }
259 
260     // TODO: replace with ProviderTestUtils
executeShellCommand(String command)261     static String executeShellCommand(String command) throws IOException {
262         return executeShellCommand(command,
263                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
264     }
265 
266     // TODO: replace with ProviderTestUtils
executeShellCommand(String command, UiAutomation uiAutomation)267     static String executeShellCommand(String command, UiAutomation uiAutomation)
268             throws IOException {
269         Log.v(TAG, "$ " + command);
270         ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
271         BufferedReader br = null;
272         try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
273             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
274             String str = null;
275             StringBuilder out = new StringBuilder();
276             while ((str = br.readLine()) != null) {
277                 Log.v(TAG, "> " + str);
278                 out.append(str);
279             }
280             return out.toString();
281         } finally {
282             if (br != null) {
283                 br.close();
284             }
285         }
286     }
287 
288     // TODO: replace with ProviderTestUtils
stageFile(int resId, File file)289     static File stageFile(int resId, File file) throws IOException {
290         // The caller may be trying to stage into a location only available to
291         // the shell user, so we need to perform the entire copy as the shell
292         final Context context = InstrumentationRegistry.getTargetContext();
293         UserManager userManager = context.getSystemService(UserManager.class);
294         if (userManager.isSystemUser() &&
295                  FileUtils.contains(Environment.getStorageDirectory(), file)) {
296             executeShellCommand("mkdir -p " + file.getParent());
297 
298             try (AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId)) {
299                 final File source = ParcelFileDescriptor.getFile(afd.getFileDescriptor());
300                 final long skip = afd.getStartOffset();
301                 final long count = afd.getLength();
302 
303                 executeShellCommand(String.format("dd bs=1 if=%s skip=%d count=%d of=%s",
304                         source.getAbsolutePath(), skip, count, file.getAbsolutePath()));
305 
306                 // Force sync to try updating other views
307                 executeShellCommand("sync");
308             }
309         } else {
310             final File dir = file.getParentFile();
311             dir.mkdirs();
312             if (!dir.exists()) {
313                 throw new FileNotFoundException("Failed to create parent for " + file);
314             }
315             try (InputStream source = context.getResources().openRawResource(resId);
316                     OutputStream target = new FileOutputStream(file)) {
317                 FileUtils.copy(source, target);
318             }
319         }
320         return file;
321     }
322 }
323