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