blob: d4ff699c88a787b2d4539b76d08aaf96e35a8935 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.contentcapture;
import static android.Manifest.permission.MANAGE_CONTENT_CAPTURE;
import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
import static android.service.contentcapture.ContentCaptureService.setClientState;
import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_DELAY_MS;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_TIMEOUT_MS;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_AUTO_DISCONNECT_TIMEOUT;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_CONFIG;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_THRESHOLD;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_REQUIRED_GROUPS_CONFIG;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_OK;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_SECURITY_EXCEPTION;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_TRUE;
import static android.view.contentcapture.ContentCaptureSession.STATE_DISABLED;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ACCEPT_DATA_SHARE_REQUEST;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_EMPTY_DATA;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_IOEXCEPTION;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_WRITE_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__REJECT_DATA_SHARE_REQUEST;
import static com.android.internal.util.SyncResultReceiver.bundleFor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.app.admin.DevicePolicyManagerInternal;
import android.app.assist.ActivityId;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ActivityPresentationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.provider.Settings;
import android.service.contentcapture.ActivityEvent.ActivityEventType;
import android.service.contentcapture.ContentCaptureServiceInfo;
import android.service.contentcapture.IDataShareCallback;
import android.service.contentcapture.IDataShareReadAdapter;
import android.service.voice.VoiceInteractionManagerInternal;
import android.util.ArraySet;
import android.util.LocalLog;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.contentcapture.ContentCaptureCondition;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureHelper;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.DataRemovalRequest;
import android.view.contentcapture.DataShareRequest;
import android.view.contentcapture.IContentCaptureManager;
import android.view.contentcapture.IContentCaptureOptionsCallback;
import android.view.contentcapture.IDataShareWriteAdapter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AbstractRemoteService;
import com.android.internal.infra.GlobalWhitelistState;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
import com.android.server.contentprotection.ContentProtectionAllowlistManager;
import com.android.server.contentprotection.ContentProtectionConsentManager;
import com.android.server.contentprotection.RemoteContentProtectionService;
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A service used to observe the contents of the screen.
*
* <p>The data collected by this service can be analyzed on-device and combined
* with other sources to provide contextual data in other areas of the system
* such as Autofill.
*/
public class ContentCaptureManagerService extends
AbstractMasterSystemService<ContentCaptureManagerService, ContentCapturePerUserService> {
private static final String TAG = ContentCaptureManagerService.class.getSimpleName();
static final String RECEIVER_BUNDLE_EXTRA_SESSIONS = "sessions";
private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
private static final int MAX_DATA_SHARE_FILE_DESCRIPTORS_TTL_MS = 1_000 * 60 * 5; // 5 minutes
private static final int MAX_CONCURRENT_FILE_SHARING_REQUESTS = 10;
private static final int DATA_SHARE_BYTE_BUFFER_LENGTH = 1_024;
private static final String CONTENT_PROTECTION_GROUP_CONFIG_SEPARATOR_GROUP = ";";
private static final String CONTENT_PROTECTION_GROUP_CONFIG_SEPARATOR_VALUE = ",";
// Needed to pass checkstyle_hook as names are too long for one line.
private static final int EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST =
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST;
private static final int EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED =
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED;
private static final int EVENT__DATA_SHARE_WRITE_FINISHED =
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_WRITE_FINISHED;
private final LocalService mLocalService = new LocalService();
private final ContentCaptureManagerServiceStub mContentCaptureManagerServiceStub =
new ContentCaptureManagerServiceStub();
@Nullable
final LocalLog mRequestsHistory;
@GuardedBy("mLock")
private ActivityManagerInternal mAm;
/**
* Users disabled by {@link android.provider.Settings.Secure#CONTENT_CAPTURE_ENABLED}
*/
@GuardedBy("mLock")
@Nullable
private SparseBooleanArray mDisabledBySettings;
/**
* Global kill-switch based on value defined by
* {@link ContentCaptureManager#DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED}.
*/
@GuardedBy("mLock")
@Nullable
private boolean mDisabledByDeviceConfig;
// Device-config settings that are cached and passed back to apps
@GuardedBy("mLock")
int mDevCfgLoggingLevel;
@GuardedBy("mLock")
int mDevCfgMaxBufferSize;
@GuardedBy("mLock")
int mDevCfgIdleFlushingFrequencyMs;
@GuardedBy("mLock")
int mDevCfgTextChangeFlushingFrequencyMs;
@GuardedBy("mLock")
int mDevCfgLogHistorySize;
@GuardedBy("mLock")
int mDevCfgIdleUnbindTimeoutMs;
@GuardedBy("mLock")
boolean mDevCfgDisableFlushForViewTreeAppearing;
@GuardedBy("mLock")
boolean mDevCfgEnableContentProtectionReceiver;
@GuardedBy("mLock")
int mDevCfgContentProtectionBufferSize;
@GuardedBy("mLock")
@NonNull
List<List<String>> mDevCfgContentProtectionRequiredGroups;
@GuardedBy("mLock")
@NonNull
List<List<String>> mDevCfgContentProtectionOptionalGroups;
@GuardedBy("mLock")
int mDevCfgContentProtectionOptionalGroupsThreshold;
@GuardedBy("mLock")
long mDevCfgContentProtectionAllowlistDelayMs;
@GuardedBy("mLock")
long mDevCfgContentProtectionAllowlistTimeoutMs;
@GuardedBy("mLock")
long mDevCfgContentProtectionAutoDisconnectTimeoutMs;
private final Executor mDataShareExecutor = Executors.newCachedThreadPool();
private final Handler mHandler = new Handler(Looper.getMainLooper());
@GuardedBy("mLock")
private final Set<String> mPackagesWithShareRequests = new HashSet<>();
private final RemoteCallbackList<IContentCaptureOptionsCallback> mCallbacks =
new RemoteCallbackList<>();
final GlobalContentCaptureOptions mGlobalContentCaptureOptions =
new GlobalContentCaptureOptions();
@GuardedBy("mLock")
@Nullable
private ComponentName mContentProtectionServiceComponentName;
@GuardedBy("mLock")
@Nullable
private ContentProtectionAllowlistManager mContentProtectionAllowlistManager;
@GuardedBy("mLock")
@Nullable
private ContentProtectionConsentManager mContentProtectionConsentManager;
public ContentCaptureManagerService(@NonNull Context context) {
super(context, new FrameworkResourcesServiceNameResolver(context,
com.android.internal.R.string.config_defaultContentCaptureService),
UserManager.DISALLOW_CONTENT_CAPTURE,
/*packageUpdatePolicy=*/ PACKAGE_UPDATE_POLICY_NO_REFRESH);
mDevCfgContentProtectionRequiredGroups =
ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_REQUIRED_GROUPS;
mDevCfgContentProtectionOptionalGroups =
ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_OPTIONAL_GROUPS;
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ActivityThread.currentApplication().getMainExecutor(),
(properties) -> onDeviceConfigChange(properties));
setDeviceConfigProperties();
if (mDevCfgLogHistorySize > 0) {
if (debug) Slog.d(TAG, "log history size: " + mDevCfgLogHistorySize);
mRequestsHistory = new LocalLog(mDevCfgLogHistorySize);
} else {
if (debug) {
Slog.d(TAG, "disabled log history because size is " + mDevCfgLogHistorySize);
}
mRequestsHistory = null;
}
final List<UserInfo> users = getSupportedUsers();
for (int i = 0; i < users.size(); i++) {
final int userId = users.get(i).id;
final boolean disabled = !isEnabledBySettings(userId);
// Sets which services are disabled by settings
if (disabled) {
Slog.i(TAG, "user " + userId + " disabled by settings");
if (mDisabledBySettings == null) {
mDisabledBySettings = new SparseBooleanArray(1);
}
mDisabledBySettings.put(userId, true);
}
// Sets the global options for the service.
mGlobalContentCaptureOptions.setServiceInfo(userId,
mServiceNameResolver.getServiceName(userId),
mServiceNameResolver.isTemporary(userId));
}
}
@Override // from AbstractMasterSystemService
protected ContentCapturePerUserService newServiceLocked(@UserIdInt int resolvedUserId,
boolean disabled) {
return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId);
}
@Override // from SystemService
public boolean isUserSupported(TargetUser user) {
return user.isFull() || user.isProfile();
}
@Override // from SystemService
public void onStart() {
publishBinderService(CONTENT_CAPTURE_MANAGER_SERVICE, mContentCaptureManagerServiceStub);
publishLocalService(ContentCaptureManagerInternal.class, mLocalService);
}
@Override // from AbstractMasterSystemService
protected void onServiceRemoved(@NonNull ContentCapturePerUserService service,
@UserIdInt int userId) {
service.destroyLocked();
}
@Override // from AbstractMasterSystemService
protected void onServicePackageUpdatingLocked(int userId) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
if (service != null) {
service.onPackageUpdatingLocked();
}
}
@Override // from AbstractMasterSystemService
protected void onServicePackageUpdatedLocked(@UserIdInt int userId) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
if (service != null) {
service.onPackageUpdatedLocked();
}
}
@Override // from AbstractMasterSystemService
protected void onServiceNameChanged(@UserIdInt int userId, @NonNull String serviceName,
boolean isTemporary) {
mGlobalContentCaptureOptions.setServiceInfo(userId, serviceName, isTemporary);
super.onServiceNameChanged(userId, serviceName, isTemporary);
}
@Override // from AbstractMasterSystemService
protected void enforceCallingPermissionForManagement() {
getContext().enforceCallingPermission(MANAGE_CONTENT_CAPTURE, TAG);
}
@Override // from AbstractMasterSystemService
protected int getMaximumTemporaryServiceDurationMs() {
return MAX_TEMP_SERVICE_DURATION_MS;
}
@Override // from AbstractMasterSystemService
protected void registerForExtraSettingsChanges(@NonNull ContentResolver resolver,
@NonNull ContentObserver observer) {
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.CONTENT_CAPTURE_ENABLED), false, observer,
UserHandle.USER_ALL);
}
@Override // from AbstractMasterSystemService
protected void onSettingsChanged(@UserIdInt int userId, @NonNull String property) {
switch (property) {
case Settings.Secure.CONTENT_CAPTURE_ENABLED:
setContentCaptureFeatureEnabledBySettingsForUser(userId,
isEnabledBySettings(userId));
return;
default:
Slog.w(TAG, "Unexpected property (" + property + "); updating cache instead");
}
}
@Override // from AbstractMasterSystemService
protected boolean isDisabledLocked(@UserIdInt int userId) {
return mDisabledByDeviceConfig || isDisabledBySettingsLocked(userId)
|| super.isDisabledLocked(userId);
}
@Override
protected void assertCalledByPackageOwner(@NonNull String packageName) {
try {
super.assertCalledByPackageOwner(packageName);
} catch (SecurityException e) {
final int callingUid = Binder.getCallingUid();
VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity
hotwordDetectionServiceIdentity =
LocalServices.getService(VoiceInteractionManagerInternal.class)
.getHotwordDetectionServiceIdentity();
if (callingUid != hotwordDetectionServiceIdentity.getIsolatedUid()) {
super.assertCalledByPackageOwner(packageName);
return;
}
final String[] packages =
getContext()
.getPackageManager()
.getPackagesForUid(hotwordDetectionServiceIdentity.getOwnerUid());
if (packages != null) {
for (String candidate : packages) {
if (packageName.equals(candidate)) return; // Found it
}
}
throw e;
}
}
private boolean isDisabledBySettingsLocked(@UserIdInt int userId) {
return mDisabledBySettings != null && mDisabledBySettings.get(userId);
}
private boolean isEnabledBySettings(@UserIdInt int userId) {
final boolean enabled = Settings.Secure.getIntForUser(getContext().getContentResolver(),
Settings.Secure.CONTENT_CAPTURE_ENABLED, 1, userId) == 1 ? true : false;
return enabled;
}
private void onDeviceConfigChange(@NonNull Properties properties) {
for (String key : properties.getKeyset()) {
switch (key) {
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED:
setDisabledByDeviceConfig(properties.getString(key, null));
return;
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL:
setLoggingLevelFromDeviceConfig();
return;
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE:
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY:
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE:
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY:
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT:
case ContentCaptureManager
.DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING:
// Content protection below
case DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_REQUIRED_GROUPS_CONFIG:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_CONFIG:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_THRESHOLD:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_DELAY_MS:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_TIMEOUT_MS:
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_AUTO_DISCONNECT_TIMEOUT:
setFineTuneParamsFromDeviceConfig();
return;
default:
Slog.i(TAG, "Ignoring change on " + key);
}
}
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
protected void setFineTuneParamsFromDeviceConfig() {
boolean enableContentProtectionReceiverOld;
boolean enableContentProtectionReceiverNew;
String contentProtectionRequiredGroupsConfig;
String contentProtectionOptionalGroupsConfig;
int contentProtectionOptionalGroupsThreshold;
long contentProtectionAllowlistDelayMs;
long contentProtectionAllowlistTimeoutMs;
ContentProtectionAllowlistManager contentProtectionAllowlistManagerOld;
synchronized (mLock) {
mDevCfgMaxBufferSize =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
mDevCfgIdleFlushingFrequencyMs =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
mDevCfgTextChangeFlushingFrequencyMs =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager
.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
mDevCfgLogHistorySize =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE,
20);
mDevCfgIdleUnbindTimeoutMs =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
(int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
mDevCfgDisableFlushForViewTreeAppearing =
DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager
.DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
false);
enableContentProtectionReceiverOld = mDevCfgEnableContentProtectionReceiver;
enableContentProtectionReceiverNew = getDeviceConfigEnableContentProtectionReceiver();
mDevCfgContentProtectionBufferSize =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE,
ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE);
contentProtectionRequiredGroupsConfig =
DeviceConfig.getString(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_REQUIRED_GROUPS_CONFIG,
ContentCaptureManager
.DEFAULT_CONTENT_PROTECTION_REQUIRED_GROUPS_CONFIG);
contentProtectionOptionalGroupsConfig =
DeviceConfig.getString(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_CONFIG,
ContentCaptureManager
.DEFAULT_CONTENT_PROTECTION_OPTIONAL_GROUPS_CONFIG);
contentProtectionOptionalGroupsThreshold =
DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_THRESHOLD,
ContentCaptureManager
.DEFAULT_CONTENT_PROTECTION_OPTIONAL_GROUPS_THRESHOLD);
contentProtectionAllowlistDelayMs =
DeviceConfig.getLong(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_DELAY_MS,
ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_ALLOWLIST_DELAY_MS);
contentProtectionAllowlistTimeoutMs =
DeviceConfig.getLong(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_TIMEOUT_MS,
ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_ALLOWLIST_TIMEOUT_MS);
mDevCfgContentProtectionAutoDisconnectTimeoutMs =
DeviceConfig.getLong(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_AUTO_DISCONNECT_TIMEOUT,
ContentCaptureManager
.DEFAULT_CONTENT_PROTECTION_AUTO_DISCONNECT_TIMEOUT_MS);
contentProtectionAllowlistManagerOld = mContentProtectionAllowlistManager;
if (verbose) {
Slog.v(
TAG,
"setFineTuneParamsFromDeviceConfig(): "
+ "bufferSize="
+ mDevCfgMaxBufferSize
+ ", idleFlush="
+ mDevCfgIdleFlushingFrequencyMs
+ ", textFlush="
+ mDevCfgTextChangeFlushingFrequencyMs
+ ", logHistory="
+ mDevCfgLogHistorySize
+ ", idleUnbindTimeoutMs="
+ mDevCfgIdleUnbindTimeoutMs
+ ", disableFlushForViewTreeAppearing="
+ mDevCfgDisableFlushForViewTreeAppearing
+ ", enableContentProtectionReceiver="
+ enableContentProtectionReceiverNew
+ ", contentProtectionBufferSize="
+ mDevCfgContentProtectionBufferSize
+ ", contentProtectionRequiredGroupsConfig="
+ contentProtectionRequiredGroupsConfig
+ ", contentProtectionOptionalGroupsConfig="
+ contentProtectionOptionalGroupsConfig
+ ", contentProtectionOptionalGroupsThreshold="
+ contentProtectionOptionalGroupsThreshold
+ ", contentProtectionAllowlistDelayMs="
+ contentProtectionAllowlistDelayMs
+ ", contentProtectionAllowlistTimeoutMs="
+ contentProtectionAllowlistTimeoutMs
+ ", contentProtectionAutoDisconnectTimeoutMs="
+ mDevCfgContentProtectionAutoDisconnectTimeoutMs);
}
}
List<List<String>> contentProtectionRequiredGroups =
parseContentProtectionGroupsConfig(contentProtectionRequiredGroupsConfig);
List<List<String>> contentProtectionOptionalGroups =
parseContentProtectionGroupsConfig(contentProtectionOptionalGroupsConfig);
ComponentName contentProtectionServiceComponentNameNew = null;
ContentProtectionAllowlistManager contentProtectionAllowlistManagerNew = null;
ContentProtectionConsentManager contentProtectionConsentManagerNew = null;
if (contentProtectionAllowlistManagerOld != null && !enableContentProtectionReceiverNew) {
contentProtectionAllowlistManagerOld.stop();
}
if (!enableContentProtectionReceiverOld && enableContentProtectionReceiverNew) {
contentProtectionServiceComponentNameNew = getContentProtectionServiceComponentName();
if (contentProtectionServiceComponentNameNew != null) {
contentProtectionAllowlistManagerNew =
createContentProtectionAllowlistManager(
contentProtectionAllowlistTimeoutMs);
contentProtectionAllowlistManagerNew.start(contentProtectionAllowlistDelayMs);
contentProtectionConsentManagerNew = createContentProtectionConsentManager();
}
}
synchronized (mLock) {
mDevCfgEnableContentProtectionReceiver = enableContentProtectionReceiverNew;
mDevCfgContentProtectionRequiredGroups = contentProtectionRequiredGroups;
mDevCfgContentProtectionOptionalGroups = contentProtectionOptionalGroups;
mDevCfgContentProtectionOptionalGroupsThreshold =
contentProtectionOptionalGroupsThreshold;
mDevCfgContentProtectionAllowlistDelayMs = contentProtectionAllowlistDelayMs;
if (enableContentProtectionReceiverOld ^ enableContentProtectionReceiverNew) {
mContentProtectionServiceComponentName = contentProtectionServiceComponentNameNew;
mContentProtectionAllowlistManager = contentProtectionAllowlistManagerNew;
mContentProtectionConsentManager = contentProtectionConsentManagerNew;
}
}
}
private void setLoggingLevelFromDeviceConfig() {
mDevCfgLoggingLevel = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL,
ContentCaptureHelper.getDefaultLoggingLevel());
ContentCaptureHelper.setLoggingLevel(mDevCfgLoggingLevel);
verbose = ContentCaptureHelper.sVerbose;
debug = ContentCaptureHelper.sDebug;
if (verbose) {
Slog.v(TAG, "setLoggingLevelFromDeviceConfig(): level=" + mDevCfgLoggingLevel
+ ", debug=" + debug + ", verbose=" + verbose);
}
}
private void setDeviceConfigProperties() {
setLoggingLevelFromDeviceConfig();
setFineTuneParamsFromDeviceConfig();
final String enabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED);
setDisabledByDeviceConfig(enabled);
}
private void setDisabledByDeviceConfig(@Nullable String explicitlyEnabled) {
if (verbose) {
Slog.v(TAG, "setDisabledByDeviceConfig(): explicitlyEnabled=" + explicitlyEnabled);
}
final List<UserInfo> users = getSupportedUsers();
final boolean newDisabledValue;
if (explicitlyEnabled != null && explicitlyEnabled.equalsIgnoreCase("false")) {
newDisabledValue = true;
} else {
newDisabledValue = false;
}
synchronized (mLock) {
if (mDisabledByDeviceConfig == newDisabledValue) {
if (verbose) {
Slog.v(TAG, "setDisabledByDeviceConfig(): already " + newDisabledValue);
}
return;
}
mDisabledByDeviceConfig = newDisabledValue;
Slog.i(TAG, "setDisabledByDeviceConfig(): set to " + mDisabledByDeviceConfig);
for (int i = 0; i < users.size(); i++) {
final int userId = users.get(i).id;
boolean disabled = mDisabledByDeviceConfig || isDisabledBySettingsLocked(userId);
Slog.i(TAG, "setDisabledByDeviceConfig(): updating service for user "
+ userId + " to " + (disabled ? "'disabled'" : "'enabled'"));
updateCachedServiceLocked(userId, disabled);
}
}
}
private void setContentCaptureFeatureEnabledBySettingsForUser(@UserIdInt int userId,
boolean enabled) {
synchronized (mLock) {
if (mDisabledBySettings == null) {
mDisabledBySettings = new SparseBooleanArray();
}
final boolean alreadyEnabled = !mDisabledBySettings.get(userId);
if (!(enabled ^ alreadyEnabled)) {
if (debug) {
Slog.d(TAG, "setContentCaptureFeatureEnabledForUser(): already " + enabled);
}
return;
}
if (enabled) {
Slog.i(TAG, "setContentCaptureFeatureEnabled(): enabling service for user "
+ userId);
mDisabledBySettings.delete(userId);
} else {
Slog.i(TAG, "setContentCaptureFeatureEnabled(): disabling service for user "
+ userId);
mDisabledBySettings.put(userId, true);
}
final boolean disabled = !enabled || mDisabledByDeviceConfig;
updateCachedServiceLocked(userId, disabled);
}
}
// Called by Shell command.
void destroySessions(@UserIdInt int userId, @NonNull IResultReceiver receiver) {
Slog.i(TAG, "destroySessions() for userId " + userId);
enforceCallingPermissionForManagement();
synchronized (mLock) {
if (userId != UserHandle.USER_ALL) {
final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
service.destroySessionsLocked();
}
} else {
visitServicesLocked((s) -> s.destroySessionsLocked());
}
}
try {
receiver.send(0, new Bundle());
} catch (RemoteException e) {
// Just ignore it...
}
}
// Called by Shell command.
void listSessions(int userId, IResultReceiver receiver) {
Slog.i(TAG, "listSessions() for userId " + userId);
enforceCallingPermissionForManagement();
final Bundle resultData = new Bundle();
final ArrayList<String> sessions = new ArrayList<>();
synchronized (mLock) {
if (userId != UserHandle.USER_ALL) {
final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
service.listSessionsLocked(sessions);
}
} else {
visitServicesLocked((s) -> s.listSessionsLocked(sessions));
}
}
resultData.putStringArrayList(RECEIVER_BUNDLE_EXTRA_SESSIONS, sessions);
try {
receiver.send(0, resultData);
} catch (RemoteException e) {
// Just ignore it...
}
}
void updateOptions(String packageName, ContentCaptureOptions options) {
mCallbacks.broadcast((callback, pkg) -> {
if (pkg.equals(packageName)) {
try {
callback.setContentCaptureOptions(options);
} catch (RemoteException e) {
Slog.w(TAG, "Unable to send setContentCaptureOptions(): " + e);
}
}
});
}
private ActivityManagerInternal getAmInternal() {
synchronized (mLock) {
if (mAm == null) {
mAm = LocalServices.getService(ActivityManagerInternal.class);
}
}
return mAm;
}
@GuardedBy("mLock")
private void assertCalledByServiceLocked(@NonNull String methodName) {
if (!isCalledByServiceLocked(methodName)) {
throw new SecurityException("caller is not user's ContentCapture service");
}
}
@GuardedBy("mLock")
private boolean isCalledByServiceLocked(@NonNull String methodName) {
final int userId = UserHandle.getCallingUserId();
final int callingUid = Binder.getCallingUid();
final String serviceName = mServiceNameResolver.getServiceName(userId);
if (serviceName == null) {
Slog.e(TAG, methodName + ": called by UID " + callingUid
+ ", but there's no service set for user " + userId);
return false;
}
final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
if (serviceComponent == null) {
Slog.w(TAG, methodName + ": invalid service name: " + serviceName);
return false;
}
final String servicePackageName = serviceComponent.getPackageName();
final PackageManager pm = getContext().getPackageManager();
final int serviceUid;
try {
serviceUid = pm.getPackageUidAsUser(servicePackageName, UserHandle.getCallingUserId());
} catch (NameNotFoundException e) {
Slog.w(TAG, methodName + ": could not verify UID for " + serviceName);
return false;
}
if (callingUid != serviceUid) {
Slog.e(TAG, methodName + ": called by UID " + callingUid + ", but service UID is "
+ serviceUid);
return false;
}
return true;
}
/**
* Executes the given {@code runnable} and if it throws a {@link SecurityException},
* send it back to the receiver.
*
* @return whether the exception was thrown or not.
*/
private boolean throwsSecurityException(@NonNull IResultReceiver result,
@NonNull Runnable runable) {
try {
runable.run();
return false;
} catch (SecurityException e) {
try {
result.send(RESULT_CODE_SECURITY_EXCEPTION, bundleFor(e.getMessage()));
} catch (RemoteException e2) {
Slog.w(TAG, "Unable to send security exception (" + e + "): ", e2);
}
}
return true;
}
@GuardedBy("mLock")
private boolean isDefaultServiceLocked(int userId) {
final String defaultServiceName = mServiceNameResolver.getDefaultServiceName(userId);
if (defaultServiceName == null) {
return false;
}
final String currentServiceName = mServiceNameResolver.getServiceName(userId);
return defaultServiceName.equals(currentServiceName);
}
@Override // from AbstractMasterSystemService
@GuardedBy("mLock")
protected void dumpLocked(String prefix, PrintWriter pw) {
super.dumpLocked(prefix, pw);
final String prefix2 = prefix + " ";
pw.print(prefix);
pw.print("Users disabled by Settings: ");
pw.println(mDisabledBySettings);
pw.print(prefix);
pw.println("DeviceConfig Settings: ");
pw.print(prefix2);
pw.print("disabled: ");
pw.println(mDisabledByDeviceConfig);
pw.print(prefix2);
pw.print("loggingLevel: ");
pw.println(mDevCfgLoggingLevel);
pw.print(prefix2);
pw.print("maxBufferSize: ");
pw.println(mDevCfgMaxBufferSize);
pw.print(prefix2);
pw.print("idleFlushingFrequencyMs: ");
pw.println(mDevCfgIdleFlushingFrequencyMs);
pw.print(prefix2);
pw.print("textChangeFlushingFrequencyMs: ");
pw.println(mDevCfgTextChangeFlushingFrequencyMs);
pw.print(prefix2);
pw.print("logHistorySize: ");
pw.println(mDevCfgLogHistorySize);
pw.print(prefix2);
pw.print("idleUnbindTimeoutMs: ");
pw.println(mDevCfgIdleUnbindTimeoutMs);
pw.print(prefix2);
pw.print("disableFlushForViewTreeAppearing: ");
pw.println(mDevCfgDisableFlushForViewTreeAppearing);
pw.print(prefix2);
pw.print("enableContentProtectionReceiver: ");
pw.println(mDevCfgEnableContentProtectionReceiver);
pw.print(prefix2);
pw.print("contentProtectionBufferSize: ");
pw.println(mDevCfgContentProtectionBufferSize);
pw.print(prefix2);
pw.print("contentProtectionRequiredGroupsSize: ");
pw.println(mDevCfgContentProtectionRequiredGroups.size());
pw.print(prefix2);
pw.print("contentProtectionOptionalGroupsSize: ");
pw.println(mDevCfgContentProtectionOptionalGroups.size());
pw.print(prefix2);
pw.print("contentProtectionOptionalGroupsThreshold: ");
pw.println(mDevCfgContentProtectionOptionalGroupsThreshold);
pw.print(prefix2);
pw.print("contentProtectionAllowlistDelayMs: ");
pw.println(mDevCfgContentProtectionAllowlistDelayMs);
pw.print(prefix2);
pw.print("contentProtectionAllowlistTimeoutMs: ");
pw.println(mDevCfgContentProtectionAllowlistTimeoutMs);
pw.print(prefix2);
pw.print("contentProtectionAutoDisconnectTimeoutMs: ");
pw.println(mDevCfgContentProtectionAutoDisconnectTimeoutMs);
pw.print(prefix);
pw.println("Global Options:");
mGlobalContentCaptureOptions.dump(prefix2, pw);
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
protected boolean getDeviceConfigEnableContentProtectionReceiver() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER,
ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER);
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
protected ContentProtectionAllowlistManager createContentProtectionAllowlistManager(
long timeoutMs) {
// Same handler as used by AbstractMasterSystemService
return new ContentProtectionAllowlistManager(
this, BackgroundThread.getHandler(), timeoutMs);
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
protected ContentProtectionConsentManager createContentProtectionConsentManager() {
// Same handler as used by AbstractMasterSystemService
return new ContentProtectionConsentManager(
BackgroundThread.getHandler(),
getContext().getContentResolver(),
LocalServices.getService(DevicePolicyManagerInternal.class));
}
@Nullable
private ComponentName getContentProtectionServiceComponentName() {
String flatComponentName = getContentProtectionServiceFlatComponentName();
if (flatComponentName == null) {
return null;
}
return ComponentName.unflattenFromString(flatComponentName);
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@Nullable
protected String getContentProtectionServiceFlatComponentName() {
return getContext()
.getString(com.android.internal.R.string.config_defaultContentProtectionService);
}
/**
* Can also throw runtime exceptions such as {@link SecurityException}.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
protected ContentCaptureServiceInfo createContentProtectionServiceInfo(
@NonNull ComponentName componentName) throws PackageManager.NameNotFoundException {
return new ContentCaptureServiceInfo(
getContext(), componentName, /* isTemp= */ false, UserHandle.getCallingUserId());
}
/** @hide */
@Nullable
public RemoteContentProtectionService createRemoteContentProtectionService() {
ComponentName componentName;
long autoDisconnectTimeoutMs;
synchronized (mLock) {
if (!mDevCfgEnableContentProtectionReceiver
|| mContentProtectionServiceComponentName == null) {
return null;
}
componentName = mContentProtectionServiceComponentName;
autoDisconnectTimeoutMs = mDevCfgContentProtectionAutoDisconnectTimeoutMs;
}
// Check permissions by trying to construct {@link ContentCaptureServiceInfo}
try {
createContentProtectionServiceInfo(componentName);
} catch (Exception ex) {
// Swallow, exception was already logged
return null;
}
return createRemoteContentProtectionService(componentName, autoDisconnectTimeoutMs);
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
protected RemoteContentProtectionService createRemoteContentProtectionService(
@NonNull ComponentName componentName, long autoDisconnectTimeoutMs) {
return new RemoteContentProtectionService(
getContext(),
componentName,
UserHandle.getCallingUserId(),
isBindInstantServiceAllowed(),
autoDisconnectTimeoutMs);
}
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
protected ContentCaptureManagerServiceStub getContentCaptureManagerServiceStub() {
return mContentCaptureManagerServiceStub;
}
/**
* Parses a simple config in format "group;group" where each "group" is itself in the format of
* "string1,string2", eg:
*
* <p>"a" -> [["a"]]
*
* <p>"a,b" -> [["a", "b"]]
*
* <p>"a,b;c;d,e" -> [["a", "b"], ["c"], ["d", "e"]]
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
protected List<List<String>> parseContentProtectionGroupsConfig(@Nullable String config) {
if (verbose) {
Slog.v(TAG, "parseContentProtectionGroupsConfig: " + config);
}
if (config == null) {
return Collections.emptyList();
}
return Arrays.stream(config.split(CONTENT_PROTECTION_GROUP_CONFIG_SEPARATOR_GROUP))
.map(this::parseContentProtectionGroupConfigValues)
.filter(group -> !group.isEmpty())
.toList();
}
private List<String> parseContentProtectionGroupConfigValues(@NonNull String group) {
return Arrays.stream(group.split(CONTENT_PROTECTION_GROUP_CONFIG_SEPARATOR_VALUE))
.filter(value -> !value.isEmpty())
.toList();
}
@GuardedBy("mLock")
private boolean isContentProtectionEnabledLocked() {
return mDevCfgEnableContentProtectionReceiver
&& mContentProtectionServiceComponentName != null
&& mContentProtectionAllowlistManager != null
&& mContentProtectionConsentManager != null
&& !(mDevCfgContentProtectionRequiredGroups.isEmpty()
&& mDevCfgContentProtectionOptionalGroups.isEmpty());
}
final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub {
@Override
public void startSession(@NonNull IBinder activityToken,
@NonNull IBinder shareableActivityToken, @NonNull ComponentName componentName,
int sessionId, int flags, @NonNull IResultReceiver result) {
Objects.requireNonNull(activityToken);
Objects.requireNonNull(shareableActivityToken);
final int userId = UserHandle.getCallingUserId();
final ActivityPresentationInfo activityPresentationInfo = getAmInternal()
.getActivityPresentationInfo(activityToken);
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
if (!isDefaultServiceLocked(userId) && !isCalledByServiceLocked("startSession()")) {
setClientState(result, STATE_DISABLED, /* binder= */ null);
return;
}
service.startSessionLocked(activityToken, shareableActivityToken,
activityPresentationInfo, sessionId, Binder.getCallingUid(), flags, result);
}
}
@Override
public void finishSession(int sessionId) {
final int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
service.finishSessionLocked(sessionId);
}
}
@Override
public void getServiceComponentName(@NonNull IResultReceiver result) {
final int userId = UserHandle.getCallingUserId();
ComponentName connectedServiceComponentName;
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
connectedServiceComponentName = service.getServiceComponentName();
}
try {
result.send(RESULT_CODE_OK, bundleFor(connectedServiceComponentName));
} catch (RemoteException e) {
Slog.w(TAG, "Unable to send service component name: " + e);
}
}
@Override
public void removeData(@NonNull DataRemovalRequest request) {
Objects.requireNonNull(request);
assertCalledByPackageOwner(request.getPackageName());
final int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
service.removeDataLocked(request);
}
}
@Override
public void shareData(@NonNull DataShareRequest request,
@NonNull IDataShareWriteAdapter clientAdapter) {
Objects.requireNonNull(request);
Objects.requireNonNull(clientAdapter);
assertCalledByPackageOwner(request.getPackageName());
final int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
if (mPackagesWithShareRequests.size() >= MAX_CONCURRENT_FILE_SHARING_REQUESTS
|| mPackagesWithShareRequests.contains(request.getPackageName())) {
try {
String serviceName = mServiceNameResolver.getServiceName(userId);
ContentCaptureMetricsLogger.writeServiceEvent(
EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST,
serviceName);
clientAdapter.error(
ContentCaptureManager.DATA_SHARE_ERROR_CONCURRENT_REQUEST);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to send error message to client");
}
return;
}
service.onDataSharedLocked(request,
new DataShareCallbackDelegate(request, clientAdapter,
ContentCaptureManagerService.this));
}
}
@Override
public void isContentCaptureFeatureEnabled(@NonNull IResultReceiver result) {
boolean enabled;
synchronized (mLock) {
if (throwsSecurityException(result,
() -> assertCalledByServiceLocked("isContentCaptureFeatureEnabled()"))) {
return;
}
final int userId = UserHandle.getCallingUserId();
enabled = !mDisabledByDeviceConfig && !isDisabledBySettingsLocked(userId);
}
try {
result.send(enabled ? RESULT_CODE_TRUE : RESULT_CODE_FALSE, /* resultData= */null);
} catch (RemoteException e) {
Slog.w(TAG, "Unable to send isContentCaptureFeatureEnabled(): " + e);
}
}
@Override
public void getServiceSettingsActivity(@NonNull IResultReceiver result) {
if (throwsSecurityException(result, () -> enforceCallingPermissionForManagement())) {
return;
}
final int userId = UserHandle.getCallingUserId();
final ComponentName componentName;
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
if (service == null) return;
componentName = service.getServiceSettingsActivityLocked();
}
try {
result.send(RESULT_CODE_OK, bundleFor(componentName));
} catch (RemoteException e) {
Slog.w(TAG, "Unable to send getServiceSettingsIntent(): " + e);
}
}
@Override
public void getContentCaptureConditions(@NonNull String packageName,
@NonNull IResultReceiver result) {
if (throwsSecurityException(result, () -> assertCalledByPackageOwner(packageName))) {
return;
}
final int userId = UserHandle.getCallingUserId();
final ArrayList<ContentCaptureCondition> conditions;
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
conditions = service == null ? null
: toList(service.getContentCaptureConditionsLocked(packageName));
}
try {
result.send(RESULT_CODE_OK, bundleFor(conditions));
} catch (RemoteException e) {
Slog.w(TAG, "Unable to send getServiceComponentName(): " + e);
}
}
@Override
public void registerContentCaptureOptionsCallback(@NonNull String packageName,
IContentCaptureOptionsCallback callback) {
assertCalledByPackageOwner(packageName);
mCallbacks.register(callback, packageName);
// Set options here in case it was updated before this was registered.
final int userId = UserHandle.getCallingUserId();
final ContentCaptureOptions options = mGlobalContentCaptureOptions.getOptions(userId,
packageName);
if (options != null) {
try {
callback.setContentCaptureOptions(options);
} catch (RemoteException e) {
Slog.w(TAG, "Unable to send setContentCaptureOptions(): " + e);
}
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
boolean showHistory = true;
if (args != null) {
for (String arg : args) {
switch (arg) {
case "--no-history":
showHistory = false;
break;
case "--help":
pw.println("Usage: dumpsys content_capture [--no-history]");
return;
default:
Slog.w(TAG, "Ignoring invalid dump arg: " + arg);
}
}
}
synchronized (mLock) {
dumpLocked("", pw);
}
pw.print("Requests history: ");
if (mRequestsHistory == null) {
pw.println("disabled by device config");
} else if (showHistory) {
pw.println();
mRequestsHistory.reverseDump(fd, pw, args);
pw.println();
} else {
pw.println();
}
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver)
throws RemoteException {
new ContentCaptureManagerServiceShellCommand(ContentCaptureManagerService.this).exec(
this, in, out, err, args, callback, resultReceiver);
}
@Override
public void resetTemporaryService(@UserIdInt int userId) {
ContentCaptureManagerService.this.resetTemporaryService(userId);
}
@Override
public void setTemporaryService(
@UserIdInt int userId, @NonNull String serviceName, int duration) {
ContentCaptureManagerService.this.setTemporaryService(
userId, serviceName, duration);
}
@Override
public void setDefaultServiceEnabled(@UserIdInt int userId, boolean enabled) {
ContentCaptureManagerService.this.setDefaultServiceEnabled(userId, enabled);
}
@Override
public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
RemoteContentProtectionService service = createRemoteContentProtectionService();
if (service == null) {
return;
}
try {
service.onLoginDetected(events);
} catch (Exception ex) {
Slog.e(TAG, "Failed to call remote service", ex);
}
}
}
private final class LocalService extends ContentCaptureManagerInternal {
@Override
public boolean isContentCaptureServiceForUser(int uid, @UserIdInt int userId) {
synchronized (mLock) {
final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
return service.isContentCaptureServiceForUserLocked(uid);
}
}
return false;
}
@Override
public boolean sendActivityAssistData(@UserIdInt int userId, @NonNull IBinder activityToken,
@NonNull Bundle data) {
synchronized (mLock) {
final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
return service.sendActivityAssistDataLocked(activityToken, data);
}
}
return false;
}
@Override
public ContentCaptureOptions getOptionsForPackage(int userId, @NonNull String packageName) {
return mGlobalContentCaptureOptions.getOptions(userId, packageName);
}
// ErrorProne says ContentCaptureManagerService.this.mLock needs to be guarded by
// 'service.mLock', which is the same as mLock.
@SuppressWarnings("GuardedBy")
@Override
public void notifyActivityEvent(int userId, @NonNull ComponentName activityComponent,
@ActivityEventType int eventType, @NonNull ActivityId activityId) {
synchronized (mLock) {
final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
if (service != null) {
service.onActivityEventLocked(activityId, activityComponent, eventType);
}
}
}
}
/**
* Content capture options associated with all services.
*
* <p>This object is defined here instead of on each {@link ContentCapturePerUserService}
* because it cannot hold a lock on the main lock when
* {@link GlobalContentCaptureOptions#getOptions(int, String)} is called by external services.
*/
final class GlobalContentCaptureOptions extends GlobalWhitelistState {
@GuardedBy("mGlobalWhitelistStateLock")
private final SparseArray<String> mServicePackages = new SparseArray<>();
@GuardedBy("mGlobalWhitelistStateLock")
private final SparseBooleanArray mTemporaryServices = new SparseBooleanArray();
private void setServiceInfo(@UserIdInt int userId, @Nullable String serviceName,
boolean isTemporary) {
synchronized (mGlobalWhitelistStateLock) {
if (isTemporary) {
mTemporaryServices.put(userId, true);
} else {
mTemporaryServices.delete(userId);
}
if (serviceName != null) {
final ComponentName componentName =
ComponentName.unflattenFromString(serviceName);
if (componentName == null) {
Slog.w(TAG, "setServiceInfo(): invalid name: " + serviceName);
mServicePackages.remove(userId);
} else {
mServicePackages.put(userId, componentName.getPackageName());
}
} else {
mServicePackages.remove(userId);
}
}
}
@Nullable
@GuardedBy("mGlobalWhitelistStateLock")
public ContentCaptureOptions getOptions(@UserIdInt int userId,
@NonNull String packageName) {
boolean isContentCaptureReceiverEnabled;
boolean isContentProtectionReceiverEnabled =
isContentProtectionReceiverEnabled(userId, packageName);
ArraySet<ComponentName> whitelistedComponents = null;
synchronized (mGlobalWhitelistStateLock) {
isContentCaptureReceiverEnabled =
isContentCaptureReceiverEnabled(userId, packageName);
if (!isContentCaptureReceiverEnabled) {
// Full package is not allowlisted: check individual components next
whitelistedComponents = getWhitelistedComponents(userId, packageName);
if (!isContentProtectionReceiverEnabled
&& whitelistedComponents == null
&& packageName.equals(mServicePackages.get(userId))) {
// No components allowlisted either, but let it go because it's the
// service's own package
if (verbose) Slog.v(TAG, "getOptionsForPackage() lite for " + packageName);
return new ContentCaptureOptions(mDevCfgLoggingLevel);
}
}
} // synchronized
// Restrict what temporary services can allowlist
if (Build.IS_USER && mServiceNameResolver.isTemporary(userId)) {
if (!packageName.equals(mServicePackages.get(userId))) {
Slog.w(TAG, "Ignoring package " + packageName + " while using temporary "
+ "service " + mServicePackages.get(userId));
return null;
}
}
if (!isContentCaptureReceiverEnabled
&& !isContentProtectionReceiverEnabled
&& whitelistedComponents == null) {
// No can do!
if (verbose) {
Slog.v(TAG, "getOptionsForPackage(" + packageName + "): not whitelisted");
}
return null;
}
synchronized (mLock) {
final ContentCaptureOptions options =
new ContentCaptureOptions(
mDevCfgLoggingLevel,
mDevCfgMaxBufferSize,
mDevCfgIdleFlushingFrequencyMs,
mDevCfgTextChangeFlushingFrequencyMs,
mDevCfgLogHistorySize,
mDevCfgDisableFlushForViewTreeAppearing,
isContentCaptureReceiverEnabled || whitelistedComponents != null,
new ContentCaptureOptions.ContentProtectionOptions(
isContentProtectionReceiverEnabled,
mDevCfgContentProtectionBufferSize,
mDevCfgContentProtectionRequiredGroups,
mDevCfgContentProtectionOptionalGroups,
mDevCfgContentProtectionOptionalGroupsThreshold),
whitelistedComponents);
if (verbose) Slog.v(TAG, "getOptionsForPackage(" + packageName + "): " + options);
return options;
}
}
@Override
public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
super.dump(prefix, pw);
synchronized (mGlobalWhitelistStateLock) {
if (mServicePackages.size() > 0) {
pw.print(prefix); pw.print("Service packages: "); pw.println(mServicePackages);
}
if (mTemporaryServices.size() > 0) {
pw.print(prefix); pw.print("Temp services: "); pw.println(mTemporaryServices);
}
}
}
@Override // from GlobalWhitelistState
public boolean isWhitelisted(@UserIdInt int userId, @NonNull String packageName) {
return isContentCaptureReceiverEnabled(userId, packageName)
|| isContentProtectionReceiverEnabled(userId, packageName);
}
@Override // from GlobalWhitelistState
public boolean isWhitelisted(@UserIdInt int userId, @NonNull ComponentName componentName) {
return super.isWhitelisted(userId, componentName)
|| isContentProtectionReceiverEnabled(userId, componentName.getPackageName());
}
private boolean isContentCaptureReceiverEnabled(
@UserIdInt int userId, @NonNull String packageName) {
return super.isWhitelisted(userId, packageName);
}
private boolean isContentProtectionReceiverEnabled(
@UserIdInt int userId, @NonNull String packageName) {
ContentProtectionConsentManager consentManager;
ContentProtectionAllowlistManager allowlistManager;
synchronized (mLock) {
if (!isContentProtectionEnabledLocked()) {
return false;
}
consentManager = mContentProtectionConsentManager;
allowlistManager = mContentProtectionAllowlistManager;
}
return consentManager.isConsentGranted(userId)
&& allowlistManager.isAllowed(packageName);
}
}
private static class DataShareCallbackDelegate extends IDataShareCallback.Stub {
@NonNull private final DataShareRequest mDataShareRequest;
@NonNull private final IDataShareWriteAdapter mClientAdapter;
@NonNull private final ContentCaptureManagerService mParentService;
@NonNull private final AtomicBoolean mLoggedWriteFinish = new AtomicBoolean(false);
DataShareCallbackDelegate(@NonNull DataShareRequest dataShareRequest,
@NonNull IDataShareWriteAdapter clientAdapter,
ContentCaptureManagerService parentService) {
mDataShareRequest = dataShareRequest;
mClientAdapter = clientAdapter;
mParentService = parentService;
}
@Override
public void accept(@NonNull IDataShareReadAdapter serviceAdapter) {
Slog.i(TAG, "Data share request accepted by Content Capture service");
logServiceEvent(CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ACCEPT_DATA_SHARE_REQUEST);
Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe();
if (clientPipe == null) {
logServiceEvent(
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL);
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
return;
}
ParcelFileDescriptor sourceIn = clientPipe.second;
ParcelFileDescriptor sinkIn = clientPipe.first;
Pair<ParcelFileDescriptor, ParcelFileDescriptor> servicePipe = createPipe();
if (servicePipe == null) {
logServiceEvent(
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL);
bestEffortCloseFileDescriptors(sourceIn, sinkIn);
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
return;
}
ParcelFileDescriptor sourceOut = servicePipe.second;
ParcelFileDescriptor sinkOut = servicePipe.first;
synchronized (mParentService.mLock) {
mParentService.mPackagesWithShareRequests.add(mDataShareRequest.getPackageName());
}
if (!setUpSharingPipeline(mClientAdapter, serviceAdapter, sourceIn, sinkOut)) {
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
bestEffortCloseFileDescriptors(sourceIn, sinkIn, sourceOut, sinkOut);
synchronized (mParentService.mLock) {
mParentService.mPackagesWithShareRequests
.remove(mDataShareRequest.getPackageName());
}
return;
}
// File descriptors received by remote apps will be copies of the current one. Close
// the ones that belong to the system server, so there's only 1 open left for the
// current pipe. Therefore when remote parties decide to close them - all descriptors
// pointing to the pipe will be closed.
bestEffortCloseFileDescriptors(sourceIn, sinkOut);
mParentService.mDataShareExecutor.execute(() -> {
boolean receivedData = false;
try (InputStream fis =
new ParcelFileDescriptor.AutoCloseInputStream(sinkIn);
OutputStream fos =
new ParcelFileDescriptor.AutoCloseOutputStream(sourceOut)) {
byte[] byteBuffer = new byte[DATA_SHARE_BYTE_BUFFER_LENGTH];
while (true) {
int readBytes = fis.read(byteBuffer);
if (readBytes == -1) {
break;
}
fos.write(byteBuffer, 0 /* offset */, readBytes);
receivedData = true;
}
} catch (IOException e) {
Slog.e(TAG, "Failed to pipe client and service streams", e);
logServiceEvent(
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_IOEXCEPTION);
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
} finally {
synchronized (mParentService.mLock) {
mParentService.mPackagesWithShareRequests
.remove(mDataShareRequest.getPackageName());
}
if (receivedData) {
if (!mLoggedWriteFinish.get()) {
logServiceEvent(EVENT__DATA_SHARE_WRITE_FINISHED);
mLoggedWriteFinish.set(true);
}
try {
mClientAdapter.finish();
} catch (RemoteException e) {
Slog.e(TAG, "Failed to call finish() the client operation", e);
}
try {
serviceAdapter.finish();
} catch (RemoteException e) {
Slog.e(TAG, "Failed to call finish() the service operation", e);
}
} else {
// Client or service may have crashed before sending.
logServiceEvent(
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_EMPTY_DATA);
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
}
}
});
mParentService.mHandler.postDelayed(() ->
enforceDataSharingTtl(
sourceIn,
sinkIn,
sourceOut,
sinkOut,
serviceAdapter),
MAX_DATA_SHARE_FILE_DESCRIPTORS_TTL_MS);
}
@Override
public void reject() {
Slog.i(TAG, "Data share request rejected by Content Capture service");
logServiceEvent(CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__REJECT_DATA_SHARE_REQUEST);
try {
mClientAdapter.rejected();
} catch (RemoteException e) {
Slog.w(TAG, "Failed to call rejected() the client operation", e);
try {
mClientAdapter.error(ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
} catch (RemoteException e2) {
Slog.w(TAG, "Failed to call error() the client operation", e2);
}
}
}
private boolean setUpSharingPipeline(
IDataShareWriteAdapter clientAdapter,
IDataShareReadAdapter serviceAdapter,
ParcelFileDescriptor sourceIn,
ParcelFileDescriptor sinkOut) {
try {
clientAdapter.write(sourceIn);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to call write() the client operation", e);
logServiceEvent(
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL);
return false;
}
try {
serviceAdapter.start(sinkOut);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to call start() the service operation", e);
logServiceEvent(
CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL);
return false;
}
return true;
}
private void enforceDataSharingTtl(ParcelFileDescriptor sourceIn,
ParcelFileDescriptor sinkIn,
ParcelFileDescriptor sourceOut,
ParcelFileDescriptor sinkOut,
IDataShareReadAdapter serviceAdapter) {
synchronized (mParentService.mLock) {
mParentService.mPackagesWithShareRequests
.remove(mDataShareRequest.getPackageName());
// Interaction finished successfully <=> all data has been written to Content
// Capture Service. If it hasn't been read successfully, service would be able
// to signal by closing the input stream while not have finished reading.
boolean finishedSuccessfully = !sinkIn.getFileDescriptor().valid()
&& !sourceOut.getFileDescriptor().valid();
if (finishedSuccessfully) {
if (!mLoggedWriteFinish.get()) {
logServiceEvent(EVENT__DATA_SHARE_WRITE_FINISHED);
mLoggedWriteFinish.set(true);
}
Slog.i(TAG, "Content capture data sharing session terminated "
+ "successfully for package '"
+ mDataShareRequest.getPackageName()
+ "'");
} else {
logServiceEvent(EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED);
Slog.i(TAG, "Reached the timeout of Content Capture data sharing session "
+ "for package '"
+ mDataShareRequest.getPackageName()
+ "', terminating the pipe.");
}
// Ensure all the descriptors are closed after the session.
bestEffortCloseFileDescriptors(sourceIn, sinkIn, sourceOut, sinkOut);
if (!finishedSuccessfully) {
sendErrorSignal(mClientAdapter, serviceAdapter,
ContentCaptureManager.DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED);
}
}
}
private Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
ParcelFileDescriptor[] fileDescriptors;
try {
fileDescriptors = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
Slog.e(TAG, "Failed to create a content capture data-sharing pipe", e);
return null;
}
if (fileDescriptors.length != 2) {
Slog.e(TAG, "Failed to create a content capture data-sharing pipe, "
+ "unexpected number of file descriptors");
return null;
}
if (!fileDescriptors[0].getFileDescriptor().valid()
|| !fileDescriptors[1].getFileDescriptor().valid()) {
Slog.e(TAG, "Failed to create a content capture data-sharing pipe, didn't "
+ "receive a pair of valid file descriptors.");
return null;
}
return Pair.create(fileDescriptors[0], fileDescriptors[1]);
}
private void bestEffortCloseFileDescriptor(ParcelFileDescriptor fd) {
try {
fd.close();
} catch (IOException e) {
Slog.e(TAG, "Failed to close a file descriptor", e);
}
}
private void bestEffortCloseFileDescriptors(ParcelFileDescriptor... fds) {
for (ParcelFileDescriptor fd : fds) {
bestEffortCloseFileDescriptor(fd);
}
}
private static void sendErrorSignal(
IDataShareWriteAdapter clientAdapter,
IDataShareReadAdapter serviceAdapter,
int errorCode) {
try {
clientAdapter.error(errorCode);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to call error() the client operation", e);
}
try {
serviceAdapter.error(errorCode);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to call error() the service operation", e);
}
}
private void logServiceEvent(int eventType) {
int userId = UserHandle.getCallingUserId();
String serviceName = mParentService.mServiceNameResolver.getServiceName(userId);
ContentCaptureMetricsLogger.writeServiceEvent(eventType, serviceName);
}
}
}