1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.file.common.testing; 17 18 import static org.robolectric.shadow.api.Shadow.directlyOn; 19 20 import android.app.Application; 21 import android.content.Context; 22 import android.os.Build; 23 import android.os.Environment; 24 import androidx.test.core.app.ApplicationProvider; 25 import java.io.File; 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.List; 29 import org.robolectric.annotation.Implementation; 30 import org.robolectric.annotation.Implements; 31 import org.robolectric.annotation.RealObject; 32 import org.robolectric.shadow.api.Shadow; 33 import org.robolectric.shadows.ShadowStatFs; 34 35 /** Common helper utilities that extend the Robolectric Shadow API. */ 36 public final class ShadowUtils { 37 38 /** 39 * Adds an external dir to the Robolectric {@code Environment} and {@code Context}. In order to 40 * use this method, the test class must be configured to use the custom {@link ShadowContextImpl} 41 * and {@link ShadowEnvironment}. 42 */ addExternalDir(String path, boolean isEmulated, boolean isMounted)43 public static File addExternalDir(String path, boolean isEmulated, boolean isMounted) { 44 File dir = ShadowEnvironment.addExternalDir(path); 45 String storageState = isMounted ? Environment.MEDIA_MOUNTED : Environment.MEDIA_REMOVED; 46 47 ShadowEnvironment.setExternalStorageEmulated(dir, isEmulated); 48 ShadowEnvironment.setExternalStorageState(dir, storageState); 49 50 // The shadow implementation of LOLLIPOP storage APIs doesn't fully handle subdirectories, so 51 // the best we can do is to set the same storage properties on each directory we're adding. 52 ShadowContextImpl shadow = 53 Shadow.extract( 54 ((Application) ApplicationProvider.getApplicationContext()).getBaseContext()); 55 List<File> packageDirs = shadow.addExternalPackageDirs(dir); 56 for (File packageDir : packageDirs) { 57 ShadowEnvironment.setExternalStorageEmulated(packageDir, isEmulated); 58 ShadowEnvironment.setExternalStorageState(packageDir, storageState); 59 } 60 61 // Configure primary storage APIs if this is the first external dir 62 if (shadow.getExternalFilesDirs(null).length == 1) { 63 ShadowEnvironment.setExternalStorageDirectory(dir); 64 ShadowEnvironment.setIsExternalStorageEmulated(isEmulated); 65 ShadowEnvironment.setExternalStorageState(storageState); 66 } 67 68 return dir; 69 } 70 71 /** 72 * Configures the information returned by the Robolectric {@code StatsFs}. 73 * 74 * @param dir The file under which {@code StatFs} should return the specified stats 75 * @param totalBytes Total number of bytes on the filesystem 76 * @param freeBytes Number of unused bytes on the filesystem 77 */ setStatFs(File dir, int totalBytes, int freeBytes)78 public static void setStatFs(File dir, int totalBytes, int freeBytes) { 79 int blockCount = totalBytes / ShadowStatFs.BLOCK_SIZE; 80 int freeBlocks = freeBytes / ShadowStatFs.BLOCK_SIZE; 81 int availableBlocks = freeBlocks; 82 ShadowStatFs.registerStats(dir, blockCount, freeBlocks, availableBlocks); 83 } 84 85 /** Extends the stock Robolectric {@code Context} shadow to support multiple externalFilesDirs. */ 86 @Implements(className = org.robolectric.shadows.ShadowContextImpl.CLASS_NAME) 87 public static class ShadowContextImpl extends org.robolectric.shadows.ShadowContextImpl { 88 @RealObject private Context realObject; 89 90 private final List<File> externalFilesDirs = new ArrayList<>(); 91 private final List<File> externalCacheDirs = new ArrayList<>(); 92 93 // Used to simulate a race condition failure on pre-N devices. See getFilesDir. 94 private boolean getFilesDirRunAlready = false; 95 96 /** 97 * Adds package-private /files and /cache subdirectories to the Robolectric {@code Context} 98 * under the named external storage {@code partition}, then returns those new subdirectories. 99 */ addExternalPackageDirs(File partition)100 List<File> addExternalPackageDirs(File partition) { 101 File filesDir = new File(partition, "Android/data/com.google.android.storage.test/files"); 102 File cacheDir = new File(partition, "Android/data/com.google.android.storage.test/cache"); 103 externalFilesDirs.add(filesDir); 104 externalCacheDirs.add(cacheDir); 105 return Arrays.asList(filesDir, cacheDir); 106 } 107 108 @Override 109 @Implementation getExternalFilesDir(String type)110 public File getExternalFilesDir(String type) { 111 return !externalFilesDirs.isEmpty() ? externalFilesDirs.get(0) : null; 112 } 113 114 @Override 115 @Implementation getExternalFilesDirs(String type)116 public File[] getExternalFilesDirs(String type) { 117 return externalFilesDirs.toArray(new File[externalFilesDirs.size()]); 118 } 119 120 @Implementation getExternalCacheDir()121 public File getExternalCacheDir() { 122 return !externalCacheDirs.isEmpty() ? externalCacheDirs.get(0) : null; 123 } 124 125 @Implementation getExternalCacheDirs()126 public File[] getExternalCacheDirs() { 127 return externalCacheDirs.toArray(new File[externalCacheDirs.size()]); 128 } 129 130 /** 131 * See b/70255835. The first call (or first few calls) of getFilesDir may return null on pre-N 132 * devices. We simulate this by returning null only on the first call here. 133 */ 134 @Implementation getFilesDir()135 public File getFilesDir() { 136 if (getFilesDirRunAlready || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 137 return directlyOn(realObject, ShadowContextImpl.CLASS_NAME, "getFilesDir"); 138 } 139 getFilesDirRunAlready = true; 140 return null; 141 } 142 } 143 144 /** 145 * Extends the stock Robolectric {@code Environment} shadow to better emulate external storage 146 * APIs on lower sdk levels. 147 */ 148 @Implements(Environment.class) 149 public static class ShadowEnvironment extends org.robolectric.shadows.ShadowEnvironment { 150 151 private static File externalStorageDirectory; 152 153 /** 154 * Sets the value returned by {@code getExternalStorageDirectory}, which should be the same path 155 * as the first directory added via {@link ShadowEnvironment#addExternalDir}. This is necessary 156 * because by default, Robolectric returns a fixed value for {@code getExternalStorageDirectory} 157 * that doesn't reflect calls to the other shadow APIs. 158 */ setExternalStorageDirectory(File dir)159 static void setExternalStorageDirectory(File dir) { 160 externalStorageDirectory = dir; 161 } 162 163 @Implementation getExternalStorageDirectory()164 public static File getExternalStorageDirectory() { 165 return externalStorageDirectory; 166 } 167 } 168 ShadowUtils()169 private ShadowUtils() {} 170 } 171