1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; 4 import static android.os.Build.VERSION_CODES.KITKAT; 5 import static android.os.Build.VERSION_CODES.LOLLIPOP; 6 import static android.os.Build.VERSION_CODES.M; 7 8 import android.os.Environment; 9 import java.io.File; 10 import java.io.IOException; 11 import java.nio.file.Files; 12 import java.nio.file.Path; 13 import java.util.ArrayList; 14 import java.util.HashMap; 15 import java.util.List; 16 import java.util.Map; 17 import org.robolectric.RuntimeEnvironment; 18 import org.robolectric.annotation.Implementation; 19 import org.robolectric.annotation.Implements; 20 import org.robolectric.annotation.Resetter; 21 import org.robolectric.util.ReflectionHelpers; 22 23 @Implements(Environment.class) 24 public class ShadowEnvironment { 25 private static String externalStorageState = Environment.MEDIA_REMOVED; 26 private static final Map<File, Boolean> STORAGE_EMULATED = new HashMap<>(); 27 private static final Map<File, Boolean> STORAGE_REMOVABLE = new HashMap<>(); 28 private static boolean sIsExternalStorageEmulated; 29 private static Path tmpExternalFilesDirBase; 30 private static final List<File> externalDirs = new ArrayList<>(); 31 private static Map<Path, String> storageState = new HashMap<>(); 32 33 static Path EXTERNAL_CACHE_DIR; 34 static Path EXTERNAL_FILES_DIR; 35 36 @Implementation getExternalStorageState()37 protected static String getExternalStorageState() { 38 return externalStorageState; 39 } 40 41 /** 42 * Sets the return value of {@link #getExternalStorageState()}. 43 * 44 * @param externalStorageState Value to return from {@link #getExternalStorageState()}. 45 */ setExternalStorageState(String externalStorageState)46 public static void setExternalStorageState(String externalStorageState) { 47 ShadowEnvironment.externalStorageState = externalStorageState; 48 } 49 50 /** 51 * Sets the return value of {@link #isExternalStorageEmulated()}. 52 * 53 * @param emulated Value to return from {@link #isExternalStorageEmulated()}. 54 */ setIsExternalStorageEmulated(boolean emulated)55 public static void setIsExternalStorageEmulated(boolean emulated) { 56 ShadowEnvironment.sIsExternalStorageEmulated = emulated; 57 } 58 59 @Implementation getExternalStorageDirectory()60 protected static File getExternalStorageDirectory() { 61 if (!exists(EXTERNAL_CACHE_DIR)) EXTERNAL_CACHE_DIR = RuntimeEnvironment.getTempDirectory().create("external-cache"); 62 return EXTERNAL_CACHE_DIR.toFile(); 63 } 64 65 @Implementation getExternalStoragePublicDirectory(String type)66 protected static File getExternalStoragePublicDirectory(String type) { 67 if (!exists(EXTERNAL_FILES_DIR)) EXTERNAL_FILES_DIR = RuntimeEnvironment.getTempDirectory().create("external-files"); 68 if (type == null) return EXTERNAL_FILES_DIR.toFile(); 69 Path path = EXTERNAL_FILES_DIR.resolve(type); 70 try { 71 Files.createDirectories(path); 72 } catch (IOException e) { 73 throw new RuntimeException(e); 74 } 75 return path.toFile(); 76 } 77 78 @Resetter reset()79 public static void reset() { 80 81 EXTERNAL_CACHE_DIR = null; 82 EXTERNAL_FILES_DIR = null; 83 84 STORAGE_EMULATED.clear(); 85 STORAGE_REMOVABLE.clear(); 86 87 storageState = new HashMap<>(); 88 externalDirs.clear(); 89 90 sIsExternalStorageEmulated = false; 91 } 92 exists(Path path)93 private static boolean exists(Path path) { 94 return path != null && Files.exists(path); 95 } 96 97 @Implementation isExternalStorageRemovable()98 protected static boolean isExternalStorageRemovable() { 99 final Boolean exists = STORAGE_REMOVABLE.get(getExternalStorageDirectory()); 100 return exists != null ? exists : false; 101 } 102 103 @Implementation(minSdk = KITKAT) getStorageState(File directory)104 protected static String getStorageState(File directory) { 105 Path directoryPath = directory.toPath(); 106 for (Map.Entry<Path, String> entry : storageState.entrySet()) { 107 if (directoryPath.startsWith(entry.getKey())) { 108 return entry.getValue(); 109 } 110 } 111 return null; 112 } 113 114 @Implementation(minSdk = LOLLIPOP) getExternalStorageState(File directory)115 protected static String getExternalStorageState(File directory) { 116 Path directoryPath = directory.toPath(); 117 for (Map.Entry<Path, String> entry : storageState.entrySet()) { 118 if (directoryPath.startsWith(entry.getKey())) { 119 return entry.getValue(); 120 } 121 } 122 return null; 123 } 124 125 @Implementation(minSdk = LOLLIPOP) isExternalStorageRemovable(File path)126 protected static boolean isExternalStorageRemovable(File path) { 127 final Boolean exists = STORAGE_REMOVABLE.get(path); 128 return exists != null ? exists : false; 129 } 130 131 @Implementation(minSdk = LOLLIPOP) isExternalStorageEmulated(File path)132 protected static boolean isExternalStorageEmulated(File path) { 133 final Boolean emulated = STORAGE_EMULATED.get(path); 134 return emulated != null ? emulated : false; 135 } 136 137 @Implementation isExternalStorageEmulated()138 protected static boolean isExternalStorageEmulated() { 139 return sIsExternalStorageEmulated; 140 } 141 142 /** 143 * Sets the "isRemovable" flag of a particular file. 144 * 145 * @param file Target file. 146 * @param isRemovable True if the filesystem is removable. 147 */ setExternalStorageRemovable(File file, boolean isRemovable)148 public static void setExternalStorageRemovable(File file, boolean isRemovable) { 149 STORAGE_REMOVABLE.put(file, isRemovable); 150 } 151 152 /** 153 * Sets the "isEmulated" flag of a particular file. 154 * 155 * @param file Target file. 156 * @param isEmulated True if the filesystem is emulated. 157 */ setExternalStorageEmulated(File file, boolean isEmulated)158 public static void setExternalStorageEmulated(File file, boolean isEmulated) { 159 STORAGE_EMULATED.put(file, isEmulated); 160 } 161 162 /** 163 * Adds a directory to list returned by {@link ShadowUserEnvironment#getExternalDirs()}. 164 * 165 * @param path the external dir to add 166 */ addExternalDir(String path)167 public static File addExternalDir(String path) { 168 Path externalFileDir; 169 if (path == null) { 170 externalFileDir = null; 171 } else { 172 try { 173 if (tmpExternalFilesDirBase == null) { 174 tmpExternalFilesDirBase = RuntimeEnvironment.getTempDirectory().create("external-files-base"); 175 } 176 externalFileDir = tmpExternalFilesDirBase.resolve(path); 177 Files.createDirectories(externalFileDir); 178 externalDirs.add(externalFileDir.toFile()); 179 } catch (IOException e) { 180 throw new RuntimeException("Could not create external files dir", e); 181 } 182 } 183 184 if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR1 185 && RuntimeEnvironment.getApiLevel() < KITKAT) { 186 if (externalDirs.size() == 1 && externalFileDir != null) { 187 Environment.UserEnvironment userEnvironment = 188 ReflectionHelpers.getStaticField(Environment.class, "sCurrentUser"); 189 ReflectionHelpers.setField( 190 userEnvironment, "mExternalStorageAndroidData", externalFileDir.toFile()); 191 } 192 } else if (RuntimeEnvironment.getApiLevel() >= KITKAT && RuntimeEnvironment.getApiLevel() < M) { 193 Environment.UserEnvironment userEnvironment = 194 ReflectionHelpers.getStaticField(Environment.class, "sCurrentUser"); 195 ReflectionHelpers.setField(userEnvironment, "mExternalDirsForApp", 196 externalDirs.toArray(new File[externalDirs.size()])); 197 } 198 199 if (externalFileDir == null) { 200 return null; 201 } 202 return externalFileDir.toFile(); 203 } 204 205 /** 206 * Sets the {@link #getExternalStorageState(File)} for given directory. 207 * 208 * @param externalStorageState Value to return from {@link #getExternalStorageState(File)}. 209 */ setExternalStorageState(File directory, String state)210 public static void setExternalStorageState(File directory, String state) { 211 storageState.put(directory.toPath(), state); 212 } 213 214 @Implements(className = "android.os.Environment$UserEnvironment", isInAndroidSdk = false, 215 minSdk = JELLY_BEAN_MR1) 216 public static class ShadowUserEnvironment { 217 218 @Implementation(minSdk = M) getExternalDirs()219 protected File[] getExternalDirs() { 220 return externalDirs.toArray(new File[externalDirs.size()]); 221 } 222 } 223 } 224