440 lines
18 KiB
Java
440 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2021 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.safetycenter;
|
|
|
|
import static android.Manifest.permission.MANAGE_SAFETY_CENTER;
|
|
import static android.Manifest.permission.READ_SAFETY_CENTER_STATUS;
|
|
import static android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE;
|
|
import static android.os.Build.VERSION_CODES.TIRAMISU;
|
|
import static android.safetycenter.SafetyCenterManager.RefreshReason;
|
|
import static android.safetycenter.config.SafetySource.SAFETY_SOURCE_TYPE_STATIC;
|
|
|
|
import static java.util.Objects.requireNonNull;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.UserIdInt;
|
|
import android.app.AppOpsManager;
|
|
import android.content.Context;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.res.Resources;
|
|
import android.os.Binder;
|
|
import android.os.RemoteCallbackList;
|
|
import android.provider.DeviceConfig;
|
|
import android.safetycenter.IOnSafetyCenterDataChangedListener;
|
|
import android.safetycenter.ISafetyCenterManager;
|
|
import android.safetycenter.SafetyCenterData;
|
|
import android.safetycenter.SafetyCenterErrorDetails;
|
|
import android.safetycenter.SafetyEvent;
|
|
import android.safetycenter.SafetySourceData;
|
|
import android.safetycenter.SafetySourceErrorDetails;
|
|
import android.safetycenter.config.SafetyCenterConfig;
|
|
import android.safetycenter.config.SafetySource;
|
|
import android.safetycenter.config.SafetySourcesGroup;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.Keep;
|
|
import androidx.annotation.RequiresApi;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.permission.util.PermissionUtils;
|
|
import com.android.server.SystemService;
|
|
|
|
import java.util.Arrays;
|
|
|
|
/**
|
|
* Service for the safety center.
|
|
*
|
|
* @hide
|
|
*/
|
|
@Keep
|
|
@RequiresApi(TIRAMISU)
|
|
public final class SafetyCenterService extends SystemService {
|
|
|
|
private static final String TAG = "SafetyCenterService";
|
|
|
|
/** Phenotype flag that determines whether SafetyCenter is enabled. */
|
|
private static final String PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled";
|
|
|
|
private final Object mApiLock = new Object();
|
|
// Refresh/rescan is guarded by another lock: sending broadcasts can be a lengthy operation and
|
|
// the APIs that will be exercised by the receivers are already protected by `mApiLock`.
|
|
private final Object mRefreshLock = new Object();
|
|
|
|
@GuardedBy("mApiLock")
|
|
private final SafetyCenterListeners mSafetyCenterListeners = new SafetyCenterListeners();
|
|
|
|
@NonNull private final SafetyCenterConfigReader mSafetyCenterConfigReader;
|
|
|
|
@GuardedBy("mApiLock")
|
|
@NonNull
|
|
private final SafetyCenterDataTracker mSafetyCenterDataTracker;
|
|
|
|
@GuardedBy("mRefreshLock")
|
|
@NonNull
|
|
private final SafetyCenterRefreshManager mSafetyCenterRefreshManager;
|
|
|
|
@NonNull private final AppOpsManager mAppOpsManager;
|
|
|
|
public SafetyCenterService(@NonNull Context context) {
|
|
super(context);
|
|
mSafetyCenterConfigReader = new SafetyCenterConfigReader(context);
|
|
mSafetyCenterDataTracker = new SafetyCenterDataTracker(context, mSafetyCenterConfigReader);
|
|
mSafetyCenterRefreshManager =
|
|
new SafetyCenterRefreshManager(context, mSafetyCenterConfigReader);
|
|
mAppOpsManager = requireNonNull(context.getSystemService(AppOpsManager.class));
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
publishBinderService(Context.SAFETY_CENTER_SERVICE, new Stub());
|
|
mSafetyCenterConfigReader.loadSafetyCenterConfig();
|
|
}
|
|
|
|
/** Service implementation of {@link ISafetyCenterManager.Stub}. */
|
|
private final class Stub extends ISafetyCenterManager.Stub {
|
|
@Override
|
|
public boolean isSafetyCenterEnabled() {
|
|
enforceAnyCallingOrSelfPermissions(
|
|
"isSafetyCenterEnabled", READ_SAFETY_CENTER_STATUS, SEND_SAFETY_CENTER_UPDATE);
|
|
// TODO(b/214568975): Decide if we should disable safety center if there is a problem
|
|
// reading the config.
|
|
|
|
return isApiEnabled();
|
|
}
|
|
|
|
@Override
|
|
public void setSafetySourceData(
|
|
@NonNull String safetySourceId,
|
|
@Nullable SafetySourceData safetySourceData,
|
|
@NonNull SafetyEvent safetyEvent,
|
|
@NonNull String packageName,
|
|
@UserIdInt int userId) {
|
|
mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "setSafetySourceData", getContext());
|
|
// TODO(b/205706756): Security: check certs?
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
SEND_SAFETY_CENTER_UPDATE, "setSafetySourceData");
|
|
if (!checkApiEnabled("setSafetySourceData")) {
|
|
return;
|
|
}
|
|
// TODO(b/218812582): Validate the SafetySourceData.
|
|
|
|
SafetyCenterData safetyCenterData;
|
|
RemoteCallbackList<IOnSafetyCenterDataChangedListener> listeners;
|
|
synchronized (mApiLock) {
|
|
safetyCenterData =
|
|
mSafetyCenterDataTracker.setSafetySourceData(
|
|
safetySourceId, safetySourceData, packageName, userId);
|
|
listeners = mSafetyCenterListeners.getListeners(userId);
|
|
}
|
|
// This doesn't need to be done while holding the lock, as RemoteCallbackList already
|
|
// handles concurrent calls.
|
|
// If the listener uses SafetyCenterManager and is executed on #directExecutor(),
|
|
// doing this while holding the lock could also potentially lead to deadlocks.
|
|
if (listeners != null && safetyCenterData != null) {
|
|
// TODO(b/218811189): This should be called on all listeners associated with the
|
|
// userId, i.e. if #setSafetySourceData is called with a work profile userId,
|
|
// we should also let the personal profile listeners know about the update.
|
|
SafetyCenterListeners.deliverUpdate(listeners, safetyCenterData);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public SafetySourceData getSafetySourceData(
|
|
@NonNull String safetySourceId,
|
|
@NonNull String packageName,
|
|
@UserIdInt int userId) {
|
|
mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "getSafetySourceData", getContext());
|
|
// TODO(b/205706756): Security: check certs?
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
SEND_SAFETY_CENTER_UPDATE, "getSafetySourceData");
|
|
if (!checkApiEnabled("getSafetySourceData")) {
|
|
return null;
|
|
}
|
|
|
|
synchronized (mApiLock) {
|
|
return mSafetyCenterDataTracker.getSafetySourceData(
|
|
safetySourceId, packageName, userId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void reportSafetySourceError(
|
|
@NonNull String safetySourceId,
|
|
@NonNull SafetySourceErrorDetails errorDetails,
|
|
@NonNull String packageName,
|
|
@UserIdInt int userId) {
|
|
mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "reportSafetySourceError", getContext());
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
SEND_SAFETY_CENTER_UPDATE, "reportSafetySourceError");
|
|
if (!checkApiEnabled("reportSafetySourceError")) {
|
|
return;
|
|
}
|
|
|
|
// TODO(b/218379298): Add implementation
|
|
RemoteCallbackList<IOnSafetyCenterDataChangedListener> listeners;
|
|
synchronized (mApiLock) {
|
|
listeners = mSafetyCenterListeners.getListeners(userId);
|
|
}
|
|
|
|
SafetyCenterListeners.deliverError(listeners, new SafetyCenterErrorDetails("Error"));
|
|
}
|
|
|
|
@Override
|
|
public void refreshSafetySources(@RefreshReason int refreshReason, @UserIdInt int userId) {
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "refreshSafetySources", getContext());
|
|
getContext().enforceCallingPermission(MANAGE_SAFETY_CENTER, "refreshSafetySources");
|
|
if (!checkApiEnabled("refreshSafetySources")) {
|
|
return;
|
|
}
|
|
|
|
// We don't require the caller to have INTERACT_ACROSS_USERS and
|
|
// START_FOREGROUND_SERVICES_FROM_BACKGROUND permissions.
|
|
final long callingId = Binder.clearCallingIdentity();
|
|
try {
|
|
synchronized (mRefreshLock) {
|
|
mSafetyCenterRefreshManager.refreshSafetySources(refreshReason);
|
|
}
|
|
} finally {
|
|
Binder.restoreCallingIdentity(callingId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public SafetyCenterConfig getSafetyCenterConfig() {
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(MANAGE_SAFETY_CENTER, "getSafetyCenterConfig");
|
|
|
|
synchronized (mApiLock) {
|
|
return mSafetyCenterConfigReader.getSafetyCenterConfig();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@NonNull
|
|
public SafetyCenterData getSafetyCenterData(@UserIdInt int userId) {
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "getSafetyCenterData", getContext());
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(MANAGE_SAFETY_CENTER, "getSafetyCenterData");
|
|
if (!checkApiEnabled("getSafetyCenterData")) {
|
|
return SafetyCenterDataTracker.getDefaultSafetyCenterData();
|
|
}
|
|
|
|
synchronized (mApiLock) {
|
|
return mSafetyCenterDataTracker.getSafetyCenterData(userId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addOnSafetyCenterDataChangedListener(
|
|
@NonNull IOnSafetyCenterDataChangedListener listener, @UserIdInt int userId) {
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "addOnSafetyCenterDataChangedListener", getContext());
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "addOnSafetyCenterDataChangedListener");
|
|
if (!checkApiEnabled("addOnSafetyCenterDataChangedListener")) {
|
|
return;
|
|
}
|
|
|
|
SafetyCenterData safetyCenterData;
|
|
synchronized (mApiLock) {
|
|
mSafetyCenterListeners.addListener(listener, userId);
|
|
safetyCenterData = mSafetyCenterDataTracker.getSafetyCenterData(userId);
|
|
}
|
|
// This doesn't need to be done while holding the lock.
|
|
// If the listener uses SafetyCenterManager and is executed on #directExecutor(),
|
|
// doing this while holding the lock could also potentially lead to deadlocks.
|
|
SafetyCenterListeners.deliverUpdate(listener, safetyCenterData);
|
|
}
|
|
|
|
@Override
|
|
public void removeOnSafetyCenterDataChangedListener(
|
|
@NonNull IOnSafetyCenterDataChangedListener listener, @UserIdInt int userId) {
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "removeOnSafetyCenterDataChangedListener", getContext());
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "removeOnSafetyCenterDataChangedListener");
|
|
if (!checkApiEnabled("removeOnSafetyCenterDataChangedListener")) {
|
|
return;
|
|
}
|
|
|
|
synchronized (mApiLock) {
|
|
mSafetyCenterListeners.removeListener(listener, userId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void dismissSafetyCenterIssue(String issueId, @UserIdInt int userId) {
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "dismissSafetyCenterIssue", getContext());
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "dismissSafetyCenterIssue");
|
|
if (!checkApiEnabled("dismissSafetyCenterIssue")) {
|
|
return;
|
|
}
|
|
// TODO(b/202387059): Implement issue dismissal.
|
|
|
|
}
|
|
|
|
@Override
|
|
public void executeSafetyCenterIssueAction(
|
|
@NonNull String safetyCenterIssueId,
|
|
@NonNull String safetyCenterActionId,
|
|
@UserIdInt int userId) {
|
|
// TODO(b/217235899): Finalize cross-user behavior.
|
|
PermissionUtils.enforceCrossUserPermission(
|
|
userId, false, "executeSafetyCenterIssueAction", getContext());
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "executeSafetyCenterIssueAction");
|
|
if (!checkApiEnabled("executeSafetyCenterIssueAction")) {
|
|
return;
|
|
}
|
|
// TODO(b/218379298): Add implementation
|
|
}
|
|
|
|
@Override
|
|
public void clearAllSafetySourceDataForTests() {
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "clearAllSafetySourceDataForTests");
|
|
if (!checkApiEnabled("clearAllSafetySourceDataForTests")) {
|
|
return;
|
|
}
|
|
|
|
synchronized (mApiLock) {
|
|
mSafetyCenterDataTracker.clear();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setSafetyCenterConfigForTests(@NonNull SafetyCenterConfig safetyCenterConfig) {
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "setSafetyCenterConfigForTests");
|
|
if (!checkApiEnabled("setSafetyCenterConfigForTests")) {
|
|
return;
|
|
}
|
|
|
|
synchronized (mRefreshLock) {
|
|
// TODO(b/217944317): Implement properly by overriding config in
|
|
// SafetyCenterConfigReader instead. This placeholder impl serves to allow this
|
|
// API to be merged in tm-dev, and final impl will be in tm-mainline-prod.
|
|
for (int i = 0; i < safetyCenterConfig.getSafetySourcesGroups().size(); i++) {
|
|
SafetySourcesGroup group = safetyCenterConfig.getSafetySourcesGroups().get(i);
|
|
for (int j = 0; j < group.getSafetySources().size(); j++) {
|
|
SafetySource safetySource = group.getSafetySources().get(j);
|
|
if (safetySource.getType() != SAFETY_SOURCE_TYPE_STATIC) {
|
|
mSafetyCenterRefreshManager.addAdditionalSafetySourcePackageNames(
|
|
safetySource.getPackageName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void clearSafetyCenterConfigForTests() {
|
|
getContext()
|
|
.enforceCallingOrSelfPermission(
|
|
MANAGE_SAFETY_CENTER, "clearSafetyCenterConfigForTests");
|
|
if (!checkApiEnabled("clearSafetyCenterConfigForTests")) {
|
|
return;
|
|
}
|
|
|
|
synchronized (mRefreshLock) {
|
|
mSafetyCenterRefreshManager.clearAdditionalSafetySourcePackageNames();
|
|
}
|
|
}
|
|
|
|
private boolean isApiEnabled() {
|
|
return getSafetyCenterConfigValue() && getDeviceConfigSafetyCenterEnabledProperty();
|
|
}
|
|
|
|
private boolean getDeviceConfigSafetyCenterEnabledProperty() {
|
|
// This call requires the READ_DEVICE_CONFIG permission.
|
|
final long callingId = Binder.clearCallingIdentity();
|
|
try {
|
|
return DeviceConfig.getBoolean(
|
|
DeviceConfig.NAMESPACE_PRIVACY,
|
|
PROPERTY_SAFETY_CENTER_ENABLED,
|
|
/* defaultValue = */ false);
|
|
} finally {
|
|
Binder.restoreCallingIdentity(callingId);
|
|
}
|
|
}
|
|
|
|
private boolean getSafetyCenterConfigValue() {
|
|
return getContext()
|
|
.getResources()
|
|
.getBoolean(
|
|
Resources.getSystem()
|
|
.getIdentifier("config_enableSafetyCenter", "bool", "android"));
|
|
}
|
|
|
|
private void enforceAnyCallingOrSelfPermissions(
|
|
@NonNull String message, String... permissions) {
|
|
if (permissions.length == 0) {
|
|
throw new IllegalArgumentException("Must check at least one permission");
|
|
}
|
|
for (int i = 0; i < permissions.length; i++) {
|
|
if (getContext().checkCallingOrSelfPermission(permissions[i])
|
|
== PackageManager.PERMISSION_GRANTED) {
|
|
return;
|
|
}
|
|
}
|
|
throw new SecurityException(
|
|
message
|
|
+ " requires any of: "
|
|
+ Arrays.toString(permissions)
|
|
+ ", but none were granted");
|
|
}
|
|
|
|
private boolean checkApiEnabled(@NonNull String message) {
|
|
if (!isApiEnabled()) {
|
|
Log.w(TAG, String.format("Called %s, but Safety Center is disabled", message));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|