/* * 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.Manifest.permission.START_TASKS_FROM_RECENTS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_OTHER; import static android.safetycenter.SafetyCenterManager.RefreshReason; import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED; import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED; import static com.android.permission.PermissionStatsLog.SAFETY_CENTER_SYSTEM_EVENT_REPORTED__RESULT__TIMEOUT; import static com.android.permission.PermissionStatsLog.SAFETY_STATE; import static com.android.safetycenter.SafetyCenterFlags.PROPERTY_SAFETY_CENTER_ENABLED; import static com.android.safetycenter.internaldata.SafetyCenterIds.toUserFriendlyString; import static java.util.Objects.requireNonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.app.StatsManager; import android.app.StatsManager.StatsPullAtomCallback; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.PackageInfoFlags; import android.content.res.Resources; import android.os.Binder; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.DeviceConfig.OnPropertiesChangedListener; import android.safetycenter.IOnSafetyCenterDataChangedListener; import android.safetycenter.ISafetyCenterManager; import android.safetycenter.SafetyCenterData; import android.safetycenter.SafetyCenterErrorDetails; import android.safetycenter.SafetyCenterManager; import android.safetycenter.SafetyEvent; import android.safetycenter.SafetySourceData; import android.safetycenter.SafetySourceErrorDetails; import android.safetycenter.SafetySourceIssue; import android.safetycenter.config.SafetyCenterConfig; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import androidx.annotation.Keep; import androidx.annotation.RequiresApi; import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.BackgroundThread; import com.android.permission.util.ForegroundThread; import com.android.permission.util.UserUtils; import com.android.safetycenter.data.SafetyCenterDataManager; import com.android.safetycenter.internaldata.SafetyCenterIds; import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; import com.android.safetycenter.internaldata.SafetyCenterIssueId; import com.android.safetycenter.internaldata.SafetyCenterIssueKey; import com.android.safetycenter.logging.SafetyCenterPullAtomCallback; import com.android.safetycenter.notifications.SafetyCenterNotificationChannels; import com.android.safetycenter.notifications.SafetyCenterNotificationReceiver; import com.android.safetycenter.notifications.SafetyCenterNotificationSender; import com.android.safetycenter.pendingintents.PendingIntentSender; import com.android.safetycenter.resources.SafetyCenterResourcesContext; import com.android.server.SystemService; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; import javax.annotation.concurrent.NotThreadSafe; /** * Service for the safety center. * * @hide */ @Keep @RequiresApi(TIRAMISU) public final class SafetyCenterService extends SystemService { private static final String TAG = "SafetyCenterService"; private final ApiLock mApiLock = new ApiLock(); @GuardedBy("mApiLock") private final SafetyCenterTimeouts mSafetyCenterTimeouts = new SafetyCenterTimeouts(); private final SafetyCenterResourcesContext mSafetyCenterResourcesContext; private final SafetyCenterNotificationChannels mNotificationChannels; @GuardedBy("mApiLock") private final SafetyCenterConfigReader mSafetyCenterConfigReader; @GuardedBy("mApiLock") private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker; @GuardedBy("mApiLock") private final SafetyCenterDataManager mSafetyCenterDataManager; @GuardedBy("mApiLock") private final SafetyCenterDataFactory mSafetyCenterDataFactory; @GuardedBy("mApiLock") private final SafetyCenterListeners mSafetyCenterListeners; @GuardedBy("mApiLock") private final SafetyCenterNotificationSender mNotificationSender; @GuardedBy("mApiLock") private final SafetyCenterBroadcastDispatcher mSafetyCenterBroadcastDispatcher; @GuardedBy("mApiLock") private final SafetyCenterDataChangeNotifier mSafetyCenterDataChangeNotifier; private final StatsPullAtomCallback mPullAtomCallback; private final boolean mDeviceSupportsSafetyCenter; /** Whether the {@link SafetyCenterConfig} was successfully loaded. */ private volatile boolean mConfigAvailable; public SafetyCenterService(Context context) { super(context); mSafetyCenterResourcesContext = new SafetyCenterResourcesContext(context); mSafetyCenterConfigReader = new SafetyCenterConfigReader(mSafetyCenterResourcesContext); mSafetyCenterRefreshTracker = new SafetyCenterRefreshTracker(context); mSafetyCenterDataManager = new SafetyCenterDataManager( context, mSafetyCenterConfigReader, mSafetyCenterRefreshTracker, mApiLock); mSafetyCenterDataFactory = new SafetyCenterDataFactory( context, mSafetyCenterResourcesContext, mSafetyCenterConfigReader, mSafetyCenterRefreshTracker, new PendingIntentFactory(context, mSafetyCenterResourcesContext), mSafetyCenterDataManager); mSafetyCenterListeners = new SafetyCenterListeners(mSafetyCenterDataFactory); mNotificationChannels = new SafetyCenterNotificationChannels(mSafetyCenterResourcesContext); mNotificationSender = SafetyCenterNotificationSender.newInstance( context, mSafetyCenterResourcesContext, mNotificationChannels, mSafetyCenterDataManager); mSafetyCenterBroadcastDispatcher = new SafetyCenterBroadcastDispatcher( context, mSafetyCenterConfigReader, mSafetyCenterRefreshTracker, mSafetyCenterDataManager); mPullAtomCallback = new SafetyCenterPullAtomCallback( context, mApiLock, mSafetyCenterConfigReader, mSafetyCenterDataFactory, mSafetyCenterDataManager); mSafetyCenterDataChangeNotifier = new SafetyCenterDataChangeNotifier(mNotificationSender, mSafetyCenterListeners); mDeviceSupportsSafetyCenter = context.getResources() .getBoolean( Resources.getSystem() .getIdentifier( "config_enableSafetyCenter", "bool", "android")); if (!mDeviceSupportsSafetyCenter) { Log.i(TAG, "Device does not support safety center, safety center will be disabled."); } } @Override public void onStart() { publishBinderService(Context.SAFETY_CENTER_SERVICE, new Stub()); if (mDeviceSupportsSafetyCenter) { synchronized (mApiLock) { mSafetyCenterResourcesContext.init(); SafetyCenterFlags.init(mSafetyCenterResourcesContext); mConfigAvailable = mSafetyCenterConfigReader.loadConfig(); if (mConfigAvailable) { mSafetyCenterDataManager.loadPersistableDataStateFromFile(); new UserBroadcastReceiver().register(getContext()); new SafetyCenterNotificationReceiver( this, mSafetyCenterDataManager, mSafetyCenterDataChangeNotifier, mApiLock) .register(getContext()); new LocaleBroadcastReceiver().register(getContext()); } } } } @Override public void onBootPhase(int phase) { if (phase == SystemService.PHASE_BOOT_COMPLETED && canUseSafetyCenter()) { registerSafetyCenterEnabledListener(); registerSafetyCenterPullAtomCallback(); mNotificationChannels.createAllChannelsForAllUsers(getContext()); } } private void registerSafetyCenterEnabledListener() { Executor foregroundThreadExecutor = ForegroundThread.getExecutor(); SafetyCenterEnabledListener listener = new SafetyCenterEnabledListener(); // Ensure the listener is called first with the current state on the same thread. foregroundThreadExecutor.execute(listener::setInitialState); DeviceConfig.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_PRIVACY, foregroundThreadExecutor, listener); } private void registerSafetyCenterPullAtomCallback() { StatsManager statsManager = requireNonNull(getContext().getSystemService(StatsManager.class)); statsManager.setPullAtomCallback( SAFETY_STATE, null, BackgroundThread.getExecutor(), mPullAtomCallback); } /** 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); return isApiEnabled(); } @Override public void setSafetySourceData( String safetySourceId, @Nullable SafetySourceData safetySourceData, SafetyEvent safetyEvent, String packageName, @UserIdInt int userId) { requireNonNull(safetySourceId); requireNonNull(safetyEvent); requireNonNull(packageName); getContext() .enforceCallingOrSelfPermission( SEND_SAFETY_CENTER_UPDATE, "setSafetySourceData"); if (!enforceCrossUserPermission("setSafetySourceData", userId) || !enforcePackage(Binder.getCallingUid(), packageName, userId) || !checkApiEnabled("setSafetySourceData")) { return; } UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); synchronized (mApiLock) { boolean hasUpdate = mSafetyCenterDataManager.setSafetySourceData( safetySourceData, safetySourceId, safetyEvent, packageName, userId); if (hasUpdate) { // When an action is successfully resolved, call notifyActionSuccess before // updateDataConsumers: Calling the former first will turn any notification for // the resolved issue into a success notification, whereas calling the latter // will simply clear any issue notification and no success message will show. if (safetyEvent.getType() == SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) { mNotificationSender.notifyActionSuccess( safetySourceId, safetyEvent, userId); } mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup, userId); } } } @Override @Nullable public SafetySourceData getSafetySourceData( String safetySourceId, String packageName, @UserIdInt int userId) { requireNonNull(safetySourceId); requireNonNull(packageName); getContext() .enforceCallingOrSelfPermission( SEND_SAFETY_CENTER_UPDATE, "getSafetySourceData"); if (!enforceCrossUserPermission("getSafetySourceData", userId) || !enforcePackage(Binder.getCallingUid(), packageName, userId) || !checkApiEnabled("getSafetySourceData")) { return null; } synchronized (mApiLock) { return mSafetyCenterDataManager.getSafetySourceData( safetySourceId, packageName, userId); } } @Override public void reportSafetySourceError( String safetySourceId, SafetySourceErrorDetails errorDetails, String packageName, @UserIdInt int userId) { requireNonNull(safetySourceId); requireNonNull(errorDetails); requireNonNull(packageName); getContext() .enforceCallingOrSelfPermission( SEND_SAFETY_CENTER_UPDATE, "reportSafetySourceError"); if (!enforceCrossUserPermission("reportSafetySourceError", userId) || !enforcePackage(Binder.getCallingUid(), packageName, userId) || !checkApiEnabled("reportSafetySourceError")) { return; } UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); synchronized (mApiLock) { boolean hasUpdate = mSafetyCenterDataManager.reportSafetySourceError( errorDetails, safetySourceId, packageName, userId); SafetyCenterErrorDetails safetyCenterErrorDetails = null; if (hasUpdate && errorDetails.getSafetyEvent().getType() == SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED) { safetyCenterErrorDetails = new SafetyCenterErrorDetails( mSafetyCenterResourcesContext.getStringByName( "resolving_action_error")); } if (hasUpdate) { mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup, userId); } if (safetyCenterErrorDetails != null) { mSafetyCenterListeners.deliverErrorForUserProfileGroup( userProfileGroup, safetyCenterErrorDetails); } } } @Override public void refreshSafetySources(@RefreshReason int refreshReason, @UserIdInt int userId) { RefreshReasons.validate(refreshReason); getContext().enforceCallingPermission(MANAGE_SAFETY_CENTER, "refreshSafetySources"); if (!enforceCrossUserPermission("refreshSafetySources", userId) || !checkApiEnabled("refreshSafetySources")) { return; } startRefreshingSafetySources(refreshReason, userId); } @Override @RequiresApi(UPSIDE_DOWN_CAKE) public void refreshSpecificSafetySources( @RefreshReason int refreshReason, @UserIdInt int userId, List safetySourceIds) { requireNonNull(safetySourceIds, "safetySourceIds cannot be null"); RefreshReasons.validate(refreshReason); getContext() .enforceCallingPermission(MANAGE_SAFETY_CENTER, "refreshSpecificSafetySources"); if (!enforceCrossUserPermission("refreshSpecificSafetySources", userId) || !checkApiEnabled("refreshSpecificSafetySources")) { return; } startRefreshingSafetySources(refreshReason, userId, safetySourceIds); } @Override @Nullable public SafetyCenterConfig getSafetyCenterConfig() { getContext() .enforceCallingOrSelfPermission(MANAGE_SAFETY_CENTER, "getSafetyCenterConfig"); // We still return the SafetyCenterConfig object when the API is disabled, as Settings // search works by adding all the entries very rarely (and relies on filtering them out // instead). if (!canUseSafetyCenter()) { Log.w(TAG, "Called getSafetyCenterConfig, but Safety Center is not supported"); return null; } synchronized (mApiLock) { return mSafetyCenterConfigReader.getSafetyCenterConfig(); } } @Override public SafetyCenterData getSafetyCenterData(String packageName, @UserIdInt int userId) { requireNonNull(packageName); getContext() .enforceCallingOrSelfPermission(MANAGE_SAFETY_CENTER, "getSafetyCenterData"); if (!enforceCrossUserPermission("getSafetyCenterData", userId) || !enforcePackage(Binder.getCallingUid(), packageName, userId) || !checkApiEnabled("getSafetyCenterData")) { return SafetyCenterDataFactory.getDefaultSafetyCenterData(); } UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); synchronized (mApiLock) { return mSafetyCenterDataFactory.assembleSafetyCenterData( packageName, userProfileGroup); } } @Override public void addOnSafetyCenterDataChangedListener( IOnSafetyCenterDataChangedListener listener, String packageName, @UserIdInt int userId) { requireNonNull(listener); requireNonNull(packageName); getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "addOnSafetyCenterDataChangedListener"); if (!enforceCrossUserPermission("addOnSafetyCenterDataChangedListener", userId) || !enforcePackage(Binder.getCallingUid(), packageName, userId) || !checkApiEnabled("addOnSafetyCenterDataChangedListener")) { return; } UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); synchronized (mApiLock) { IOnSafetyCenterDataChangedListener registeredListener = mSafetyCenterListeners.addListener(listener, packageName, userId); if (registeredListener == null) { return; } SafetyCenterListeners.deliverDataForListener( registeredListener, mSafetyCenterDataFactory.assembleSafetyCenterData( packageName, userProfileGroup)); } } @Override public void removeOnSafetyCenterDataChangedListener( IOnSafetyCenterDataChangedListener listener, @UserIdInt int userId) { requireNonNull(listener); getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "removeOnSafetyCenterDataChangedListener"); if (!enforceCrossUserPermission("removeOnSafetyCenterDataChangedListener", userId) || !checkApiEnabled("removeOnSafetyCenterDataChangedListener")) { return; } synchronized (mApiLock) { mSafetyCenterListeners.removeListener(listener, userId); } } @Override public void dismissSafetyCenterIssue(String issueId, @UserIdInt int userId) { requireNonNull(issueId); getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "dismissSafetyCenterIssue"); if (!enforceCrossUserPermission("dismissSafetyCenterIssue", userId) || !checkApiEnabled("dismissSafetyCenterIssue")) { return; } SafetyCenterIssueId safetyCenterIssueId = SafetyCenterIds.issueIdFromString(issueId); SafetyCenterIssueKey safetyCenterIssueKey = safetyCenterIssueId.getSafetyCenterIssueKey(); UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); enforceSameUserProfileGroup( "dismissSafetyCenterIssue", userProfileGroup, safetyCenterIssueKey.getUserId()); synchronized (mApiLock) { SafetySourceIssue safetySourceIssue = mSafetyCenterDataManager.getSafetySourceIssue(safetyCenterIssueKey); if (safetySourceIssue == null) { Log.w(TAG, "Attempt to dismiss an issue that is not provided by the source"); // Don't send the error to the UI here, since it could happen when clicking the // button multiple times in a row (e.g. if the source is clearing the issue as a // result of the onDismissPendingIntent). return; } if (mSafetyCenterDataManager.isIssueDismissed( safetyCenterIssueKey, safetySourceIssue.getSeverityLevel())) { Log.w(TAG, "Attempt to dismiss an issue that is already dismissed"); // Don't send the error to the UI here, since it could happen when clicking the // button multiple times in a row. return; } mSafetyCenterDataManager.dismissSafetyCenterIssue(safetyCenterIssueKey); PendingIntent onDismissPendingIntent = safetySourceIssue.getOnDismissPendingIntent(); if (onDismissPendingIntent != null && !dispatchPendingIntent(onDismissPendingIntent, null)) { Log.w( TAG, "Error dispatching dismissal for issue: " + safetyCenterIssueKey.getSafetySourceIssueId() + ", of source: " + safetyCenterIssueKey.getSafetySourceId()); // We still consider the dismissal a success if there is an error dispatching // the dismissal PendingIntent, since SafetyCenter won't surface this warning // anymore. } mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup, userId); } } @Override public void executeSafetyCenterIssueAction( String issueId, String issueActionId, @UserIdInt int userId) { requireNonNull(issueId); requireNonNull(issueActionId); getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "executeSafetyCenterIssueAction"); if (!enforceCrossUserPermission("executeSafetyCenterIssueAction", userId) || !checkApiEnabled("executeSafetyCenterIssueAction")) { return; } SafetyCenterIssueId safetyCenterIssueId = SafetyCenterIds.issueIdFromString(issueId); SafetyCenterIssueKey safetyCenterIssueKey = safetyCenterIssueId.getSafetyCenterIssueKey(); SafetyCenterIssueActionId safetyCenterIssueActionId = SafetyCenterIds.issueActionIdFromString(issueActionId); if (!safetyCenterIssueActionId.getSafetyCenterIssueKey().equals(safetyCenterIssueKey)) { throw new IllegalArgumentException( toUserFriendlyString(safetyCenterIssueId) + " and " + toUserFriendlyString(safetyCenterIssueActionId) + " do not match"); } UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); enforceSameUserProfileGroup( "executeSafetyCenterIssueAction", userProfileGroup, safetyCenterIssueKey.getUserId()); Integer taskId = safetyCenterIssueId.hasTaskId() ? safetyCenterIssueId.getTaskId() : null; executeIssueActionInternal(safetyCenterIssueActionId, userProfileGroup, taskId); } @Override public void clearAllSafetySourceDataForTests() { getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "clearAllSafetySourceDataForTests"); if (!checkApiEnabled("clearAllSafetySourceDataForTests")) { return; } List userProfileGroups = UserProfileGroup.getAllUserProfileGroups(getContext()); synchronized (mApiLock) { // TODO(b/236693607): Should tests leave real data untouched? clearDataLocked(); mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroups); } } @Override public void setSafetyCenterConfigForTests(SafetyCenterConfig safetyCenterConfig) { requireNonNull(safetyCenterConfig); getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "setSafetyCenterConfigForTests"); if (!checkApiEnabled("setSafetyCenterConfigForTests")) { return; } List userProfileGroups = UserProfileGroup.getAllUserProfileGroups(getContext()); synchronized (mApiLock) { mSafetyCenterConfigReader.setConfigOverrideForTests(safetyCenterConfig); // TODO(b/236693607): Should tests leave real data untouched? clearDataLocked(); mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroups); } } @Override public void clearSafetyCenterConfigForTests() { getContext() .enforceCallingOrSelfPermission( MANAGE_SAFETY_CENTER, "clearSafetyCenterConfigForTests"); if (!checkApiEnabled("clearSafetyCenterConfigForTests")) { return; } List userProfileGroups = UserProfileGroup.getAllUserProfileGroups(getContext()); synchronized (mApiLock) { mSafetyCenterConfigReader.clearConfigOverrideForTests(); // TODO(b/236693607): Should tests leave real data untouched? clearDataLocked(); mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroups); } } private boolean isApiEnabled() { return canUseSafetyCenter() && SafetyCenterFlags.getSafetyCenterEnabled(); } private void enforceAnyCallingOrSelfPermissions(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]) == PERMISSION_GRANTED) { return; } } throw new SecurityException( message + " requires any of: " + Arrays.toString(permissions) + ", but none were granted"); } /** Enforces cross user permission and returns whether the user is valid. */ private boolean enforceCrossUserPermission(String message, @UserIdInt int userId) { UserUtils.enforceCrossUserPermission(userId, false, message, getContext()); if (!UserUtils.isUserExistent(userId, getContext())) { Log.w( TAG, "Called " + message + " with user id " + userId + ", which does not correspond to an existing user"); return false; } if (!UserProfileGroup.isSupported(userId, getContext())) { Log.w( TAG, "Called " + message + " with user id " + userId + ", which is an unsupported user"); return false; } return true; } /** * Returns {@code true} if the {@code packageName} exists and it belongs to the {@code * callingUid}. * *

Throws a {@link SecurityException} if the {@code packageName} does not belong to the * {@code callingUid}. */ private boolean enforcePackage(int callingUid, String packageName, @UserIdInt int userId) { if (TextUtils.isEmpty(packageName)) { throw new IllegalArgumentException("packageName may not be empty"); } int actualUid; PackageManager packageManager = getContext().getPackageManager(); try { actualUid = packageManager.getPackageUidAsUser( packageName, PackageInfoFlags.of(0), userId); } catch (NameNotFoundException e) { Log.e(TAG, "packageName=" + packageName + ", not found for userId=" + userId, e); return false; } if (callingUid == Process.ROOT_UID || callingUid == Process.SYSTEM_UID) { return true; } if (UserHandle.getAppId(callingUid) != UserHandle.getAppId(actualUid)) { throw new SecurityException( "packageName=" + packageName + ", does not belong to callingUid=" + callingUid); } return true; } private boolean checkApiEnabled(String message) { if (!isApiEnabled()) { Log.w(TAG, "Called " + message + ", but Safety Center is disabled"); return false; } return true; } private void enforceSameUserProfileGroup( String message, UserProfileGroup userProfileGroup, @UserIdInt int userId) { if (!userProfileGroup.contains(userId)) { throw new SecurityException( message + " requires target user id " + userId + " to be within the same profile group of the caller: " + userProfileGroup); } } @Override public int handleShellCommand( ParcelFileDescriptor in, ParcelFileDescriptor out, ParcelFileDescriptor err, String[] args) { return new SafetyCenterShellCommandHandler( getContext(), this, mDeviceSupportsSafetyCenter) .exec( this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } /** Dumps state for debugging purposes. */ @Override protected void dump(FileDescriptor fd, PrintWriter fout, @Nullable String[] args) { if (!checkDumpPermission(fout)) { return; } List subjects = Arrays.asList(args); boolean all = subjects.isEmpty(); synchronized (mApiLock) { if (all || subjects.contains("service")) { SafetyCenterService.this.dumpLocked(fout); } if (all || subjects.contains("flags")) { SafetyCenterFlags.dump(fout); } if (all || subjects.contains("config")) { mSafetyCenterConfigReader.dump(fout); } if (all || subjects.contains("data")) { mSafetyCenterDataManager.dump(fd, fout); } if (all || subjects.contains("refresh")) { mSafetyCenterRefreshTracker.dump(fout); } if (all || subjects.contains("timeouts")) { mSafetyCenterTimeouts.dump(fout); } if (all || subjects.contains("listeners")) { mSafetyCenterListeners.dump(fout); } if (all || subjects.contains("notifications")) { mNotificationSender.dump(fout); } } } private boolean checkDumpPermission(PrintWriter writer) { if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PERMISSION_GRANTED) { writer.println( "Permission Denial: can't dump " + "safety_center" + " from from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + " due to missing " + android.Manifest.permission.DUMP + " permission"); return false; } else { return true; } } } /** * An {@link OnPropertiesChangedListener} for {@link * SafetyCenterFlags#PROPERTY_SAFETY_CENTER_ENABLED} that sends broadcasts when the SafetyCenter * property is enabled or disabled. * *

This listener assumes that the {@link SafetyCenterFlags#PROPERTY_SAFETY_CENTER_ENABLED} * value maps to {@link SafetyCenterManager#isSafetyCenterEnabled()}. It should only be * registered if the device supports SafetyCenter and the {@link SafetyCenterConfig} was loaded * successfully. * *

This listener is not thread-safe; it should be called on a single thread. */ @NotThreadSafe private final class SafetyCenterEnabledListener implements OnPropertiesChangedListener { private boolean mSafetyCenterEnabled; @Override public void onPropertiesChanged(DeviceConfig.Properties properties) { if (!properties.getKeyset().contains(PROPERTY_SAFETY_CENTER_ENABLED)) { return; } boolean safetyCenterEnabled = properties.getBoolean(PROPERTY_SAFETY_CENTER_ENABLED, false); if (mSafetyCenterEnabled == safetyCenterEnabled) { return; } onSafetyCenterEnabledChanged(safetyCenterEnabled); } private void setInitialState() { mSafetyCenterEnabled = SafetyCenterFlags.getSafetyCenterEnabled(); Log.w(TAG, "SafetyCenter is " + (mSafetyCenterEnabled ? "enabled." : "disabled.")); } private void onSafetyCenterEnabledChanged(boolean safetyCenterEnabled) { Log.w(TAG, "SafetyCenter is now " + (safetyCenterEnabled ? "enabled." : "disabled.")); if (safetyCenterEnabled) { onApiEnabled(); } else { onApiDisabled(); } mSafetyCenterEnabled = safetyCenterEnabled; } private void onApiEnabled() { synchronized (mApiLock) { mSafetyCenterBroadcastDispatcher.sendEnabledChanged(); } } private void onApiDisabled() { synchronized (mApiLock) { clearDataLocked(); mSafetyCenterListeners.clear(); mSafetyCenterBroadcastDispatcher.sendEnabledChanged(); } } } /** A {@link Runnable} that is called to signal a refresh timeout. */ private final class RefreshTimeout implements Runnable { private final String mRefreshBroadcastId; @RefreshReason private final int mRefreshReason; private final UserProfileGroup mUserProfileGroup; RefreshTimeout( String refreshBroadcastId, @RefreshReason int refreshReason, UserProfileGroup userProfileGroup) { mRefreshBroadcastId = refreshBroadcastId; mRefreshReason = refreshReason; mUserProfileGroup = userProfileGroup; } @Override public void run() { synchronized (mApiLock) { mSafetyCenterTimeouts.remove(this); ArraySet stillInFlight = mSafetyCenterRefreshTracker.timeoutRefresh(mRefreshBroadcastId); if (stillInFlight == null) { return; } boolean showErrorEntriesOnTimeout = SafetyCenterFlags.getShowErrorEntriesOnTimeout(); boolean setError = showErrorEntriesOnTimeout && !RefreshReasons.isBackgroundRefresh(mRefreshReason); for (int i = 0; i < stillInFlight.size(); i++) { mSafetyCenterDataManager.markSafetySourceRefreshTimedOut( stillInFlight.valueAt(i), setError); } mSafetyCenterDataChangeNotifier.updateDataConsumers(mUserProfileGroup); if (!showErrorEntriesOnTimeout) { mSafetyCenterListeners.deliverErrorForUserProfileGroup( mUserProfileGroup, new SafetyCenterErrorDetails( mSafetyCenterResourcesContext.getStringByName( "refresh_timeout"))); } } Log.v( TAG, "Cleared refresh with broadcastId:" + mRefreshBroadcastId + " after a timeout"); } @Override public String toString() { return "RefreshTimeout{" + "mRefreshBroadcastId='" + mRefreshBroadcastId + '\'' + ", mUserProfileGroup=" + mUserProfileGroup + '}'; } } /** A {@link Runnable} that is called to signal a resolving action timeout. */ private final class ResolvingActionTimeout implements Runnable { private final SafetyCenterIssueActionId mSafetyCenterIssueActionId; private final UserProfileGroup mUserProfileGroup; ResolvingActionTimeout( SafetyCenterIssueActionId safetyCenterIssueActionId, UserProfileGroup userProfileGroup) { mSafetyCenterIssueActionId = safetyCenterIssueActionId; mUserProfileGroup = userProfileGroup; } @Override public void run() { synchronized (mApiLock) { mSafetyCenterTimeouts.remove(this); SafetySourceIssue safetySourceIssue = mSafetyCenterDataManager.getSafetySourceIssue( mSafetyCenterIssueActionId.getSafetyCenterIssueKey()); boolean safetyCenterDataHasChanged = mSafetyCenterDataManager.unmarkSafetyCenterIssueActionInFlight( mSafetyCenterIssueActionId, safetySourceIssue, SAFETY_CENTER_SYSTEM_EVENT_REPORTED__RESULT__TIMEOUT); if (!safetyCenterDataHasChanged) { return; } mSafetyCenterDataChangeNotifier.updateDataConsumers(mUserProfileGroup); mSafetyCenterListeners.deliverErrorForUserProfileGroup( mUserProfileGroup, new SafetyCenterErrorDetails( mSafetyCenterResourcesContext.getStringByName( "resolving_action_error"))); } } @Override public String toString() { return "ResolvingActionTimeout{" + "mSafetyCenterIssueActionId=" + toUserFriendlyString(mSafetyCenterIssueActionId) + ", mUserProfileGroup=" + mUserProfileGroup + '}'; } } private boolean canUseSafetyCenter() { return mDeviceSupportsSafetyCenter && mConfigAvailable; } /** {@link BroadcastReceiver} which handles Locale changes. */ private final class LocaleBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "LocaleBroadcastReceiver"; void register(Context context) { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); context.registerReceiverForAllUsers(this, filter, null, null); } @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Locale changed broadcast received"); mNotificationChannels.createAllChannelsForAllUsers(getContext()); } } /** * {@link BroadcastReceiver} which handles user and work profile related broadcasts that Safety * Center is interested including quiet mode turning on/off and accounts being added/removed. */ private final class UserBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "UserBroadcastReceiver"; void register(Context context) { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); context.registerReceiverForAllUsers(this, filter, null, null); } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action == null) { Log.w(TAG, "Received broadcast with null action!"); return; } UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class); if (userHandle == null) { Log.w(TAG, "Received " + action + " broadcast missing user extra!"); return; } int userId = userHandle.getIdentifier(); if (!UserProfileGroup.isSupported(userId, context)) { Log.i( TAG, "Received broadcast for user id " + userId + ", which is an unsupported user"); return; } Log.d(TAG, "Received " + action + " broadcast for user " + userId); switch (action) { case Intent.ACTION_USER_REMOVED: case Intent.ACTION_MANAGED_PROFILE_REMOVED: removeUser(userId, true); break; case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE: removeUser(userId, false); // fall through! case Intent.ACTION_USER_ADDED: case Intent.ACTION_MANAGED_PROFILE_ADDED: case Intent.ACTION_MANAGED_PROFILE_AVAILABLE: startRefreshingSafetySources(REFRESH_REASON_OTHER, userId); mNotificationChannels.createAllChannelsForUser(getContext(), userHandle); break; } } } private void removeUser(@UserIdInt int userId, boolean clearDataPermanently) { UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); synchronized (mApiLock) { mSafetyCenterListeners.clearForUser(userId); mSafetyCenterRefreshTracker.clearRefreshForUser(userId); if (clearDataPermanently) { mSafetyCenterDataManager.clearForUser(userId); mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup, userId); } else { mSafetyCenterListeners.deliverDataForUserProfileGroup(userProfileGroup); } } } private void startRefreshingSafetySources( @RefreshReason int refreshReason, @UserIdInt int userId) { startRefreshingSafetySources(refreshReason, userId, null); } private void startRefreshingSafetySources( @RefreshReason int refreshReason, @UserIdInt int userId, @Nullable List selectedSafetySourceIds) { UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId); synchronized (mApiLock) { String refreshBroadcastId = mSafetyCenterBroadcastDispatcher.sendRefreshSafetySources( refreshReason, userProfileGroup, selectedSafetySourceIds); if (refreshBroadcastId == null) { return; } RefreshTimeout refreshTimeout = new RefreshTimeout(refreshBroadcastId, refreshReason, userProfileGroup); mSafetyCenterTimeouts.add( refreshTimeout, SafetyCenterFlags.getRefreshSourcesTimeout(refreshReason)); mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup); } } /** * Executes the {@link SafetySourceIssue.Action} specified by the given {@link * SafetyCenterIssueActionId}. * *

No validation is performed on the contents of the given ID. */ public void executeIssueActionInternal(SafetyCenterIssueActionId safetyCenterIssueActionId) { SafetyCenterIssueKey safetyCenterIssueKey = safetyCenterIssueActionId.getSafetyCenterIssueKey(); UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), safetyCenterIssueKey.getUserId()); executeIssueActionInternal(safetyCenterIssueActionId, userProfileGroup, null); } private void executeIssueActionInternal( SafetyCenterIssueActionId safetyCenterIssueActionId, UserProfileGroup userProfileGroup, @Nullable Integer taskId) { synchronized (mApiLock) { SafetySourceIssue.Action safetySourceIssueAction = mSafetyCenterDataManager.getSafetySourceIssueAction(safetyCenterIssueActionId); if (safetySourceIssueAction == null) { Log.w( TAG, "Attempt to execute an issue action that is not provided by the source," + " that was dismissed, or is already in flight"); // Don't send the error to the UI here, since it could happen when clicking the // button multiple times in a row. return; } PendingIntent issueActionPendingIntent = safetySourceIssueAction.getPendingIntent(); if (!dispatchPendingIntent(issueActionPendingIntent, taskId)) { Log.w( TAG, "Error dispatching action: " + toUserFriendlyString(safetyCenterIssueActionId)); CharSequence errorMessage; if (safetySourceIssueAction.willResolve()) { errorMessage = mSafetyCenterResourcesContext.getStringByName("resolving_action_error"); } else { errorMessage = mSafetyCenterResourcesContext.getStringByName("redirecting_error"); } mSafetyCenterListeners.deliverErrorForUserProfileGroup( userProfileGroup, new SafetyCenterErrorDetails(errorMessage)); return; } if (safetySourceIssueAction.willResolve()) { mSafetyCenterDataManager.markSafetyCenterIssueActionInFlight( safetyCenterIssueActionId); ResolvingActionTimeout resolvingActionTimeout = new ResolvingActionTimeout(safetyCenterIssueActionId, userProfileGroup); mSafetyCenterTimeouts.add( resolvingActionTimeout, SafetyCenterFlags.getResolvingActionTimeout()); mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup); } } } private boolean dispatchPendingIntent( PendingIntent pendingIntent, @Nullable Integer launchTaskId) { if (launchTaskId != null && getContext().checkCallingOrSelfPermission(START_TASKS_FROM_RECENTS) != PERMISSION_GRANTED) { launchTaskId = null; } return PendingIntentSender.trySend(pendingIntent, launchTaskId); } @GuardedBy("mApiLock") private void clearDataLocked() { mSafetyCenterDataManager.clear(); mSafetyCenterTimeouts.clear(); mSafetyCenterRefreshTracker.clearRefresh(); mNotificationSender.cancelAllNotifications(); } /** Dumps state for debugging purposes. */ @GuardedBy("mApiLock") private void dumpLocked(PrintWriter fout) { fout.println("SERVICE"); fout.println( "\tSafetyCenterService{" + "mDeviceSupportsSafetyCenter=" + mDeviceSupportsSafetyCenter + ", mConfigAvailable=" + mConfigAvailable + '}'); fout.println(); } }