1 /*
2  * Copyright 2022 The Android Open Source Project
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 
17 package androidx.webkit;
18 
19 import android.content.Context;
20 
21 import androidx.annotation.GuardedBy;
22 import androidx.annotation.RequiresFeature;
23 import androidx.webkit.internal.ApiHelperForP;
24 import androidx.webkit.internal.StartupApiFeature;
25 import androidx.webkit.internal.WebViewFeatureInternal;
26 
27 import org.chromium.support_lib_boundary.ProcessGlobalConfigConstants;
28 import org.jspecify.annotations.NonNull;
29 
30 import java.io.File;
31 import java.lang.reflect.Field;
32 import java.util.HashMap;
33 import java.util.concurrent.atomic.AtomicReference;
34 
35 /**
36  * Process Global Configuration for WebView.
37  * <p>
38  * WebView has some process-global configuration parameters that cannot be changed once WebView has
39  * been loaded. This class allows apps to set these parameters.
40  * <p>
41  * If it is used, the configuration should be set and
42  * {@link #apply(androidx.webkit.ProcessGlobalConfig)} should be called prior to
43  * loading WebView into the calling process. Most of the methods in
44  * {@link android.webkit} and {@link androidx.webkit} packages load WebView, so the
45  * configuration should be applied before calling any of these methods.
46  * <p>
47  * The following code configures the data directory suffix that WebView
48  * uses and then applies the configuration. WebView uses this configuration when it is loaded.
49  * <pre class="prettyprint">
50  * ProcessGlobalConfig config = new ProcessGlobalConfig();
51  * config.setDataDirectorySuffix("random_suffix")
52  * ProcessGlobalConfig.apply(config);
53  * </pre>
54  * <p>
55  * {@link ProcessGlobalConfig#apply(androidx.webkit.ProcessGlobalConfig)} can only be called once.
56  * <p>
57  * Only a single thread should access this class at a given time.
58  * <p>
59  * The configuration should be set up as early as possible during application startup, to ensure
60  * that it happens before any other thread can call a method that loads WebView.
61  */
62 public class ProcessGlobalConfig {
63     private static final AtomicReference<HashMap<String, Object>> sProcessGlobalConfig =
64             new AtomicReference<>();
65     private static final Object sLock = new Object();
66     @GuardedBy("sLock")
67     private static boolean sApplyCalled = false;
68     String mDataDirectorySuffix;
69     String mDataDirectoryBasePath;
70     String mCacheDirectoryBasePath;
71     Boolean mPartitionedCookiesEnabled;
72 
73     /**
74      * Creates a {@link ProcessGlobalConfig} object.
75      */
ProcessGlobalConfig()76     public ProcessGlobalConfig() {
77     }
78 
79     /**
80      * Define the directory used to store WebView data for the current process.
81      * <p>
82      * The provided suffix will be used when constructing data and cache
83      * directory paths. If this API is not called, no suffix will be used.
84      * Each directory can be used by only one process in the application. If more
85      * than one process in an app wishes to use WebView, only one process can use
86      * the default directory, and other processes must call this API to define
87      * a unique suffix.
88      * <p>
89      * This means that different processes in the same application cannot directly
90      * share WebView-related data, since the data directories must be distinct.
91      * Applications that use this API may have to explicitly pass data between
92      * processes. For example, login cookies may have to be copied from one
93      * process's cookie jar to the other using {@link android.webkit.CookieManager} if both
94      * processes' WebViews are intended to be logged in.
95      * <p>
96      * Most applications should simply ensure that all components of the app
97      * that rely on WebView are in the same process, to avoid needing multiple
98      * data directories. The {@link android.webkit.WebView#disableWebView} method can be used to
99      * ensure that the other processes do not use WebView by accident in this case.
100      * <p>
101      * This is a compatibility method for
102      * {@link android.webkit.WebView#setDataDirectorySuffix(String)}
103      *
104      * @param context a Context to access application assets This value cannot be null.
105      * @param suffix The directory name suffix to be used for the current
106      *               process. Must not contain a path separator and should not be empty.
107      * @return the ProcessGlobalConfig that has the value set to allow chaining of setters
108      * @throws UnsupportedOperationException if underlying WebView does not support the use of
109      *                                       the method.
110      * @throws IllegalArgumentException if the suffix contains a path separator or is empty.
111      */
112     @RequiresFeature(name = WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX,
113             enforcement =
114                     "androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
setDataDirectorySuffix(@onNull Context context, @NonNull String suffix)115     public @NonNull ProcessGlobalConfig setDataDirectorySuffix(@NonNull Context context,
116             @NonNull String suffix) {
117         final StartupApiFeature.P feature =
118                 WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
119         if (!feature.isSupported(context)) {
120             throw WebViewFeatureInternal.getUnsupportedOperationException();
121         }
122         if (suffix.equals("")) {
123             throw new IllegalArgumentException("Suffix cannot be an empty string");
124         }
125         if (suffix.indexOf(File.separatorChar) >= 0) {
126             throw new IllegalArgumentException("Suffix " + suffix
127                     + " contains a path separator");
128         }
129         mDataDirectorySuffix = suffix;
130         return this;
131     }
132 
133     /**
134      * Set the base directories that WebView will use for the current process.
135      * <p>
136      * If this method is not used, WebView uses the default base paths defined by the Android
137      * framework.
138      * <p>
139      * WebView will create and use a subdirectory under each of the base paths supplied to this
140      * method.
141      * <p>
142      * This method can be used in conjunction with {@link #setDataDirectorySuffix(Context, String)}.
143      * A different subdirectory is created for each suffix.
144      * <p>
145      * The base paths must be absolute paths.
146      * <p>
147      * The data directory must not be under the Android cache directory, as Android may delete
148      * cache files when disk space is low and WebView may not function properly if this occurs.
149      * Refer to
150      * <a href="https://developer.android.com/training/data-storage/app-specific#internal-remove-cache">this</a>
151      * link.
152      * <p>
153      * If the specified directories already exist then they must be readable and writable by the
154      * current process. If they do not already exist, WebView will attempt to create them during
155      * initialization, along with any missing parent directories. In such a case, the directory
156      * in which WebView creates missing directories must be readable and writable by the
157      * current process.
158      *
159      * @param context a Context to access application assets. This value cannot be null.
160      * @param dataDirectoryBasePath the absolute base path for the WebView data directory.
161      * @param cacheDirectoryBasePath the absolute base path for the WebView cache directory.
162      * @return the ProcessGlobalConfig that has the value set to allow chaining of setters
163      * @throws UnsupportedOperationException if underlying WebView does not support the use of
164      *                                       the method.
165      * @throws IllegalArgumentException if the paths supplied do not have the right permissions
166      */
167     @SuppressWarnings("StreamFiles")
168     @RequiresFeature(name =
169             WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS,
170             enforcement =
171                     "androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
setDirectoryBasePaths(@onNull Context context, @NonNull File dataDirectoryBasePath, @NonNull File cacheDirectoryBasePath)172     public @NonNull ProcessGlobalConfig setDirectoryBasePaths(@NonNull Context context,
173             @NonNull File dataDirectoryBasePath, @NonNull File cacheDirectoryBasePath) {
174         final StartupApiFeature.NoFramework feature =
175                 WebViewFeatureInternal.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATH;
176         if (!feature.isSupported(context)) {
177             throw WebViewFeatureInternal.getUnsupportedOperationException();
178         }
179         if (!dataDirectoryBasePath.isAbsolute()) {
180             throw new IllegalArgumentException("dataDirectoryBasePath must be a non-empty absolute"
181                     + " path");
182         }
183         if (!cacheDirectoryBasePath.isAbsolute()) {
184             throw new IllegalArgumentException("cacheDirectoryBasePath must be a non-empty absolute"
185                     + " path");
186         }
187         mDataDirectoryBasePath = dataDirectoryBasePath.getAbsolutePath();
188         mCacheDirectoryBasePath = cacheDirectoryBasePath.getAbsolutePath();
189         return this;
190     }
191 
192     /**
193      * Configures whether partitioned cookies should be enabled or not. Refer to
194      * <a href="https://github.com/privacycg/CHIPS">this</a>
195      * link for more details.
196      *
197      * <p>
198      * This is enabled for WebView M114 and above.
199      */
200     @RequiresFeature(name = WebViewFeature.STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES,
201             enforcement =
202                     "androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
setPartitionedCookiesEnabled( @onNull Context context, boolean isEnabled)203     public @NonNull ProcessGlobalConfig setPartitionedCookiesEnabled(
204             @NonNull Context context, boolean isEnabled) {
205         final StartupApiFeature.NoFramework feature =
206                 WebViewFeatureInternal.STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES;
207         if (!feature.isSupported(context)) {
208             throw WebViewFeatureInternal.getUnsupportedOperationException();
209         }
210         mPartitionedCookiesEnabled = isEnabled;
211         return this;
212     }
213 
214     /**
215      * Applies the configuration to be used by WebView on loading.
216      * <p>
217      * This method can only be called once.
218      * <p>
219      * Calling this method will not cause WebView to be loaded and will not block the calling
220      * thread.
221      *
222      * @param config the config to be applied
223      * @throws IllegalStateException if WebView has already been initialized
224      *                               in the current process or if this method was called before
225      */
apply(@onNull ProcessGlobalConfig config)226     public static void apply(@NonNull ProcessGlobalConfig config) {
227         // TODO(crbug.com/1355297): We can check if we are storing the config in the place that
228         //  WebView is going to look for it, and throw if they are not the same.
229         //  For this, we would need to reflect into Android Framework internals to get
230         //  ActivityThread.currentApplication().getClassLoader() and see if it is the same as
231         //  this.getClass().getClassLoader(). This would add reflection that we might not add a
232         //  framework API for. Once we know what framework path we will take for
233         //  ProcessGlobalConfig, revisit this.
234         synchronized (sLock) {
235             if (sApplyCalled) {
236                 throw new IllegalStateException("ProcessGlobalConfig#apply was "
237                         + "called more than once, which is an illegal operation. The configuration "
238                         + "settings provided by ProcessGlobalConfig take effect only once, when "
239                         + "WebView is first loaded into the current process. Every process should "
240                         + "only ever create a single instance of ProcessGlobalConfig and apply it "
241                         + "once, before any calls to android.webkit APIs, such as during early app "
242                         + "startup."
243                 );
244             }
245             sApplyCalled = true;
246         }
247         HashMap<String, Object> configMap = new HashMap<>();
248         if (webViewCurrentlyLoaded()) {
249             throw new IllegalStateException("WebView has already been loaded in the current "
250                     + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
251                     + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
252                     + "calls to android.webkit APIs, such as during early app startup.");
253         }
254         if (config.mDataDirectorySuffix != null) {
255             final StartupApiFeature.P feature =
256                     WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
257             if (feature.isSupportedByFramework()) {
258                 ApiHelperForP.setDataDirectorySuffix(config.mDataDirectorySuffix);
259             } else {
260                 configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_SUFFIX,
261                         config.mDataDirectorySuffix);
262             }
263         }
264         if (config.mDataDirectoryBasePath != null) {
265             configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_BASE_PATH,
266                     config.mDataDirectoryBasePath);
267         }
268         if (config.mCacheDirectoryBasePath != null) {
269             configMap.put(ProcessGlobalConfigConstants.CACHE_DIRECTORY_BASE_PATH,
270                     config.mCacheDirectoryBasePath);
271         }
272         if (config.mPartitionedCookiesEnabled != null) {
273             configMap.put(ProcessGlobalConfigConstants.CONFIGURE_PARTITIONED_COOKIES,
274                     config.mPartitionedCookiesEnabled);
275         }
276         if (!sProcessGlobalConfig.compareAndSet(null, configMap)) {
277             throw new RuntimeException("Attempting to set ProcessGlobalConfig"
278                     + "#sProcessGlobalConfig when it was already set");
279         }
280     }
281 
webViewCurrentlyLoaded()282     private static boolean webViewCurrentlyLoaded() {
283         // TODO(crbug.com/1355297): This is racy but it is the best we can do for now since we can't
284         //  access the lock for sProviderInstance in WebView. Evaluate a framework path for
285         //  ProcessGlobalConfig.
286         try {
287             Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
288             Field providerInstanceField =
289                     webViewFactoryClass.getDeclaredField("sProviderInstance");
290             providerInstanceField.setAccessible(true);
291             return providerInstanceField.get(null) != null;
292         } catch (Exception e) {
293             // This means WebViewFactory was not found or sProviderInstance was not found within
294             // the class. If that is true, WebView doesn't seem to be loaded.
295             return false;
296         }
297     }
298 }
299