• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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