// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.StrictMode;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.protobuf.InvalidProtocolBufferException;
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.android_webview.common.AwFeatures;
import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.common.Lifetime;
import org.chromium.android_webview.common.PlatformServiceBridge;
import org.chromium.android_webview.common.services.ICrashReceiverService;
import org.chromium.android_webview.common.services.IMetricsBridgeService;
import org.chromium.android_webview.common.services.ServiceConnectionDelayRecorder;
import org.chromium.android_webview.common.services.ServiceHelper;
import org.chromium.android_webview.common.services.ServiceNames;
import org.chromium.android_webview.metrics.AwMetricsLogUploader;
import org.chromium.android_webview.metrics.AwMetricsServiceClient;
import org.chromium.android_webview.metrics.AwNonembeddedUmaReplayer;
import org.chromium.android_webview.metrics.MetricsFilteringDecorator;
import org.chromium.android_webview.policy.AwPolicyProvider;
import org.chromium.android_webview.proto.MetricsBridgeRecords.HistogramRecord;
import org.chromium.android_webview.safe_browsing.AwSafeBrowsingConfigHelper;
import org.chromium.android_webview.supervised_user.AwSupervisedUserUrlClassifier;
import org.chromium.base.BaseSwitches;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.PowerMonitor;
import org.chromium.base.StreamUtil;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TimeUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.component_updater.ComponentLoaderPolicyBridge;
import org.chromium.components.component_updater.EmbeddedComponentLoader;
import org.chromium.components.metrics.AndroidMetricsFeatures;
import org.chromium.components.metrics.AndroidMetricsLogConsumer;
import org.chromium.components.metrics.AndroidMetricsLogUploader;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.components.policy.CombinedPolicyProvider;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.ChildProcessCreationParams;
import org.chromium.content_public.browser.ChildProcessLauncherHelper;
import org.chromium.ui.display.DisplayAndroidManager;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/** Wrapper for the steps needed to initialize the java and native sides of webview chromium. */
@JNINamespace("android_webview")
@Lifetime.Singleton
public final class AwBrowserProcess {
private static final String TAG = "AwBrowserProcess";
private static final String WEBVIEW_DIR_BASENAME = "webview";
private static final int MINUTES_PER_DAY =
(int) TimeUnit.SECONDS.toMinutes(TimeUtils.SECONDS_PER_DAY);
// To avoid any potential synchronization issues we post all minidump-copying actions to
// the same sequence to be run serially.
private static final TaskRunner sSequencedTaskRunner =
PostTask.createSequencedTaskRunner(TaskTraits.BEST_EFFORT_MAY_BLOCK);
private static String sWebViewPackageName;
private static @ApkType int sApkType;
private static @Nullable String sProcessDataDirSuffix;
/**
* Loads the native library, and performs basic static construction of objects needed
* to run webview in this process. Does not create threads; safe to call from zygote.
* Note: it is up to the caller to ensure this is only called once.
*
* @param processDataDirSuffix The suffix to use when setting the data directory for this
* process; null to use no suffix.
*/
public static void loadLibrary(String processDataDirSuffix) {
loadLibrary(null, null, processDataDirSuffix);
}
/**
* Loads the native library, and performs basic static construction of objects needed to run
* webview in this process. Does not create threads; safe to call from zygote. Note: it is up to
* the caller to ensure this is only called once.
*
* @param processDataDirBasePath The base path to use when setting the data directory for this
* process; null to use default base path.
* @param processCacheDirBasePath The base path to use when setting the cache directory for this
* process; null to use default base path.
* @param processDataDirSuffix The suffix to use when setting the data directory for this
* process; null to use no suffix.
*/
public static void loadLibrary(
String processDataDirBasePath,
String processCacheDirBasePath,
String processDataDirSuffix) {
LibraryLoader.getInstance().setLibraryProcessType(LibraryProcessType.PROCESS_WEBVIEW);
sProcessDataDirSuffix = processDataDirSuffix;
if (processDataDirSuffix == null) {
PathUtils.setPrivateDirectoryPath(
processDataDirBasePath,
processCacheDirBasePath,
WEBVIEW_DIR_BASENAME,
"WebView");
} else {
String processDataDirName = WEBVIEW_DIR_BASENAME + "_" + processDataDirSuffix;
PathUtils.setPrivateDirectoryPath(
processDataDirBasePath,
processCacheDirBasePath,
processDataDirName,
processDataDirName);
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
LibraryLoader.getInstance().loadNow();
// Switch the command line implementation from Java to native.
// It's okay for the WebView to do this before initialization because we have
// setup the JNI bindings by this point.
LibraryLoader.getInstance().switchCommandLineForWebView();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/**
* Configures child process launcher. This is required only if child services are used in
* WebView.
*/
public static void configureChildProcessLauncher() {
final boolean isExternalService = true;
final boolean bindToCaller = true;
final boolean ignoreVisibilityForImportance = true;
ChildProcessCreationParams.set(
getWebViewPackageName(),
/* privilegedServicesName= */ null,
getWebViewPackageName(),
/* sandboxedServicesName= */ null,
isExternalService,
LibraryProcessType.PROCESS_WEBVIEW_CHILD,
bindToCaller,
ignoreVisibilityForImportance);
}
/**
* Configures child process launcher for tests. This is required for multiprocess mode to ensure
* the process type of the child process is WebView, but many of the other fields from
* configureChildProcessLauncher do not work in testing, so tests need a customized version of
* that method.
*/
public static void configureChildProcessLauncherForTesting() {
final boolean isExternalService = false;
final boolean bindToCaller = false;
final boolean ignoreVisibilityForImportance = false;
ChildProcessCreationParams.set(
ContextUtils.getApplicationContext().getPackageName(),
/* privilegedServicesName= */ null,
ContextUtils.getApplicationContext().getPackageName(),
/* sandboxedServicesName= */ null,
isExternalService,
LibraryProcessType.PROCESS_WEBVIEW_CHILD,
bindToCaller,
ignoreVisibilityForImportance);
}
/**
* Starts the chromium browser process running within this process. Creates threads
* and performs other per-app resource allocations; must not be called from zygote.
* Note: it is up to the caller to ensure this is only called once.
*/
public static void start() {
try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped("AwBrowserProcess.start")) {
final Context appContext = ContextUtils.getApplicationContext();
AwBrowserProcessJni.get().setProcessNameCrashKey(ContextUtils.getProcessName());
AwDataDirLock.lock(appContext);
// We must post to the UI thread to cover the case that the user
// has invoked Chromium startup by using the (thread-safe)
// CookieManager rather than creating a WebView.
ThreadUtils.runOnUiThreadBlocking(
() -> {
boolean multiProcess =
CommandLine.getInstance()
.hasSwitch(AwSwitches.WEBVIEW_SANDBOXED_RENDERER);
if (multiProcess) {
PostTask.postTask(
TaskTraits.BEST_EFFORT,
() -> {
ChildProcessLauncherHelper.warmUpOnAnyThread(
appContext, true);
});
}
configureDisplayAndroidManager();
// The policies are used by browser startup, so we need to register the
// policy providers before starting the browser process. This only registers
// java objects and doesn't need the native library.
CombinedPolicyProvider.get()
.registerProvider(new AwPolicyProvider(appContext));
// Check android settings but only when safebrowsing is enabled.
try (ScopedSysTraceEvent e2 =
ScopedSysTraceEvent.scoped("AwBrowserProcess.maybeEnable")) {
AwSafeBrowsingConfigHelper.maybeEnableSafeBrowsingFromManifest();
}
try (ScopedSysTraceEvent e2 =
ScopedSysTraceEvent.scoped(
"AwBrowserProcess.startBrowserProcessesSync")) {
BrowserStartupController.getInstance()
.startBrowserProcessesSync(
LibraryProcessType.PROCESS_WEBVIEW,
!multiProcess,
/* startGpuProcess= */ false);
}
PowerMonitor.create();
PlatformServiceBridge.getInstance().setSafeBrowsingHandler();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AwContentsLifecycleNotifier.initialize();
}
});
AwSupervisedUserUrlClassifier classifier = AwSupervisedUserUrlClassifier.getInstance();
if (classifier != null) {
classifier.checkIfNeedRestrictedContentBlocking();
}
}
PostTask.postTask(
TaskTraits.BEST_EFFORT,
() -> {
RecordHistogram.recordSparseHistogram(
"Android.PlayServices.Version",
PlatformServiceBridge.getInstance().getGmsVersionCode());
});
}
public static void setWebViewPackageName(String webViewPackageName) {
assert sWebViewPackageName == null || sWebViewPackageName.equals(webViewPackageName);
sWebViewPackageName = webViewPackageName;
}
public static String getWebViewPackageName() {
if (sWebViewPackageName == null) return ""; // May be null in testing.
return sWebViewPackageName;
}
public static void setProcessDataDirSuffixForTesting(@Nullable String processDataDirSuffix) {
sProcessDataDirSuffix = processDataDirSuffix;
}
@Nullable
public static String getProcessDataDirSuffix() {
return sProcessDataDirSuffix;
}
public static void initializeApkType(ApplicationInfo info) {
if (info.sharedLibraryFiles != null && info.sharedLibraryFiles.length > 0) {
// Only Trichrome uses shared library files.
sApkType = ApkType.TRICHROME;
} else if (info.className.toLowerCase(Locale.ROOT).contains("monochrome")) {
// Only Monochrome has "monochrome" in the application class name.
sApkType = ApkType.MONOCHROME;
} else {
// Everything else must be standalone.
sApkType = ApkType.STANDALONE;
}
}
/** Returns the WebView APK type. */
@CalledByNative
public static @ApkType int getApkType() {
return sApkType;
}
/** Trigger minidump copying, which in turn triggers minidump uploading. */
@CalledByNative
private static void triggerMinidumpUploading() {
handleMinidumpsAndSetMetricsConsent(/* updateMetricsConsent= */ false);
}
/**
* Trigger minidump uploading, and optionaly also update the metrics-consent value depending on
* whether the Android Checkbox is toggled on.
* @param updateMetricsConsent whether to update the metrics-consent value to represent the
* Android Checkbox toggle.
*/
public static void handleMinidumpsAndSetMetricsConsent(final boolean updateMetricsConsent) {
try (ScopedSysTraceEvent e1 =
ScopedSysTraceEvent.scoped(
"AwBrowserProcess.handleMinidumpsAndSetMetricsConsent")) {
final boolean enableMinidumpUploadingForTesting =
CommandLine.getInstance()
.hasSwitch(BaseSwitches.ENABLE_CRASH_REPORTER_FOR_TESTING);
if (enableMinidumpUploadingForTesting) {
handleMinidumps(/* userApproved= */ true);
}
PlatformServiceBridge.getInstance()
.queryMetricsSetting(
enabled -> {
ThreadUtils.assertOnUiThread();
boolean userApproved = Boolean.TRUE.equals(enabled);
if (updateMetricsConsent) {
AwMetricsServiceClient.setConsentSetting(userApproved);
}
if (!enableMinidumpUploadingForTesting) {
handleMinidumps(userApproved);
}
});
}
}
private static String getCrashUuid(File file) {
String fileName = file.getName();
// crash report uuid is the minidump file name without any extensions.
int firstDotIndex = fileName.indexOf('.');
if (firstDotIndex == -1) {
firstDotIndex = fileName.length();
}
return fileName.substring(0, firstDotIndex);
}
private static void deleteMinidumps(final File[] minidumpFiles) {
for (File minidump : minidumpFiles) {
if (!minidump.delete()) {
Log.w(TAG, "Couldn't delete file " + minidump.getAbsolutePath());
}
}
}
private static void transmitMinidumps(
final File[] minidumpFiles,
final Map<String, Map<String, String>> crashesInfoMap,
final ICrashReceiverService service) {
// Pass file descriptors pointing to our minidumps to the
// minidump-copying service, allowing it to copy contents of the
// minidumps to WebView's data directory.
// Delete the app filesystem references to the minidumps after passing
// the file descriptors so that we avoid trying to copy the minidumps
// again if anything goes wrong. This makes sense given that a failure
// to copy a file usually means that retrying won't succeed either,
// because e.g. the disk is full, or the file system is corrupted.
int fileCount = minidumpFiles.length;
// TODO(crbug.com/40883324): We should limit the number of crashes we upload in
// order to not use too much data, and in order to minimize the chance of exhausting
// file descriptors (https://crbug.com/1399777).
ParcelFileDescriptor[] minidumpFds = new ParcelFileDescriptor[fileCount];
Map<String, String>[] crashInfos = new Map[fileCount];
for (int i = 0; i < fileCount; ++i) {
File file = minidumpFiles[i];
ParcelFileDescriptor p = null;
try {
p = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
} catch (IOException e) {
}
minidumpFds[i] = p;
crashInfos[i] = crashesInfoMap.get(getCrashUuid(file));
}
try {
// AIDL does not support arrays of objects, so use a List here.
service.transmitCrashes(minidumpFds, Arrays.asList(crashInfos));
} catch (Exception e) {
// Exception can be RemoteException, or "RuntimeException: Too many open files".
// https://crbug.com/1399777
// TODO(gsennton): add a UMA metric here to ensure we aren't losing
// too many minidumps because of this.
}
deleteMinidumps(minidumpFiles);
for (ParcelFileDescriptor fd : minidumpFds) {
StreamUtil.closeQuietly(fd);
}
}
/**
* Pass Minidumps to a separate Service declared in the WebView provider package.
* That Service will copy the Minidumps to its own data directory - at which point we can delete
* our copies in the app directory.
* @param userApproved whether we have user consent to upload crash data - if we do, copy the
* minidumps, if we don't, delete them.
*/
public static void handleMinidumps(boolean userApproved) {
sSequencedTaskRunner.postTask(() -> handleMinidumpsInternal(userApproved));
}
private static void handleMinidumpsInternal(final boolean userApproved) {
try {
final Context appContext = ContextUtils.getApplicationContext();
final File cacheDir = new File(PathUtils.getCacheDirectory());
final CrashFileManager crashFileManager = new CrashFileManager(cacheDir);
// The lifecycle of a minidump in the app directory is very simple:
// foo.dmpNNNNN --
// where NNNNN is a Process ID (PID) -- gets created, and is either deleted
// or
// copied over to the shared crash directory for all WebView-using apps.
Map<String, Map<String, String>> crashesInfoMap =
crashFileManager.importMinidumpsCrashKeys();
final File[] minidumpFiles = crashFileManager.getCurrentMinidumpsSansLogcat();
if (minidumpFiles.length == 0) return;
// Delete the minidumps if the user doesn't allow crash data uploading.
if (!userApproved) {
deleteMinidumps(minidumpFiles);
return;
}
final Intent intent = new Intent();
intent.setClassName(getWebViewPackageName(), ServiceNames.CRASH_RECEIVER_SERVICE);
ServiceConnection connection =
new ServiceConnection() {
private boolean mHasConnected;
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
if (mHasConnected) return;
mHasConnected = true;
// onServiceConnected is called on the UI thread, so punt
// this back to the background thread.
sSequencedTaskRunner.postTask(
() -> {
transmitMinidumps(
minidumpFiles,
crashesInfoMap,
ICrashReceiverService.Stub.asInterface(service));
appContext.unbindService(this);
});
}
@Override
public void onServiceDisconnected(ComponentName className) {}
};
if (!ServiceHelper.bindService(
appContext, intent, connection, Context.BIND_AUTO_CREATE)) {
Log.w(TAG, "Could not bind to Minidump-copying Service " + intent);
}
} catch (RuntimeException e) {
// We don't want to crash the app if we hit an unexpected exception during
// minidump uploading as this could potentially put the app into a
// persistently bad state.
// Just log it.
Log.e(TAG, "Exception during minidump uploading process!", e);
}
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
@IntDef({
TransmissionResult.SUCCESS,
TransmissionResult.MALFORMED_PROTOBUF,
TransmissionResult.REMOTE_EXCEPTION
})
private @interface TransmissionResult {
int SUCCESS = 0;
int MALFORMED_PROTOBUF = 1;
int REMOTE_EXCEPTION = 2;
int COUNT = 3;
}
private static void logTransmissionResult(@TransmissionResult int sample) {
RecordHistogram.recordEnumeratedHistogram(
"Android.WebView.NonEmbeddedMetrics.TransmissionResult",
sample,
TransmissionResult.COUNT);
}
/**
* Record very long times UMA histogram up to 4 days.
*
* @param name histogram name.
* @param time time sample in millis.
*/
private static void recordVeryLongTimesHistogram(String name, long time) {
long timeMins = TimeUnit.MILLISECONDS.toMinutes(time);
int sample;
// Safely convert to int to avoid positive or negative overflow.
if (timeMins > Integer.MAX_VALUE) {
sample = Integer.MAX_VALUE;
} else if (timeMins < Integer.MIN_VALUE) {
sample = Integer.MIN_VALUE;
} else {
sample = (int) timeMins;
}
RecordHistogram.recordCustomCountHistogram(name, sample, 1, 4 * MINUTES_PER_DAY, 50);
}
/**
* Connect to {@link org.chromium.android_webview.services.MetricsBridgeService} to retrieve
* any recorded UMA metrics from nonembedded WebView services and transmit them back using
* UMA APIs.
*/
public static void collectNonembeddedMetrics() {
if (ManifestMetadataUtil.isAppOptedOutFromMetricsCollection()) {
Log.d(TAG, "App opted out from metrics collection, not connecting to metrics service");
return;
}
final Intent intent = new Intent();
intent.setClassName(getWebViewPackageName(), ServiceNames.METRICS_BRIDGE_SERVICE);
ServiceConnectionDelayRecorder connection =
new ServiceConnectionDelayRecorder() {
private boolean mHasConnected;
@Override
public void onServiceConnectedImpl(ComponentName className, IBinder service) {
if (mHasConnected) return;
mHasConnected = true;
// onServiceConnected is called on the UI thread, so punt this back to the
// background thread.
PostTask.postTask(
TaskTraits.BEST_EFFORT,
() -> {
sendMetricsToService(service);
ContextUtils.getApplicationContext().unbindService(this);
});
}
@Override
public void onServiceDisconnected(ComponentName className) {}
};
Context appContext = ContextUtils.getApplicationContext();
if (!connection.bind(appContext, intent, Context.BIND_AUTO_CREATE)) {
Log.d(TAG, "Could not bind to MetricsBridgeService " + intent);
}
}
private static void sendMetricsToService(IBinder service) {
try {
IMetricsBridgeService metricsService = IMetricsBridgeService.Stub.asInterface(service);
List<byte[]> data = metricsService.retrieveNonembeddedMetrics();
RecordHistogram.recordCount1000Histogram(
"Android.WebView.NonEmbeddedMetrics.NumHistograms", data.size());
long systemTime = System.currentTimeMillis();
for (byte[] recordData : data) {
HistogramRecord record = HistogramRecord.parseFrom(recordData);
AwNonembeddedUmaReplayer.replayMethodCall(record);
if (record.hasMetadata()) {
long timeRecorded = record.getMetadata().getTimeRecorded();
recordVeryLongTimesHistogram(
"Android.WebView.NonEmbeddedMetrics.HistogramRecordAge",
systemTime - timeRecorded);
}
}
logTransmissionResult(TransmissionResult.SUCCESS);
} catch (InvalidProtocolBufferException e) {
Log.d(TAG, "Malformed metrics log proto", e);
logTransmissionResult(TransmissionResult.MALFORMED_PROTOBUF);
} catch (Exception e) {
// RemoteException, IllegalArgumentException
// (https://crbug.com/1403976)
Log.d(TAG, "Remote Exception in MetricsBridgeService#retrieveMetrics", e);
logTransmissionResult(TransmissionResult.REMOTE_EXCEPTION);
}
}
/**
* Load components files from {@link
* org.chromium.android_webview.services.ComponentsProviderService}.
*/
public static void loadComponents() {
ComponentLoaderPolicyBridge[] componentPolicies =
AwBrowserProcessJni.get().getComponentLoaderPolicies();
// Don't connect to the service if there are no components to load.
if (componentPolicies.length == 0) {
return;
}
EmbeddedComponentLoader loader =
new EmbeddedComponentLoader(Arrays.asList(componentPolicies));
final Intent intent = new Intent();
intent.setClassName(
getWebViewPackageName(), EmbeddedComponentLoader.AW_COMPONENTS_PROVIDER_SERVICE);
loader.connect(intent);
}
/** Initialize the metrics uploader. */
public static void initializeMetricsLogUploader() {
boolean metricServiceEnabledOnlySdkRuntime =
ContextUtils.isSdkSandboxProcess()
&& AwFeatureMap.isEnabled(
AwFeatures.WEBVIEW_USE_METRICS_UPLOAD_SERVICE_ONLY_SDK_RUNTIME);
if (metricServiceEnabledOnlySdkRuntime
|| AwFeatureMap.isEnabled(AwFeatures.WEBVIEW_USE_METRICS_UPLOAD_SERVICE)) {
boolean isAsync =
AwFeatureMap.isEnabled(
AndroidMetricsFeatures.ANDROID_METRICS_ASYNC_METRIC_LOGGING);
AwMetricsLogUploader uploader = new AwMetricsLogUploader(isAsync);
// Open a connection during startup while connecting to other services such as
// ComponentsProviderService and VariationSeedServer to try to avoid spinning the
// nonembedded ":webview_service" twice.
uploader.initialize();
AndroidMetricsLogUploader.setConsumer(new MetricsFilteringDecorator(uploader));
} else {
AndroidMetricsLogConsumer directUploader =
data -> {
PlatformServiceBridge.getInstance().logMetrics(data);
return HttpURLConnection.HTTP_OK;
};
AndroidMetricsLogUploader.setConsumer(new MetricsFilteringDecorator(directUploader));
}
}
private static void configureDisplayAndroidManager() {
DisplayAndroidManager.disableHdrSdrRatioCallback();
}
// Do not instantiate this class.
private AwBrowserProcess() {}
@NativeMethods
interface Natives {
void setProcessNameCrashKey(String processName);
ComponentLoaderPolicyBridge[] getComponentLoaderPolicies();
}
}