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