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.provider.cts;
18 
19 import static android.provider.cts.MediaStoreTest.TAG;
20 
21 import static com.google.common.truth.Truth.assertWithMessage;
22 import static org.junit.Assert.fail;
23 
24 import android.app.UiAutomation;
25 import android.content.Context;
26 import android.content.res.AssetFileDescriptor;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.os.Environment;
30 import android.os.FileUtils;
31 import android.os.ParcelFileDescriptor;
32 import android.os.UserHandle;
33 import android.os.UserManager;
34 import android.provider.MediaStore;
35 import android.provider.MediaStore.MediaColumns;
36 import android.provider.cts.MediaStoreUtils.PendingParams;
37 import android.provider.cts.MediaStoreUtils.PendingSession;
38 import android.system.ErrnoException;
39 import android.system.Os;
40 import android.system.OsConstants;
41 import android.util.Log;
42 
43 import androidx.test.InstrumentationRegistry;
44 
45 import com.android.compatibility.common.util.Timeout;
46 
47 import java.io.BufferedReader;
48 import java.io.File;
49 import java.io.FileInputStream;
50 import java.io.FileNotFoundException;
51 import java.io.FileOutputStream;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.InputStreamReader;
55 import java.io.OutputStream;
56 import java.nio.charset.StandardCharsets;
57 import java.security.DigestInputStream;
58 import java.security.MessageDigest;
59 import java.util.HashSet;
60 import java.util.Objects;
61 import java.util.regex.Matcher;
62 import java.util.regex.Pattern;
63 
64 /**
65  * Utility methods for provider cts tests.
66  */
67 public class ProviderTestUtils {
68 
69     private static final int BACKUP_TIMEOUT_MILLIS = 4000;
70     private static final Pattern BMGR_ENABLED_PATTERN = Pattern.compile(
71             "^Backup Manager currently (enabled|disabled)$");
72 
73     private static final Pattern PATTERN_STORAGE_PATH = Pattern.compile(
74             "(?i)^/storage/[^/]+/(?:[0-9]+/)?");
75 
76     private static final Timeout IO_TIMEOUT = new Timeout("IO_TIMEOUT", 2_000, 2, 2_000);
77 
getSharedVolumeNames()78     static Iterable<String> getSharedVolumeNames() {
79         // We test both new and legacy volume names
80         final HashSet<String> testVolumes = new HashSet<>();
81         testVolumes.addAll(
82                 MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext()));
83         testVolumes.add(MediaStore.VOLUME_EXTERNAL);
84         return testVolumes;
85     }
86 
resolveVolumeName(String volumeName)87     static String resolveVolumeName(String volumeName) {
88         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
89             return MediaStore.VOLUME_EXTERNAL_PRIMARY;
90         } else {
91             return volumeName;
92         }
93     }
94 
setDefaultSmsApp(boolean setToSmsApp, String packageName, UiAutomation uiAutomation)95     static void setDefaultSmsApp(boolean setToSmsApp, String packageName, UiAutomation uiAutomation)
96             throws Exception {
97         String mode = setToSmsApp ? "allow" : "default";
98         String cmd = "appops set %s %s %s";
99         executeShellCommand(String.format(cmd, packageName, "WRITE_SMS", mode), uiAutomation);
100         executeShellCommand(String.format(cmd, packageName, "READ_SMS", mode), uiAutomation);
101     }
102 
executeShellCommand(String command)103     static String executeShellCommand(String command) throws IOException {
104         return executeShellCommand(command,
105                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
106     }
107 
executeShellCommand(String command, UiAutomation uiAutomation)108     static String executeShellCommand(String command, UiAutomation uiAutomation)
109             throws IOException {
110         Log.v(TAG, "$ " + command);
111         ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
112         BufferedReader br = null;
113         try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
114             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
115             String str = null;
116             StringBuilder out = new StringBuilder();
117             while ((str = br.readLine()) != null) {
118                 Log.v(TAG, "> " + str);
119                 out.append(str);
120             }
121             return out.toString();
122         } finally {
123             if (br != null) {
124                 br.close();
125             }
126         }
127     }
128 
setBackupTransport(String transport, UiAutomation uiAutomation)129     static String setBackupTransport(String transport, UiAutomation uiAutomation) throws Exception {
130         String output = executeShellCommand("bmgr transport " + transport, uiAutomation);
131         Pattern pattern = Pattern.compile("\\(formerly (.*)\\)$");
132         Matcher matcher = pattern.matcher(output);
133         if (matcher.find()) {
134             return matcher.group(1);
135         } else {
136             throw new Exception("non-parsable output setting bmgr transport: " + output);
137         }
138     }
139 
setBackupEnabled(boolean enable, UiAutomation uiAutomation)140     static boolean setBackupEnabled(boolean enable, UiAutomation uiAutomation) throws Exception {
141         // Check to see the previous state of the backup service
142         boolean previouslyEnabled = false;
143         String output = executeShellCommand("bmgr enabled", uiAutomation);
144         Matcher matcher = BMGR_ENABLED_PATTERN.matcher(output.trim());
145         if (matcher.find()) {
146             previouslyEnabled = "enabled".equals(matcher.group(1));
147         } else {
148             throw new RuntimeException("Backup output format changed.  No longer matches"
149                     + " expected regex: " + BMGR_ENABLED_PATTERN + "\nactual: '" + output + "'");
150         }
151 
152         executeShellCommand("bmgr enable " + enable, uiAutomation);
153         return previouslyEnabled;
154     }
155 
hasBackupTransport(String transport, UiAutomation uiAutomation)156     static boolean hasBackupTransport(String transport, UiAutomation uiAutomation)
157             throws Exception {
158         String output = executeShellCommand("bmgr list transports", uiAutomation);
159         for (String t : output.split(" ")) {
160             if ("*".equals(t)) {
161                 // skip the current selection marker.
162                 continue;
163             } else if (Objects.equals(transport, t)) {
164                 return true;
165             }
166         }
167         return false;
168     }
169 
runBackup(String packageName, UiAutomation uiAutomation)170     static void runBackup(String packageName, UiAutomation uiAutomation) throws Exception {
171         executeShellCommand("bmgr backupnow " + packageName, uiAutomation);
172         Thread.sleep(BACKUP_TIMEOUT_MILLIS);
173     }
174 
runRestore(String packageName, UiAutomation uiAutomation)175     static void runRestore(String packageName, UiAutomation uiAutomation) throws Exception {
176         executeShellCommand("bmgr restore 1 " + packageName, uiAutomation);
177         Thread.sleep(BACKUP_TIMEOUT_MILLIS);
178     }
179 
wipeBackup(String backupTransport, String packageName, UiAutomation uiAutomation)180     static void wipeBackup(String backupTransport, String packageName, UiAutomation uiAutomation)
181             throws Exception {
182         executeShellCommand("bmgr wipe " + backupTransport + " " + packageName, uiAutomation);
183     }
184 
185     /**
186      * Waits until a file exists, or fails.
187      *
188      * @return existing file.
189      */
waitUntilExists(File file)190     public static File waitUntilExists(File file) throws IOException {
191         try {
192             return IO_TIMEOUT.run("file '" + file + "' doesn't exist yet", () -> {
193                 return file.exists() ? file : null; // will retry if it returns null
194             });
195         } catch (Exception e) {
196             throw new IOException(e);
197         }
198     }
199 
stageDir(String volumeName)200     static File stageDir(String volumeName) throws IOException {
201         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
202             volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
203         }
204         File dir = Environment.buildPath(MediaStore.getVolumePath(volumeName), "Android", "media",
205                 "android.provider.cts");
206         Log.d(TAG, "stageDir(" + volumeName + "): returning " + dir);
207         return dir;
208     }
209 
stageDownloadDir(String volumeName)210     static File stageDownloadDir(String volumeName) throws IOException {
211         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
212             volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
213         }
214         return Environment.buildPath(MediaStore.getVolumePath(volumeName),
215                 Environment.DIRECTORY_DOWNLOADS, "android.provider.cts");
216     }
217 
stageFile(int resId, File file)218     static File stageFile(int resId, File file) throws IOException {
219         // The caller may be trying to stage into a location only available to
220         // the shell user, so we need to perform the entire copy as the shell
221         final Context context = InstrumentationRegistry.getTargetContext();
222         UserManager userManager = context.getSystemService(UserManager.class);
223         if (userManager.isSystemUser() &&
224                     FileUtils.contains(Environment.getStorageDirectory(), file)) {
225             executeShellCommand("mkdir -p " + file.getParent());
226             try (AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId)) {
227                 final File source = ParcelFileDescriptor.getFile(afd.getFileDescriptor());
228                 final long skip = afd.getStartOffset();
229                 final long count = afd.getLength();
230 
231                 executeShellCommand(String.format("dd bs=1 if=%s skip=%d count=%d of=%s",
232                         source.getAbsolutePath(), skip, count, file.getAbsolutePath()));
233 
234                 // Force sync to try updating other views
235                 executeShellCommand("sync");
236             }
237         } else {
238             final File dir = file.getParentFile();
239             dir.mkdirs();
240             if (!dir.exists()) {
241                 throw new FileNotFoundException("Failed to create parent for " + file);
242             }
243             try (InputStream source = context.getResources().openRawResource(resId);
244                     OutputStream target = new FileOutputStream(file)) {
245                 FileUtils.copy(source, target);
246             }
247         }
248         return waitUntilExists(file);
249     }
250 
stageMedia(int resId, Uri collectionUri)251     static Uri stageMedia(int resId, Uri collectionUri) throws IOException {
252         return stageMedia(resId, collectionUri, "image/png");
253     }
254 
stageMedia(int resId, Uri collectionUri, String mimeType)255     static Uri stageMedia(int resId, Uri collectionUri, String mimeType) throws IOException {
256         final Context context = InstrumentationRegistry.getTargetContext();
257         final String displayName = "cts" + System.nanoTime();
258         final PendingParams params = new PendingParams(collectionUri, displayName, mimeType);
259         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
260         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
261             try (InputStream source = context.getResources().openRawResource(resId);
262                     OutputStream target = session.openOutputStream()) {
263                 FileUtils.copy(source, target);
264             }
265             return session.publish();
266         }
267     }
268 
scanFile(File file)269     static Uri scanFile(File file) throws Exception {
270         Uri uri = MediaStore.scanFile(InstrumentationRegistry.getTargetContext(), file);
271         assertWithMessage("no URI for '%s'", file).that(uri).isNotNull();
272         return uri;
273     }
274 
scanFileFromShell(File file)275     static Uri scanFileFromShell(File file) throws Exception {
276         Uri uri = MediaStore.scanFileFromShell(InstrumentationRegistry.getTargetContext(), file);
277         assertWithMessage("no URI for '%s'", file).that(uri).isNotNull();
278         return uri;
279     }
280 
scanVolume(File file)281     static void scanVolume(File file) throws Exception {
282         MediaStore.scanVolume(InstrumentationRegistry.getTargetContext(), file);
283     }
284 
hash(InputStream in)285     public static byte[] hash(InputStream in) throws Exception {
286         try (DigestInputStream digestIn = new DigestInputStream(in,
287                 MessageDigest.getInstance("SHA-1"));
288                 OutputStream out = new FileOutputStream(new File("/dev/null"))) {
289             FileUtils.copy(digestIn, out);
290             return digestIn.getMessageDigest().digest();
291         }
292     }
293 
assertExists(String path)294     public static void assertExists(String path) throws IOException {
295         assertExists(null, path);
296     }
297 
assertExists(File file)298     public static void assertExists(File file) throws IOException {
299         assertExists(null, file.getAbsolutePath());
300     }
301 
assertExists(String msg, String path)302     public static void assertExists(String msg, String path) throws IOException {
303         if (!access(path)) {
304             fail(msg);
305         }
306     }
307 
assertNotExists(String path)308     public static void assertNotExists(String path) throws IOException {
309         assertNotExists(null, path);
310     }
311 
assertNotExists(File file)312     public static void assertNotExists(File file) throws IOException {
313         assertNotExists(null, file.getAbsolutePath());
314     }
315 
assertNotExists(String msg, String path)316     public static void assertNotExists(String msg, String path) throws IOException {
317         if (access(path)) {
318             fail(msg);
319         }
320     }
321 
access(String path)322     private static boolean access(String path) throws IOException {
323         // The caller may be trying to stage into a location only available to
324         // the shell user, so we need to perform the entire copy as the shell
325         if (FileUtils.contains(Environment.getStorageDirectory(), new File(path))) {
326             return executeShellCommand("ls -la " + path).contains(path);
327         } else {
328             try {
329                 Os.access(path, OsConstants.F_OK);
330                 return true;
331             } catch (ErrnoException e) {
332                 if (e.errno == OsConstants.ENOENT) {
333                     return false;
334                 } else {
335                     throw new IOException(e.getMessage());
336                 }
337             }
338         }
339     }
340 
containsId(Uri uri, long id)341     public static boolean containsId(Uri uri, long id) {
342         try (Cursor c = InstrumentationRegistry.getTargetContext().getContentResolver().query(uri,
343                 new String[] { MediaColumns._ID }, null, null)) {
344             while (c.moveToNext()) {
345                 if (c.getLong(0) == id) return true;
346             }
347         }
348         return false;
349     }
350 
getRawFile(Uri uri)351     public static File getRawFile(Uri uri) throws Exception {
352         final String res = ProviderTestUtils.executeShellCommand("content query --uri " + uri
353                 + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
354                 + " --projection _data",
355                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
356         final int i = res.indexOf("_data=");
357         if (i >= 0) {
358             return new File(res.substring(i + 6));
359         } else {
360             throw new FileNotFoundException("Failed to find _data for " + uri + "; found " + res);
361         }
362     }
363 
getRawFileHash(File file)364     public static String getRawFileHash(File file) throws Exception {
365         final String res = ProviderTestUtils.executeShellCommand(
366                 "sha1sum " + file.getAbsolutePath(),
367                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
368         if (Pattern.matches("[0-9a-fA-F]{40}.+", res)) {
369             return res.substring(0, 40);
370         } else {
371             throw new FileNotFoundException("Failed to find hash for " + file + "; found " + res);
372         }
373     }
374 
getRelativeFile(Uri uri)375     public static File getRelativeFile(Uri uri) throws Exception {
376         final String path = getRawFile(uri).getAbsolutePath();
377         final Matcher matcher = PATTERN_STORAGE_PATH.matcher(path);
378         if (matcher.find()) {
379             return new File(path.substring(matcher.end()));
380         } else {
381             throw new IllegalArgumentException();
382         }
383     }
384 }
385