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