• 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.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