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.backends; 17 18 import android.annotation.TargetApi; 19 import android.content.Context; 20 import android.os.Build; 21 import android.os.Environment; 22 import android.os.StatFs; 23 import android.os.SystemClock; 24 import android.util.Log; 25 import java.io.File; 26 import java.io.IOException; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collections; 30 import java.util.List; 31 32 /** 33 * Provides access to high-level information about the Android file environment. These utilities are 34 * neither intended nor available for use outside of the MobStore library implementation. 35 */ 36 public final class AndroidFileEnvironment { 37 38 private static final String TAG = "AndroidFileEnvironment"; 39 40 /** Returns all {@code dirs} that are currently mounted with full read/write access. */ getMountedExternalDirs(List<File> dirs)41 public static List<File> getMountedExternalDirs(List<File> dirs) { 42 List<File> result = new ArrayList<>(); 43 for (File dir : dirs) { 44 if (dir == null) { 45 continue; 46 } 47 String state = getStorageState(dir); 48 if (Log.isLoggable(TAG, Log.DEBUG)) { 49 Log.d(TAG, String.format("External storage: [%s] is [%s]", dir.getAbsolutePath(), state)); 50 } 51 if (Environment.MEDIA_MOUNTED.equals(state)) { 52 result.add(dir); 53 } 54 } 55 return result; 56 } 57 58 /** 59 * Returns the current state of the shared/external storage media at the given path. This is a 60 * private API to support {@link Environment#getStorageState(File)} across all sdk levels. 61 */ getStorageState(File dir)62 private static String getStorageState(File dir) { 63 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 64 return getStorageStateKK(dir); 65 } else { 66 return getStorageStateICS(dir); 67 } 68 } 69 70 /** Private API to support {@link #getStorageState} on sdk KK and higher. */ 71 @TargetApi(Build.VERSION_CODES.KITKAT) getStorageStateKK(File dir)72 private static String getStorageStateKK(File dir) { 73 return Environment.getStorageState(dir); 74 } 75 76 /** Private API to support {@link #getStorageState} on lower sdk levels. */ getStorageStateICS(File dir)77 private static String getStorageStateICS(File dir) { 78 // Implementation taken directly from EnvironmentCompat#getStorageState. Note that JB and below 79 // only support one external storage partition, thus can only return a meaningful value for a 80 // directory under that partition. 81 try { 82 String dirPath = dir.getCanonicalPath(); 83 String externalPath = Environment.getExternalStorageDirectory().getCanonicalPath(); 84 if (dirPath.startsWith(externalPath)) { 85 return Environment.getExternalStorageState(); 86 } 87 } catch (IOException e) { 88 Log.w(TAG, "Failed to resolve canonical path", e); 89 } 90 return "unknown"; // == Environment.MEDIA_UNKNOWN, which isn't available below KK 91 } 92 93 /** 94 * Returns all available non-emulated external cache directories. This method does not guarantee 95 * that the returned paths are mounted. 96 */ getNonEmulatedExternalCacheDirs(Context context)97 public static List<File> getNonEmulatedExternalCacheDirs(Context context) { 98 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 99 return getNonEmulatedExternalCacheDirsLP(context); 100 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 101 return getNonEmulatedExternalCacheDirsKK(context); 102 } else { 103 return getNonEmulatedExternalCacheDirsICS(context); 104 } 105 } 106 107 /** 108 * Private API; please use {@link #getNonEmulatedExternalCacheDirs} directly. This implementation 109 * of {@link #getNonEmulatedExternalCacheDirs} uses the new APIs available on LOLLIPOP and later 110 * in order to query each external storage partition for emulation. 111 */ 112 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNonEmulatedExternalCacheDirsLP(Context context)113 private static List<File> getNonEmulatedExternalCacheDirsLP(Context context) { 114 List<File> result = new ArrayList<>(); 115 116 for (File dir : Arrays.asList(context.getExternalCacheDirs())) { 117 try { 118 if (dir != null && !Environment.isExternalStorageEmulated(dir)) { 119 result.add(dir); 120 } 121 } catch (IllegalArgumentException e) { 122 // NOTE: on some devices and API levels, Environment.isExternalStorageEmulated(File) 123 // will throw an exception if the partition is not mounted. In any case this means the dir 124 // is unavailable, so we can continue past it. See b/29833349 for more info. 125 // TODO(b/64078707): enable Robolectric to throw exceptions here to increase test coverage 126 Log.w( 127 TAG, 128 String.format("isExternalStorageEmulated(File) failed for [%s]", dir.getAbsolutePath()), 129 e); 130 continue; 131 } 132 } 133 134 return result; 135 } 136 137 /** 138 * Private API; please use {@link #getNonEmulatedExternalCacheDirs} directly. This implementation 139 * of {@link #getNonEmulatedExternalCacheDirs} supports lower SDK levels and can't query secondary 140 * partitions for emulation. However, only the primary partition can be emulated on such devices. 141 */ 142 @TargetApi(Build.VERSION_CODES.KITKAT) getNonEmulatedExternalCacheDirsKK(Context context)143 private static List<File> getNonEmulatedExternalCacheDirsKK(Context context) { 144 List<File> result = new ArrayList<>(); 145 146 // If the primary external storage is non-emulated, return it 147 File[] dirs = context.getExternalCacheDirs(); 148 if (!Environment.isExternalStorageEmulated() && dirs[0] != null) { 149 result.add(dirs[0]); 150 } 151 152 // Check secondary storage. We skip the first dir (primary), which we already checked. Secondary 153 // dirs cannot be explicitly checked for emulation because of the API level, but are assumed 154 // to be non-emulated. See {@link https://source.android.com/devices/storage/config-example} and 155 // {@link com.google.android.apps.gmm.shared.util.FileUtil#getNonEmulatedExternalFilesDirKK}. 156 for (int i = 1; i < dirs.length; i++) { 157 if (dirs[i] != null) { 158 result.add(dirs[i]); 159 } 160 } 161 162 return result; 163 } 164 165 /** 166 * Private API; please use {@link #getNonEmulatedExternalCacheDirs} directly. This implementation 167 * supports sdk levels below KitKat, and due to the limited API can only return a single external 168 * storage partition (which may be emulated, in which case none are returned). 169 */ getNonEmulatedExternalCacheDirsICS(Context context)170 private static List<File> getNonEmulatedExternalCacheDirsICS(Context context) { 171 File dir = context.getExternalCacheDir(); 172 if (!Environment.isExternalStorageEmulated() && dir != null) { 173 return Arrays.asList(dir); 174 } 175 return Collections.emptyList(); 176 } 177 178 /** Returns the number of bytes free and available on the file system of {@code dir}. */ getAvailableStorageSpace(File dir)179 public static long getAvailableStorageSpace(File dir) { 180 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 181 return getAvailableStorageSpaceJBMR2(dir); 182 } else { 183 return getAvailableStorageSpaceICS(dir); 184 } 185 } 186 187 /** 188 * Based on cl/189267818. Paraphrased here: 189 * 190 * <p>According to AGSA bug b/30959609 and similar bugs in other 1st party apps, the Context can 191 * return a null filesDir on SDK versions before N. The root cause is a race condition between two 192 * threads that try to initialize the application's directory structure immediately following 193 * installation. One thread waits, and the other returns a null File pointer. The bug is fixed in 194 * Android N. The workaround for older releases is to wait. A short while after failing, the 195 * directory structure is initialized, and the previously failing Context returns a valid File 196 * pointer. If that doesn't work, then the Context must be broken for other reasons. We throw an 197 * IllegalStateException in this case. 198 */ 199 // TODO(b/70255835): rename to not suggest N is safe since bug affects up to and including sdk N getFilesDirWithPreNWorkaround(Context context)200 public static File getFilesDirWithPreNWorkaround(Context context) { 201 File filesDir = context.getFilesDir(); 202 // According to Android docs, this can't happen, but a pre-N bug makes this sometimes return 203 // null. See b/30959609 for details. 204 if (filesDir == null) { 205 // The cause is an internal race condition. Sleep and try again. 206 SystemClock.sleep(100); 207 filesDir = context.getFilesDir(); 208 if (filesDir == null) { 209 throw new IllegalStateException("getFilesDir returned null twice."); 210 } 211 } 212 return filesDir; 213 } 214 215 /** Private API to support {@link #getAvailableStorageSpace} on sdk JB-MR2 and higher. */ 216 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) getAvailableStorageSpaceJBMR2(File dir)217 private static long getAvailableStorageSpaceJBMR2(File dir) { 218 StatFs stat = new StatFs(dir.getPath()); 219 return stat.getAvailableBytes(); 220 } 221 222 /** Private API to support {@link #getAvailableStorageSpace} on lower sdk levels. */ getAvailableStorageSpaceICS(File dir)223 private static long getAvailableStorageSpaceICS(File dir) { 224 StatFs stat = new StatFs(dir.getPath()); 225 return (long) stat.getBlockSize() * stat.getAvailableBlocks(); 226 } 227 228 /** 229 * Returns the data directory of {@code context} in the DirectBoot storage partition. Each call to 230 * this method creates a new instance of {@link Context}, so the result should reused if possible. 231 */ 232 @TargetApi(Build.VERSION_CODES.N) getDeviceProtectedDataDir(Context context)233 public static File getDeviceProtectedDataDir(Context context) { 234 Context dpsContext = context.createDeviceProtectedStorageContext(); 235 File dpsFilesDir = getFilesDirWithPreNWorkaround(dpsContext); 236 File dpsDataDir = dpsFilesDir.getParentFile(); 237 return dpsDataDir; 238 } 239 AndroidFileEnvironment()240 private AndroidFileEnvironment() {} 241 } 242