| /* |
| * Copyright (C) 2022 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.wm; |
| |
| import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND; |
| import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; |
| import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; |
| import static android.content.pm.PackageManager.PERMISSION_GRANTED; |
| import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; |
| import static android.os.Process.SYSTEM_UID; |
| import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; |
| |
| import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ACTIVITY_STARTS; |
| import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; |
| import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; |
| import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; |
| import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; |
| import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; |
| import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; |
| import static com.android.window.flags.Flags.balShowToasts; |
| import static com.android.window.flags.Flags.balShowToastsBlocked; |
| import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; |
| |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.ActivityManager; |
| import android.app.ActivityOptions; |
| import android.app.AppOpsManager; |
| import android.app.BackgroundStartPrivileges; |
| import android.app.compat.CompatChanges; |
| import android.compat.annotation.ChangeId; |
| import android.compat.annotation.EnabledAfter; |
| import android.content.ComponentName; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.os.Process; |
| import android.os.UserHandle; |
| import android.provider.DeviceConfig; |
| import android.util.ArraySet; |
| import android.util.DebugUtils; |
| import android.util.Slog; |
| import android.widget.Toast; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.FrameworkStatsLog; |
| import com.android.server.UiThread; |
| import com.android.server.am.PendingIntentRecord; |
| import com.android.window.flags.Flags; |
| |
| import java.lang.annotation.Retention; |
| import java.util.HashMap; |
| import java.util.StringJoiner; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.function.Predicate; |
| |
| /** |
| * Helper class to check permissions for starting Activities. |
| * |
| * <p>This class collects all the logic to prevent malicious attempts to start activities. |
| */ |
| public class BackgroundActivityStartController { |
| |
| private static final String TAG = |
| TAG_WITH_CLASS_NAME ? "BackgroundActivityStartController" : TAG_ATM; |
| |
| private static final long ASM_GRACEPERIOD_TIMEOUT_MS = TIMEOUT_MS; |
| private static final int ASM_GRACEPERIOD_MAX_REPEATS = 5; |
| private static final int NO_PROCESS_UID = -1; |
| /** If enabled the creator will not allow BAL on its behalf by default. */ |
| @ChangeId |
| @EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE) |
| private static final long DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR = |
| 296478951; |
| public static final ActivityOptions ACTIVITY_OPTIONS_SYSTEM_DEFINED = |
| ActivityOptions.makeBasic() |
| .setPendingIntentBackgroundActivityStartMode( |
| ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) |
| .setPendingIntentCreatorBackgroundActivityStartMode( |
| ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); |
| |
| private final ActivityTaskManagerService mService; |
| |
| private final ActivityTaskSupervisor mSupervisor; |
| |
| // TODO(b/263368846) Rename when ASM logic is moved in |
| @Retention(SOURCE) |
| @IntDef({BAL_BLOCK, |
| BAL_ALLOW_DEFAULT, |
| BAL_ALLOW_ALLOWLISTED_UID, |
| BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| BAL_ALLOW_VISIBLE_WINDOW, |
| BAL_ALLOW_PENDING_INTENT, |
| BAL_ALLOW_PERMISSION, |
| BAL_ALLOW_SAW_PERMISSION, |
| BAL_ALLOW_GRACE_PERIOD, |
| BAL_ALLOW_FOREGROUND, |
| BAL_ALLOW_SDK_SANDBOX |
| }) |
| public @interface BalCode {} |
| |
| static final int BAL_BLOCK = 0; |
| |
| static final int BAL_ALLOW_DEFAULT = 1; |
| |
| // Following codes are in order of precedence |
| |
| /** Important UIDs which should be always allowed to launch activities */ |
| static final int BAL_ALLOW_ALLOWLISTED_UID = 2; |
| |
| /** Apps that fulfill a certain role that can can always launch new tasks */ |
| static final int BAL_ALLOW_ALLOWLISTED_COMPONENT = 3; |
| |
| /** Apps which currently have a visible window or are bound by a service with a visible |
| * window */ |
| static final int BAL_ALLOW_VISIBLE_WINDOW = 4; |
| |
| /** Allowed due to the PendingIntent sender */ |
| static final int BAL_ALLOW_PENDING_INTENT = 5; |
| |
| /** App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges |
| * granted to it */ |
| static final int BAL_ALLOW_PERMISSION = 6; |
| |
| /** Process has SYSTEM_ALERT_WINDOW permission granted to it */ |
| static final int BAL_ALLOW_SAW_PERMISSION = 7; |
| |
| /** App is in grace period after an activity was started or finished */ |
| static final int BAL_ALLOW_GRACE_PERIOD = 8; |
| |
| /** App is in a foreground task or bound to a foreground service (but not itself visible) */ |
| static final int BAL_ALLOW_FOREGROUND = 9; |
| |
| /** Process belongs to a SDK sandbox */ |
| static final int BAL_ALLOW_SDK_SANDBOX = 10; |
| |
| static String balCodeToString(@BalCode int balCode) { |
| switch (balCode) { |
| case BAL_ALLOW_ALLOWLISTED_COMPONENT: |
| return "BAL_ALLOW_ALLOWLISTED_COMPONENT"; |
| case BAL_ALLOW_ALLOWLISTED_UID: |
| return "BAL_ALLOW_ALLOWLISTED_UID"; |
| case BAL_ALLOW_DEFAULT: |
| return "BAL_ALLOW_DEFAULT"; |
| case BAL_ALLOW_FOREGROUND: |
| return "BAL_ALLOW_FOREGROUND"; |
| case BAL_ALLOW_GRACE_PERIOD: |
| return "BAL_ALLOW_GRACE_PERIOD"; |
| case BAL_ALLOW_PENDING_INTENT: |
| return "BAL_ALLOW_PENDING_INTENT"; |
| case BAL_ALLOW_PERMISSION: |
| return "BAL_ALLOW_PERMISSION"; |
| case BAL_ALLOW_SAW_PERMISSION: |
| return "BAL_ALLOW_SAW_PERMISSION"; |
| case BAL_ALLOW_SDK_SANDBOX: |
| return "BAL_ALLOW_SDK_SANDBOX"; |
| case BAL_ALLOW_VISIBLE_WINDOW: |
| return "BAL_ALLOW_VISIBLE_WINDOW"; |
| case BAL_BLOCK: |
| return "BAL_BLOCK"; |
| default: |
| throw new IllegalArgumentException("Unexpected value: " + balCode); |
| } |
| } |
| |
| @GuardedBy("mService.mGlobalLock") |
| private HashMap<Integer, FinishedActivityEntry> mTaskIdToFinishedActivity = new HashMap<>(); |
| @GuardedBy("mService.mGlobalLock") |
| private FinishedActivityEntry mTopFinishedActivity = null; |
| |
| BackgroundActivityStartController( |
| final ActivityTaskManagerService service, final ActivityTaskSupervisor supervisor) { |
| mService = service; |
| mSupervisor = supervisor; |
| } |
| |
| private boolean isHomeApp(int uid, @Nullable String packageName) { |
| if (mService.mHomeProcess != null) { |
| // Fast check |
| return uid == mService.mHomeProcess.mUid; |
| } |
| if (packageName == null) { |
| return false; |
| } |
| ComponentName activity = |
| mService.getPackageManagerInternalLocked() |
| .getDefaultHomeActivity(UserHandle.getUserId(uid)); |
| return activity != null && packageName.equals(activity.getPackageName()); |
| } |
| |
| private class BalState { |
| |
| private final String mCallingPackage; |
| private final int mCallingUid; |
| private final int mCallingPid; |
| private final @ActivityTaskManagerService.AppSwitchState int mAppSwitchState; |
| private final boolean mCallingUidHasAnyVisibleWindow; |
| private final @ActivityManager.ProcessState int mCallingUidProcState; |
| private final boolean mIsCallingUidPersistentSystemProcess; |
| private final BackgroundStartPrivileges mBalAllowedByPiSender; |
| private final BackgroundStartPrivileges mBalAllowedByPiCreatorWithHardening; |
| private final BackgroundStartPrivileges mBalAllowedByPiCreator; |
| private final String mRealCallingPackage; |
| private final int mRealCallingUid; |
| private final int mRealCallingPid; |
| private final boolean mRealCallingUidHasAnyVisibleWindow; |
| private final @ActivityManager.ProcessState int mRealCallingUidProcState; |
| private final boolean mIsRealCallingUidPersistentSystemProcess; |
| private final PendingIntentRecord mOriginatingPendingIntent; |
| private final BackgroundStartPrivileges mForcedBalByPiSender; |
| private final Intent mIntent; |
| private final WindowProcessController mCallerApp; |
| private final WindowProcessController mRealCallerApp; |
| private final boolean mIsCallForResult; |
| |
| private BalState(int callingUid, int callingPid, final String callingPackage, |
| int realCallingUid, int realCallingPid, |
| WindowProcessController callerApp, |
| PendingIntentRecord originatingPendingIntent, |
| BackgroundStartPrivileges forcedBalByPiSender, |
| ActivityRecord resultRecord, |
| Intent intent, |
| ActivityOptions checkedOptions) { |
| this.mCallingPackage = callingPackage; |
| mCallingUid = callingUid; |
| mCallingPid = callingPid; |
| mRealCallingUid = realCallingUid; |
| mRealCallingPid = realCallingPid; |
| mCallerApp = callerApp; |
| mForcedBalByPiSender = forcedBalByPiSender; |
| mOriginatingPendingIntent = originatingPendingIntent; |
| mIntent = intent; |
| mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid); |
| mIsCallForResult = resultRecord != null; |
| if (balRequireOptInByPendingIntentCreator() // auto-opt in introduced with this feature |
| && (originatingPendingIntent == null // not a PendingIntent |
| || mIsCallForResult) // sent for result |
| ) { |
| // grant BAL privileges unless explicitly opted out |
| mBalAllowedByPiCreatorWithHardening = mBalAllowedByPiCreator = |
| checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED |
| ? BackgroundStartPrivileges.NONE |
| : BackgroundStartPrivileges.ALLOW_BAL; |
| mBalAllowedByPiSender = |
| checkedOptions.getPendingIntentBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED |
| ? BackgroundStartPrivileges.NONE |
| : BackgroundStartPrivileges.ALLOW_BAL; |
| } else { |
| // for PendingIntents we restrict BAL based on target_sdk |
| mBalAllowedByPiCreatorWithHardening = getBackgroundStartPrivilegesAllowedByCreator( |
| callingUid, callingPackage, checkedOptions); |
| final BackgroundStartPrivileges mBalAllowedByPiCreatorWithoutHardening = |
| checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED |
| ? BackgroundStartPrivileges.NONE |
| : BackgroundStartPrivileges.ALLOW_BAL; |
| mBalAllowedByPiCreator = balRequireOptInByPendingIntentCreator() |
| ? mBalAllowedByPiCreatorWithHardening |
| : mBalAllowedByPiCreatorWithoutHardening; |
| mBalAllowedByPiSender = |
| PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( |
| checkedOptions, realCallingUid, mRealCallingPackage); |
| } |
| mAppSwitchState = mService.getBalAppSwitchesState(); |
| mCallingUidProcState = mService.mActiveUids.getUidState(callingUid); |
| mIsCallingUidPersistentSystemProcess = |
| mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; |
| mCallingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid); |
| if (realCallingUid == NO_PROCESS_UID) { |
| // no process provided |
| mRealCallingUidProcState = PROCESS_STATE_NONEXISTENT; |
| mRealCallingUidHasAnyVisibleWindow = false; |
| mRealCallerApp = null; |
| mIsRealCallingUidPersistentSystemProcess = false; |
| } else if (callingUid == realCallingUid) { |
| mRealCallingUidProcState = mCallingUidProcState; |
| mRealCallingUidHasAnyVisibleWindow = mCallingUidHasAnyVisibleWindow; |
| // In the PendingIntent case callerApp is not passed in, so resolve it ourselves. |
| mRealCallerApp = callerApp == null |
| ? mService.getProcessController(realCallingPid, realCallingUid) |
| : callerApp; |
| mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess; |
| } else { |
| mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid); |
| mRealCallingUidHasAnyVisibleWindow = |
| mService.hasActiveVisibleWindow(realCallingUid); |
| mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid); |
| mIsRealCallingUidPersistentSystemProcess = |
| mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; |
| } |
| } |
| |
| private BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCreator( |
| int callingUid, String callingPackage, ActivityOptions checkedOptions) { |
| switch (checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()) { |
| case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED: |
| return BackgroundStartPrivileges.ALLOW_BAL; |
| case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED: |
| return BackgroundStartPrivileges.NONE; |
| case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: |
| // no explicit choice by the app - let us decide what to do |
| if (callingPackage != null) { |
| // determine based on the calling/creating package |
| boolean changeEnabled = CompatChanges.isChangeEnabled( |
| DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR, |
| callingPackage, |
| UserHandle.getUserHandleForUid(callingUid)); |
| return changeEnabled ? BackgroundStartPrivileges.NONE |
| : BackgroundStartPrivileges.ALLOW_BAL; |
| } |
| // determine based on the calling/creating uid if we cannot determine the |
| // actual package name (e.g. shared uid) |
| boolean changeEnabled = CompatChanges.isChangeEnabled( |
| DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR, |
| callingUid); |
| return changeEnabled ? BackgroundStartPrivileges.NONE |
| : BackgroundStartPrivileges.ALLOW_BAL; |
| default: |
| throw new IllegalStateException("unsupported BackgroundActivityStartMode: " |
| + checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()); |
| } |
| } |
| |
| private String getDebugPackageName(String packageName, int uid) { |
| if (packageName != null) { |
| return packageName; // use actual package |
| } |
| if (uid == 0) { |
| return "root[debugOnly]"; |
| } |
| String name = mService.mContext.getPackageManager().getNameForUid(uid); |
| if (name == null) { |
| name = "uid=" + uid; |
| } |
| return name + "[debugOnly]"; |
| } |
| |
| /** @return valid targetSdk or <code>-1</code> */ |
| private int getTargetSdk(String packageName) { |
| if (packageName == null) { |
| return -1; |
| } |
| try { |
| PackageManager pm = mService.mContext.getPackageManager(); |
| return pm.getTargetSdkVersion(packageName); |
| } catch (Exception e) { |
| return -1; |
| } |
| } |
| |
| private boolean hasRealCaller() { |
| return mRealCallingUid != NO_PROCESS_UID; |
| } |
| |
| private boolean isPendingIntent() { |
| return mOriginatingPendingIntent != null && hasRealCaller(); |
| } |
| |
| private boolean callerIsRealCaller() { |
| return mCallingUid == mRealCallingUid; |
| } |
| |
| private String dump(BalVerdict resultIfPiCreatorAllowsBal, |
| BalVerdict resultIfPiSenderAllowsBal) { |
| StringBuilder sb = new StringBuilder(2048); |
| sb.append("[callingPackage: ") |
| .append(getDebugPackageName(mCallingPackage, mCallingUid)); |
| sb.append("; callingPackageTargetSdk: ").append(getTargetSdk(mCallingPackage)); |
| sb.append("; callingUid: ").append(mCallingUid); |
| sb.append("; callingPid: ").append(mCallingPid); |
| sb.append("; appSwitchState: ").append(mAppSwitchState); |
| sb.append("; callingUidHasAnyVisibleWindow: ").append(mCallingUidHasAnyVisibleWindow); |
| sb.append("; callingUidProcState: ").append(DebugUtils.valueToString( |
| ActivityManager.class, "PROCESS_STATE_", mCallingUidProcState)); |
| sb.append("; isCallingUidPersistentSystemProcess: ") |
| .append(mIsCallingUidPersistentSystemProcess); |
| sb.append("; forcedBalByPiSender: ").append(mForcedBalByPiSender); |
| sb.append("; intent: ").append(mIntent); |
| sb.append("; callerApp: ").append(mCallerApp); |
| if (mCallerApp != null) { |
| sb.append("; inVisibleTask: ").append(mCallerApp.hasActivityInVisibleTask()); |
| } |
| sb.append("; balAllowedByPiCreator: ").append(mBalAllowedByPiCreator); |
| sb.append("; balAllowedByPiCreatorWithHardening: ") |
| .append(mBalAllowedByPiCreatorWithHardening); |
| sb.append("; resultIfPiCreatorAllowsBal: ").append(resultIfPiCreatorAllowsBal); |
| sb.append("; hasRealCaller: ").append(hasRealCaller()); |
| sb.append("; isCallForResult: ").append(mIsCallForResult); |
| sb.append("; isPendingIntent: ").append(isPendingIntent()); |
| if (hasRealCaller()) { |
| sb.append("; realCallingPackage: ") |
| .append(getDebugPackageName(mRealCallingPackage, mRealCallingUid)); |
| sb.append("; realCallingPackageTargetSdk: ") |
| .append(getTargetSdk(mRealCallingPackage)); |
| sb.append("; realCallingUid: ").append(mRealCallingUid); |
| sb.append("; realCallingPid: ").append(mRealCallingPid); |
| sb.append("; realCallingUidHasAnyVisibleWindow: ") |
| .append(mRealCallingUidHasAnyVisibleWindow); |
| sb.append("; realCallingUidProcState: ").append(DebugUtils.valueToString( |
| ActivityManager.class, "PROCESS_STATE_", mRealCallingUidProcState)); |
| sb.append("; isRealCallingUidPersistentSystemProcess: ") |
| .append(mIsRealCallingUidPersistentSystemProcess); |
| sb.append("; originatingPendingIntent: ").append(mOriginatingPendingIntent); |
| sb.append("; realCallerApp: ").append(mRealCallerApp); |
| if (mRealCallerApp != null) { |
| sb.append("; realInVisibleTask: ") |
| .append(mRealCallerApp.hasActivityInVisibleTask()); |
| } |
| sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender); |
| sb.append("; resultIfPiSenderAllowsBal: ").append(resultIfPiSenderAllowsBal); |
| } |
| sb.append("]"); |
| return sb.toString(); |
| } |
| } |
| |
| static class BalVerdict { |
| |
| static final BalVerdict BLOCK = new BalVerdict(BAL_BLOCK, false, "Blocked"); |
| static final BalVerdict ALLOW_BY_DEFAULT = |
| new BalVerdict(BAL_ALLOW_DEFAULT, false, "Default"); |
| private final @BalCode int mCode; |
| private final boolean mBackground; |
| private final String mMessage; |
| private String mProcessInfo; |
| // indicates BAL would be blocked because only creator of the PI has the privilege to allow |
| // BAL, the sender does not have the privilege to allow BAL. |
| private boolean mOnlyCreatorAllows; |
| /** indicates that this verdict is based on the real calling UID and not the calling UID */ |
| private boolean mBasedOnRealCaller; |
| |
| BalVerdict(@BalCode int balCode, boolean background, String message) { |
| this.mBackground = background; |
| this.mCode = balCode; |
| this.mMessage = message; |
| } |
| |
| public BalVerdict withProcessInfo(String msg, WindowProcessController process) { |
| mProcessInfo = msg + " (uid=" + process.mUid + ",pid=" + process.getPid() + ")"; |
| return this; |
| } |
| |
| boolean blocks() { |
| return mCode == BAL_BLOCK; |
| } |
| |
| boolean allows() { |
| return !blocks(); |
| } |
| |
| BalVerdict setOnlyCreatorAllows(boolean onlyCreatorAllows) { |
| mOnlyCreatorAllows = onlyCreatorAllows; |
| return this; |
| } |
| |
| boolean onlyCreatorAllows() { |
| return mOnlyCreatorAllows; |
| } |
| |
| private BalVerdict setBasedOnRealCaller() { |
| mBasedOnRealCaller = true; |
| return this; |
| } |
| |
| private boolean isBasedOnRealCaller() { |
| return mBasedOnRealCaller; |
| } |
| |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append(balCodeToString(mCode)); |
| if (DEBUG_ACTIVITY_STARTS) { |
| builder.append(" ("); |
| if (mBackground) { |
| builder.append("Background "); |
| } |
| builder.append("Activity start "); |
| if (mCode == BAL_BLOCK) { |
| builder.append("denied"); |
| } else { |
| builder.append("allowed: ").append(mMessage); |
| } |
| if (mProcessInfo != null) { |
| builder.append(" "); |
| builder.append(mProcessInfo); |
| } |
| builder.append(")"); |
| } |
| return builder.toString(); |
| } |
| |
| public @BalCode int getRawCode() { |
| return mCode; |
| } |
| |
| public @BalCode int getCode() { |
| if (mBasedOnRealCaller && mCode != BAL_BLOCK) { |
| // for compatibility always return BAL_ALLOW_PENDING_INTENT if based on real caller |
| return BAL_ALLOW_PENDING_INTENT; |
| } |
| return mCode; |
| } |
| } |
| |
| /** |
| * Check if a (background) activity start is allowed. |
| * |
| * @param callingUid The UID that wants to start the activity. |
| * @param callingPid The PID that wants to start the activity. |
| * @param callingPackage The package name that wants to start the activity. |
| * @param realCallingUid The UID that actually calls this method (only if this handles a |
| * PendingIntent, otherwise -1) |
| * @param realCallingPid The PID that actually calls this method (only if this handles a |
| * * PendingIntent, otherwise -1) |
| * @param callerApp The process that calls this method (only if not a PendingIntent) |
| * @param originatingPendingIntent PendingIntentRecord that originated this activity start or |
| * null if not originated by PendingIntent |
| * @param forcedBalByPiSender If set to allow, the |
| * PendingIntent's sender will try to force allow background activity starts. |
| * This is only possible if the sender of the PendingIntent is a system process. |
| * @param resultRecord If not null, this indicates that the caller expects a result. |
| * @param intent Intent that should be started. |
| * @param checkedOptions ActivityOptions to allow specific opt-ins/opt outs. |
| * |
| * @return A verdict denoting which BAL rule allows an activity to be started, |
| * or if the launch should be blocked. |
| */ |
| BalVerdict checkBackgroundActivityStart( |
| int callingUid, |
| int callingPid, |
| final String callingPackage, |
| int realCallingUid, |
| int realCallingPid, |
| WindowProcessController callerApp, |
| PendingIntentRecord originatingPendingIntent, |
| BackgroundStartPrivileges forcedBalByPiSender, |
| ActivityRecord resultRecord, |
| Intent intent, |
| ActivityOptions checkedOptions) { |
| |
| if (checkedOptions == null) { |
| // replace null with a constant to simplify evaluation |
| checkedOptions = ACTIVITY_OPTIONS_SYSTEM_DEFINED; |
| } |
| |
| BalState state = new BalState(callingUid, callingPid, callingPackage, |
| realCallingUid, realCallingPid, callerApp, originatingPendingIntent, |
| forcedBalByPiSender, resultRecord, intent, checkedOptions); |
| |
| // In the case of an SDK sandbox calling uid, check if the corresponding app uid has a |
| // visible window. |
| if (Process.isSdkSandboxUid(state.mRealCallingUid)) { |
| int realCallingSdkSandboxUidToAppUid = |
| Process.getAppUidForSdkSandboxUid(state.mRealCallingUid); |
| // realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition |
| // to realCallingUid when calculating resultForRealCaller below. |
| if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) { |
| BalVerdict balVerdict = new BalVerdict(BAL_ALLOW_SDK_SANDBOX, /*background*/ false, |
| "uid in SDK sandbox has visible (non-toast) window"); |
| return statsLog(balVerdict, state); |
| } |
| } |
| |
| BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state); |
| |
| if (!state.hasRealCaller()) { |
| BalVerdict resultForRealCaller = null; // nothing to compute |
| if (resultForCaller.allows()) { |
| if (DEBUG_ACTIVITY_STARTS) { |
| Slog.d(TAG, "Background activity start allowed. " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| } |
| return statsLog(resultForCaller, state); |
| } |
| return abortLaunch(state, resultForCaller, resultForRealCaller); |
| } |
| |
| // The realCaller result is only calculated for PendingIntents (indicated by a valid |
| // realCallingUid). If caller and realCaller are same UID and we are already allowed based |
| // on the caller (i.e. creator of the PendingIntent) there is no need to calculate this |
| // again, but if the result is block it is possible that there are additional exceptions |
| // that allow based on the realCaller (i.e. sender of the PendingIntent), e.g. if the |
| // realCallerApp process is allowed to start (in the creator path the callerApp for |
| // PendingIntents is null). |
| BalVerdict resultForRealCaller = state.callerIsRealCaller() && resultForCaller.allows() |
| ? resultForCaller |
| : checkBackgroundActivityStartAllowedBySender(state, checkedOptions) |
| .setBasedOnRealCaller(); |
| if (state.isPendingIntent()) { |
| resultForCaller.setOnlyCreatorAllows( |
| resultForCaller.allows() && resultForRealCaller.blocks()); |
| } |
| |
| // Handle cases with explicit opt-in |
| if (resultForCaller.allows() |
| && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) { |
| if (DEBUG_ACTIVITY_STARTS) { |
| Slog.d(TAG, "Activity start explicitly allowed by caller. " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| } |
| return statsLog(resultForCaller, state); |
| } |
| if (resultForRealCaller.allows() |
| && checkedOptions.getPendingIntentBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) { |
| if (DEBUG_ACTIVITY_STARTS) { |
| Slog.d(TAG, "Activity start explicitly allowed by real caller. " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| } |
| return statsLog(resultForRealCaller, state); |
| } |
| // Handle PendingIntent cases with default behavior next |
| boolean callerCanAllow = resultForCaller.allows() |
| && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; |
| boolean realCallerCanAllow = resultForRealCaller.allows() |
| && checkedOptions.getPendingIntentBackgroundActivityStartMode() |
| == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; |
| if (callerCanAllow && realCallerCanAllow) { |
| // Both caller and real caller allow with system defined behavior |
| if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { |
| // Will be allowed even with BAL hardening. |
| if (DEBUG_ACTIVITY_STARTS) { |
| Slog.d(TAG, "Activity start allowed by caller. " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| } |
| // return the realCaller result for backwards compatibility |
| return statsLog(resultForRealCaller, state); |
| } |
| if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { |
| Slog.wtf(TAG, |
| "With Android 15 BAL hardening this activity start may be blocked" |
| + " if the PI creator upgrades target_sdk to 35+" |
| + " AND the PI sender upgrades target_sdk to 34+! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| showBalRiskToast("BAL would be blocked", state); |
| // return the realCaller result for backwards compatibility |
| return statsLog(resultForRealCaller, state); |
| } |
| Slog.wtf(TAG, |
| "Without Android 15 BAL hardening this activity start would be allowed" |
| + " (missing opt in by PI creator or sender)! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| return abortLaunch(state, resultForCaller, resultForRealCaller); |
| } |
| if (callerCanAllow) { |
| // Allowed before V by creator |
| if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { |
| // Will be allowed even with BAL hardening. |
| if (DEBUG_ACTIVITY_STARTS) { |
| Slog.d(TAG, "Activity start allowed by caller. " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| } |
| return statsLog(resultForCaller, state); |
| } |
| if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { |
| Slog.wtf(TAG, |
| "With Android 15 BAL hardening this activity start may be blocked" |
| + " if the PI creator upgrades target_sdk to 35+! " |
| + " (missing opt in by PI creator)! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| showBalRiskToast("BAL would be blocked", state); |
| return statsLog(resultForCaller, state); |
| } |
| Slog.wtf(TAG, |
| "Without Android 15 BAL hardening this activity start would be allowed" |
| + " (missing opt in by PI creator)! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| return abortLaunch(state, resultForCaller, resultForRealCaller); |
| } |
| if (realCallerCanAllow) { |
| // Allowed before U by sender |
| if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) { |
| Slog.wtf(TAG, |
| "With Android 14 BAL hardening this activity start will be blocked" |
| + " if the PI sender upgrades target_sdk to 34+! " |
| + " (missing opt in by PI sender)! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| showBalRiskToast("BAL would be blocked", state); |
| return statsLog(resultForRealCaller, state); |
| } |
| Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed" |
| + " (missing opt in by PI sender)! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| return abortLaunch(state, resultForCaller, resultForRealCaller); |
| } |
| // neither the caller not the realCaller can allow or have explicitly opted out |
| return abortLaunch(state, resultForCaller, resultForRealCaller); |
| } |
| |
| private BalVerdict abortLaunch(BalState state, BalVerdict resultForCaller, |
| BalVerdict resultForRealCaller) { |
| Slog.w(TAG, "Background activity launch blocked! " |
| + state.dump(resultForCaller, resultForRealCaller)); |
| showBalBlockedToast("BAL blocked", state); |
| return statsLog(BalVerdict.BLOCK, state); |
| } |
| |
| /** |
| * @return A code denoting which BAL rule allows an activity to be started, |
| * or {@link #BAL_BLOCK} if the launch should be blocked |
| */ |
| BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) { |
| int callingUid = state.mCallingUid; |
| int callingPid = state.mCallingPid; |
| final String callingPackage = state.mCallingPackage; |
| WindowProcessController callerApp = state.mCallerApp; |
| |
| // don't abort for the most important UIDs |
| final int callingAppId = UserHandle.getAppId(callingUid); |
| if (callingUid == Process.ROOT_UID |
| || callingAppId == Process.SYSTEM_UID |
| || callingAppId == Process.NFC_UID) { |
| return new BalVerdict( |
| BAL_ALLOW_ALLOWLISTED_UID, /*background*/ false, |
| "Important callingUid"); |
| } |
| |
| // Always allow home application to start activities. |
| if (isHomeApp(callingUid, callingPackage)) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ false, |
| "Home app"); |
| } |
| |
| // IME should always be allowed to start activity, like IME settings. |
| final WindowState imeWindow = |
| mService.mRootWindowContainer.getCurrentInputMethodWindow(); |
| if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ false, |
| "Active ime"); |
| } |
| |
| // This is used to block background activity launch even if the app is still |
| // visible to user after user clicking home button. |
| final int appSwitchState = mService.getBalAppSwitchesState(); |
| |
| // don't abort if the callingUid has a visible window or is a persistent system process |
| final int callingUidProcState = mService.mActiveUids.getUidState(callingUid); |
| final boolean callingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid); |
| final boolean isCallingUidPersistentSystemProcess = |
| callingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; |
| |
| // Normal apps with visible app window will be allowed to start activity if app switching |
| // is allowed, or apps like live wallpaper with non app visible window will be allowed. |
| final boolean appSwitchAllowedOrFg = |
| appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY; |
| if (appSwitchAllowedOrFg && callingUidHasAnyVisibleWindow) { |
| return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, |
| /*background*/ false, "callingUid has visible window"); |
| } |
| if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) { |
| return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, |
| /*background*/ false, "callingUid has non-app visible window"); |
| } |
| |
| if (isCallingUidPersistentSystemProcess) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ false, "callingUid is persistent system process"); |
| } |
| |
| // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission |
| if (ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, |
| callingPid, callingUid) == PERMISSION_GRANTED) { |
| return new BalVerdict(BAL_ALLOW_PERMISSION, |
| /*background*/ true, |
| "START_ACTIVITIES_FROM_BACKGROUND permission granted"); |
| } |
| // don't abort if the caller has the same uid as the recents component |
| if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ true, "Recents Component"); |
| } |
| // don't abort if the callingUid is the device owner |
| if (mService.isDeviceOwner(callingUid)) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ true, "Device Owner"); |
| } |
| // don't abort if the callingUid is a affiliated profile owner |
| if (mService.isAffiliatedProfileOwner(callingUid)) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ true, "Affiliated Profile Owner"); |
| } |
| // don't abort if the callingUid has companion device |
| final int callingUserId = UserHandle.getUserId(callingUid); |
| if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ true, "Companion App"); |
| } |
| // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission |
| if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) { |
| Slog.w( |
| TAG, |
| "Background activity start for " |
| + callingPackage |
| + " allowed because SYSTEM_ALERT_WINDOW permission is granted."); |
| return new BalVerdict(BAL_ALLOW_SAW_PERMISSION, |
| /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted"); |
| } |
| // don't abort if the callingUid and callingPackage have the |
| // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop |
| if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow( |
| AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION, |
| callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) { |
| return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true, |
| "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted"); |
| } |
| |
| // If we don't have callerApp at this point, no caller was provided to startActivity(). |
| // That's the case for PendingIntent-based starts, since the creator's process might not be |
| // up and alive. |
| // Don't abort if the callerApp or other processes of that uid are allowed in any way. |
| BalVerdict callerAppAllowsBal = checkProcessAllowsBal(callerApp, state); |
| if (callerAppAllowsBal.allows()) { |
| return callerAppAllowsBal; |
| } |
| |
| // If we are here, it means all exemptions based on the creator failed |
| return BalVerdict.BLOCK; |
| } |
| |
| /** |
| * @return A code denoting which BAL rule allows an activity to be started, |
| * or {@link #BAL_BLOCK} if the launch should be blocked |
| */ |
| BalVerdict checkBackgroundActivityStartAllowedBySender( |
| BalState state, |
| ActivityOptions checkedOptions) { |
| |
| if (PendingIntentRecord.isPendingIntentBalAllowedByPermission(checkedOptions) |
| && ActivityManager.checkComponentPermission( |
| android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, |
| state.mRealCallingUid, NO_PROCESS_UID, true) == PackageManager.PERMISSION_GRANTED) { |
| return new BalVerdict(BAL_ALLOW_PERMISSION, |
| /*background*/ false, |
| "realCallingUid has BAL permission."); |
| } |
| |
| // Normal apps with visible app window will be allowed to start activity if app switching |
| // is allowed, or apps like live wallpaper with non app visible window will be allowed. |
| final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW |
| || state.mAppSwitchState == APP_SWITCH_FG_ONLY; |
| if (Flags.balImproveRealCallerVisibilityCheck()) { |
| if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) { |
| return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, |
| /*background*/ false, "realCallingUid has visible window"); |
| } |
| if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) { |
| return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, |
| /*background*/ false, "realCallingUid has non-app visible window"); |
| } |
| } else { |
| // don't abort if the realCallingUid has a visible window |
| // TODO(b/171459802): We should check appSwitchAllowed also |
| if (state.mRealCallingUidHasAnyVisibleWindow) { |
| return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, |
| /*background*/ false, |
| "realCallingUid has visible (non-toast) window."); |
| } |
| } |
| |
| // if the realCallingUid is a persistent system process, abort if the IntentSender |
| // wasn't allowed to start an activity |
| if (state.mForcedBalByPiSender.allowsBackgroundActivityStarts() |
| && state.mIsRealCallingUidPersistentSystemProcess) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_UID, |
| /*background*/ false, |
| "realCallingUid is persistent system process AND intent " |
| + "sender forced to allow."); |
| } |
| // don't abort if the realCallingUid is an associated companion app |
| if (mService.isAssociatedCompanionApp( |
| UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) { |
| return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, |
| /*background*/ false, |
| "realCallingUid is a companion app."); |
| } |
| |
| // don't abort if the callerApp or other processes of that uid are allowed in any way |
| BalVerdict realCallerAppAllowsBal = |
| checkProcessAllowsBal(state.mRealCallerApp, state); |
| if (realCallerAppAllowsBal.allows()) { |
| return realCallerAppAllowsBal; |
| } |
| |
| // If we are here, it means all exemptions based on PI sender failed |
| return BalVerdict.BLOCK; |
| } |
| |
| /** |
| * Check if the app allows BAL. |
| * |
| * See {@link BackgroundLaunchProcessController#areBackgroundActivityStartsAllowed(int, int, |
| * String, int, boolean, boolean, boolean, long, long, long)} for details on the |
| * exceptions. |
| */ |
| private BalVerdict checkProcessAllowsBal(WindowProcessController app, |
| BalState state) { |
| if (app == null) { |
| return BalVerdict.BLOCK; |
| } |
| // first check the original calling process |
| final BalVerdict balAllowedForCaller = app |
| .areBackgroundActivityStartsAllowed(state.mAppSwitchState); |
| if (balAllowedForCaller.allows()) { |
| return balAllowedForCaller.withProcessInfo("callerApp process", app); |
| } else { |
| // only if that one wasn't allowed, check the other ones |
| final ArraySet<WindowProcessController> uidProcesses = |
| mService.mProcessMap.getProcesses(app.mUid); |
| if (uidProcesses != null) { |
| for (int i = uidProcesses.size() - 1; i >= 0; i--) { |
| final WindowProcessController proc = uidProcesses.valueAt(i); |
| if (proc != app) { |
| BalVerdict balAllowedForUid = proc.areBackgroundActivityStartsAllowed( |
| state.mAppSwitchState); |
| if (balAllowedForUid.allows()) { |
| return balAllowedForCaller.withProcessInfo("process", proc); |
| } |
| } |
| } |
| } |
| } |
| return BalVerdict.BLOCK; |
| } |
| |
| /** |
| * Log activity starts which violate one of the following rules of the |
| * activity security model (ASM): |
| * See go/activity-security for rationale behind the rules. |
| * 1. Within a task, only an activity matching a top UID of the task can start activities |
| * 2. Only activities within a foreground task, which match a top UID of the task, can |
| * create a new task or bring an existing one into the foreground |
| */ |
| boolean checkActivityAllowedToStart(@Nullable ActivityRecord sourceRecord, |
| @NonNull ActivityRecord targetRecord, boolean newTask, @NonNull Task targetTask, |
| int launchFlags, int balCode, int callingUid, int realCallingUid) { |
| // BAL Exception allowed in all cases |
| if (balCode == BAL_ALLOW_ALLOWLISTED_UID) { |
| return true; |
| } |
| |
| // Intents with FLAG_ACTIVITY_NEW_TASK will always be considered as creating a new task |
| // even if the intent is delivered to an existing task. |
| boolean taskToFront = newTask |
| || (launchFlags & FLAG_ACTIVITY_NEW_TASK) == FLAG_ACTIVITY_NEW_TASK; |
| |
| // BAL exception only allowed for new tasks |
| if (taskToFront) { |
| if (balCode == BAL_ALLOW_ALLOWLISTED_COMPONENT |
| || balCode == BAL_ALLOW_PERMISSION |
| || balCode == BAL_ALLOW_PENDING_INTENT |
| || balCode == BAL_ALLOW_SAW_PERMISSION |
| || balCode == BAL_ALLOW_VISIBLE_WINDOW) { |
| return true; |
| } |
| } |
| |
| if (balCode == BAL_ALLOW_GRACE_PERIOD) { |
| if (taskToFront && mTopFinishedActivity != null |
| && mTopFinishedActivity.mUid == callingUid) { |
| return true; |
| } else if (!taskToFront) { |
| FinishedActivityEntry finishedEntry = |
| mTaskIdToFinishedActivity.get(targetTask.mTaskId); |
| if (finishedEntry != null && finishedEntry.mUid == callingUid) { |
| return true; |
| } |
| } |
| } |
| |
| BlockActivityStart bas = null; |
| if (sourceRecord != null) { |
| boolean passesAsmChecks = true; |
| Task sourceTask = sourceRecord.getTask(); |
| |
| // Allow launching into a new task (or a task matching the launched activity's |
| // affinity) only if the current task is foreground or mutating its own task. |
| // The latter can happen eg. if caller uses NEW_TASK flag and the activity being |
| // launched matches affinity of source task. |
| if (taskToFront) { |
| passesAsmChecks = sourceTask != null |
| && (sourceTask.isVisible() || sourceTask == targetTask); |
| } |
| |
| if (passesAsmChecks) { |
| Task taskToCheck = taskToFront ? sourceTask : targetTask; |
| bas = isTopActivityMatchingUidAbsentForAsm(taskToCheck, sourceRecord.getUid(), |
| sourceRecord); |
| } |
| } else if (!taskToFront) { |
| // We don't have a sourceRecord, and we're launching into an existing task. |
| // Allow if callingUid is top of stack. |
| bas = isTopActivityMatchingUidAbsentForAsm(targetTask, callingUid, |
| /*sourceRecord*/null); |
| } |
| |
| if (bas != null && !bas.mWouldBlockActivityStartIgnoringFlag) { |
| return true; |
| } |
| |
| // ASM rules have failed. Log why |
| return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid, |
| newTask, targetTask, targetRecord, balCode, launchFlags, bas, taskToFront); |
| } |
| |
| private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid, |
| int realCallingUid, boolean newTask, Task targetTask, ActivityRecord targetRecord, |
| @BalCode int balCode, int launchFlags, BlockActivityStart bas, boolean taskToFront) { |
| |
| ActivityRecord targetTopActivity = targetTask == null ? null |
| : targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop()); |
| |
| int action = newTask || sourceRecord == null |
| ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_NEW_TASK |
| : (sourceRecord.getTask().equals(targetTask) |
| ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK |
| : FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK); |
| |
| boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags |
| .shouldRestrictActivitySwitch(callingUid) |
| && (bas == null || bas.mBlockActivityStartIfFlagEnabled); |
| |
| String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord, |
| targetRecord, targetTask, targetTopActivity, realCallingUid, balCode, |
| blockActivityStartAndFeatureEnabled, taskToFront); |
| |
| FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, |
| /* caller_uid */ |
| sourceRecord != null ? sourceRecord.getUid() : callingUid, |
| /* caller_activity_class_name */ |
| sourceRecord != null ? sourceRecord.info.name : null, |
| /* target_task_top_activity_uid */ |
| targetTopActivity != null ? targetTopActivity.getUid() : NO_PROCESS_UID, |
| /* target_task_top_activity_class_name */ |
| targetTopActivity != null ? targetTopActivity.info.name : null, |
| /* target_task_is_different */ |
| newTask || sourceRecord == null || targetTask == null |
| || !targetTask.equals(sourceRecord.getTask()), |
| /* target_activity_uid */ |
| targetRecord.getUid(), |
| /* target_activity_class_name */ |
| targetRecord.info.name, |
| /* target_intent_action */ |
| targetRecord.intent.getAction(), |
| /* target_intent_flags */ |
| launchFlags, |
| /* action */ |
| action, |
| /* version */ |
| ActivitySecurityModelFeatureFlags.ASM_VERSION, |
| /* multi_window - we have our source not in the target task, but both are visible */ |
| targetTask != null && sourceRecord != null |
| && !targetTask.equals(sourceRecord.getTask()) && targetTask.isVisible(), |
| /* bal_code */ |
| balCode, |
| /* debug_info */ |
| asmDebugInfo |
| ); |
| |
| String launchedFromPackageName = targetRecord.launchedFromPackage; |
| if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) { |
| String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK |
| + (blockActivityStartAndFeatureEnabled ? " blocked " : " would block ") |
| + getApplicationLabel(mService.mContext.getPackageManager(), |
| launchedFromPackageName); |
| showToast(toastText); |
| |
| Slog.i(TAG, asmDebugInfo); |
| } |
| |
| if (blockActivityStartAndFeatureEnabled) { |
| Slog.e(TAG, "[ASM] Abort Launching r: " + targetRecord |
| + " as source: " |
| + (sourceRecord != null ? sourceRecord : launchedFromPackageName) |
| + " is in background. New task: " + newTask |
| + ". Top activity: " + targetTopActivity |
| + ". BAL Code: " + balCodeToString(balCode)); |
| |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private void showBalBlockedToast(String toastText, BalState state) { |
| if (balShowToastsBlocked()) { |
| showToast(toastText |
| + " caller:" + state.mCallingPackage |
| + " realCaller:" + state.mRealCallingPackage); |
| } |
| } |
| |
| private void showBalRiskToast(String toastText, BalState state) { |
| if (balShowToasts()) { |
| showToast(toastText |
| + " caller:" + state.mCallingPackage |
| + " realCaller:" + state.mRealCallingPackage); |
| } |
| } |
| |
| private void showToast(String toastText) { |
| UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, |
| toastText, Toast.LENGTH_LONG).show()); |
| } |
| |
| /** |
| * If the top activity uid does not match the launching or launched activity, and the launch was |
| * not requested from the top uid, we want to clear out all non matching activities to prevent |
| * the top activity being sandwiched. |
| * Both creator and sender UID are considered for the launching activity. |
| */ |
| void clearTopIfNeeded(@NonNull Task targetTask, @Nullable ActivityRecord sourceRecord, |
| @NonNull ActivityRecord targetRecord, int callingUid, int realCallingUid, |
| int launchFlags, @BalCode int balCode) { |
| if ((launchFlags & FLAG_ACTIVITY_NEW_TASK) != FLAG_ACTIVITY_NEW_TASK |
| || balCode == BAL_ALLOW_ALLOWLISTED_UID) { |
| // Launch is from the same task, (a top or privileged UID), or is directly privileged. |
| return; |
| } |
| |
| int startingUid = targetRecord.getUid(); |
| Predicate<ActivityRecord> isLaunchingOrLaunched = ar -> |
| ar.isUid(startingUid) || ar.isUid(callingUid) || ar.isUid(realCallingUid); |
| |
| // Return early if we know for sure we won't need to clear any activities by just checking |
| // the top activity. |
| ActivityRecord targetTaskTop = targetTask.getTopMostActivity(); |
| if (targetTaskTop == null || isLaunchingOrLaunched.test(targetTaskTop)) { |
| return; |
| } |
| |
| // Find the first activity which matches a safe UID and is not finishing. Clear everything |
| // above it |
| boolean shouldBlockActivityStart = ActivitySecurityModelFeatureFlags |
| .shouldRestrictActivitySwitch(callingUid); |
| int[] finishCount = new int[0]; |
| if (shouldBlockActivityStart) { |
| ActivityRecord activity = targetTask.getActivity(isLaunchingOrLaunched); |
| if (activity == null) { |
| // mStartActivity is not in task, so clear everything |
| activity = targetRecord; |
| } |
| |
| finishCount = new int[1]; |
| targetTask.performClearTop(activity, launchFlags, finishCount); |
| if (finishCount[0] > 0) { |
| Slog.w(TAG, "Cleared top n: " + finishCount[0] + " activities from task t: " |
| + targetTask + " not matching top uid: " + callingUid); |
| } |
| } |
| |
| if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid) |
| && (!shouldBlockActivityStart || finishCount[0] > 0)) { |
| showToast((shouldBlockActivityStart |
| ? "Top activities cleared by " |
| : "Top activities would be cleared by ") |
| + ActivitySecurityModelFeatureFlags.DOC_LINK); |
| |
| Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord, |
| targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart, |
| /* taskToFront */ true)); |
| } |
| } |
| |
| /** |
| * Returns home if the passed in callingUid is not top of the stack, rather than returning to |
| * previous task. |
| */ |
| void checkActivityAllowedToClearTask(@NonNull Task task, int callingUid, |
| @NonNull String callerActivityClassName) { |
| // We may have already checked that the callingUid has additional clearTask privileges, and |
| // cleared the calling identify. If so, we infer we do not need further restrictions here. |
| if (callingUid == SYSTEM_UID || !task.isVisible() || task.inMultiWindowMode()) { |
| return; |
| } |
| |
| TaskDisplayArea displayArea = task.getTaskDisplayArea(); |
| if (displayArea == null) { |
| // If there is no associated display area, we can not return home. |
| return; |
| } |
| |
| BlockActivityStart bas = isTopActivityMatchingUidAbsentForAsm(task, callingUid, null); |
| if (!bas.mWouldBlockActivityStartIgnoringFlag) { |
| return; |
| } |
| |
| ActivityRecord topActivity = task.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop()); |
| FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, |
| /* caller_uid */ |
| callingUid, |
| /* caller_activity_class_name */ |
| callerActivityClassName, |
| /* target_task_top_activity_uid */ |
| topActivity == null ? NO_PROCESS_UID : topActivity.getUid(), |
| /* target_task_top_activity_class_name */ |
| topActivity == null ? null : topActivity.info.name, |
| /* target_task_is_different */ |
| false, |
| /* target_activity_uid */ |
| NO_PROCESS_UID, |
| /* target_activity_class_name */ |
| null, |
| /* target_intent_action */ |
| null, |
| /* target_intent_flags */ |
| 0, |
| /* action */ |
| FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK, |
| /* version */ |
| ActivitySecurityModelFeatureFlags.ASM_VERSION, |
| /* multi_window */ |
| false, |
| /* bal_code */ |
| -1, |
| /* debug_info */ |
| null |
| ); |
| |
| boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags |
| .shouldRestrictActivitySwitch(callingUid) |
| && bas.mBlockActivityStartIfFlagEnabled; |
| |
| PackageManager pm = mService.mContext.getPackageManager(); |
| String callingPackage = pm.getNameForUid(callingUid); |
| final CharSequence callingLabel; |
| if (callingPackage == null) { |
| callingPackage = String.valueOf(callingUid); |
| callingLabel = callingPackage; |
| } else { |
| callingLabel = getApplicationLabel(pm, callingPackage); |
| } |
| |
| if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) { |
| showToast((ActivitySecurityModelFeatureFlags.DOC_LINK |
| + (restrictActivitySwitch ? " returned home due to " |
| : " would return home due to ") |
| + callingLabel)); |
| } |
| |
| // If the activity switch should be restricted, return home rather than the |
| // previously top task, to prevent users from being confused which app they're |
| // viewing |
| if (restrictActivitySwitch) { |
| Slog.w(TAG, "[ASM] Return to home as source: " + callingPackage |
| + " is not on top of task t: " + task); |
| displayArea.moveHomeActivityToTop("taskRemoved"); |
| } else { |
| Slog.i(TAG, "[ASM] Would return to home as source: " + callingPackage |
| + " is not on top of task t: " + task); |
| } |
| } |
| |
| /** |
| * For the purpose of ASM, ‘Top UID” for a task is defined as an activity UID |
| * 1. Which is top of the stack in z-order |
| * a. Excluding any activities with the flag ‘isAlwaysOnTop’ and |
| * b. Excluding any activities which are `finishing` |
| * 2. Or top of an adjacent task fragment to (1) |
| * <p> |
| * The 'sourceRecord' can be considered top even if it is 'finishing' |
| * |
| * Returns a class where the elements are: |
| * <pre> |
| * shouldBlockActivityStart: {@code true} if we should actually block the transition (takes into |
| * consideration feature flag and targetSdk). |
| * wouldBlockActivityStartIgnoringFlags: {@code true} if we should warn about the transition via |
| * toasts. This happens if the transition would be blocked in case both the app was targeting V+ |
| * and the feature was enabled. |
| * </pre> |
| */ |
| private BlockActivityStart isTopActivityMatchingUidAbsentForAsm(@NonNull Task task, |
| int uid, @Nullable ActivityRecord sourceRecord) { |
| // If the source is visible, consider it 'top'. |
| if (sourceRecord != null && sourceRecord.isVisible()) { |
| return new BlockActivityStart(false, false); |
| } |
| |
| // Always allow actual top activity to clear task |
| ActivityRecord topActivity = task.getTopMostActivity(); |
| if (topActivity != null && topActivity.isUid(uid)) { |
| return new BlockActivityStart(false, false); |
| } |
| |
| // Consider the source activity, whether or not it is finishing. Do not consider any other |
| // finishing activity. |
| Predicate<ActivityRecord> topOfStackPredicate = (ar) -> ar.equals(sourceRecord) |
| || (!ar.finishing && !ar.isAlwaysOnTop()); |
| |
| // Check top of stack (or the first task fragment for embedding). |
| topActivity = task.getActivity(topOfStackPredicate); |
| if (topActivity == null) { |
| return new BlockActivityStart(true, true); |
| } |
| |
| BlockActivityStart pair = blockCrossUidActivitySwitchFromBelow(topActivity, uid); |
| if (!pair.mBlockActivityStartIfFlagEnabled) { |
| return pair; |
| } |
| |
| // Even if the top activity is not a match, we may be in an embedded activity scenario with |
| // an adjacent task fragment. Get the second fragment. |
| TaskFragment taskFragment = topActivity.getTaskFragment(); |
| if (taskFragment == null) { |
| return pair; |
| } |
| |
| TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); |
| if (adjacentTaskFragment == null) { |
| return pair; |
| } |
| |
| // Check the second fragment. |
| topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate); |
| if (topActivity == null) { |
| return new BlockActivityStart(true, true); |
| } |
| |
| return blockCrossUidActivitySwitchFromBelow(topActivity, uid); |
| } |
| |
| /** |
| * Determines if a source is allowed to add or remove activities from the task, |
| * if the current ActivityRecord is above it in the stack |
| * |
| * A transition is blocked ({@code false} returned) if all of the following are met: |
| * <pre> |
| * 1. The source activity and the current activity record belong to different apps |
| * (i.e, have different UIDs). |
| * 2. Both the source activity and the current activity target U+ |
| * 3. The current activity has not set |
| * {@link ActivityRecord#setAllowCrossUidActivitySwitchFromBelow(boolean)} to {@code true} |
| * </pre> |
| * |
| * Returns a class where the elements are: |
| * <pre> |
| * shouldBlockActivityStart: {@code true} if we should actually block the transition (takes into |
| * consideration feature flag and targetSdk). |
| * wouldBlockActivityStartIgnoringFlags: {@code true} if we should warn about the transition via |
| * toasts. This happens if the transition would be blocked in case both the app was targeting V+ |
| * and the feature was enabled. |
| * </pre> |
| * |
| * @param sourceUid The source (s) activity performing the state change |
| */ |
| private BlockActivityStart blockCrossUidActivitySwitchFromBelow(ActivityRecord ar, |
| int sourceUid) { |
| if (ar.isUid(sourceUid)) { |
| return new BlockActivityStart(false, false); |
| } |
| |
| // If mAllowCrossUidActivitySwitchFromBelow is set, honor it. |
| if (ar.mAllowCrossUidActivitySwitchFromBelow) { |
| return new BlockActivityStart(false, false); |
| } |
| |
| // At this point, we would block if the feature is launched and both apps were V+ |
| // Since we have a feature flag, we need to check that too |
| // TODO(b/258792202) Replace with CompatChanges and replace Pair with boolean once feature |
| // flag is removed |
| boolean restrictActivitySwitch = |
| ActivitySecurityModelFeatureFlags.shouldRestrictActivitySwitch(ar.getUid()) |
| && ActivitySecurityModelFeatureFlags |
| .shouldRestrictActivitySwitch(sourceUid); |
| return new BackgroundActivityStartController |
| .BlockActivityStart(restrictActivitySwitch, true); |
| } |
| |
| /** |
| * Only called when an activity launch may be blocked, which should happen very rarely |
| */ |
| private String getDebugInfoForActivitySecurity(@NonNull String action, |
| @Nullable ActivityRecord sourceRecord, @NonNull ActivityRecord targetRecord, |
| @Nullable Task targetTask, @Nullable ActivityRecord targetTopActivity, |
| int realCallingUid, @BalCode int balCode, |
| boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) { |
| final String prefix = "[ASM] "; |
| Function<ActivityRecord, String> recordToString = (ar) -> { |
| if (ar == null) { |
| return null; |
| } |
| return (ar == sourceRecord ? " [source]=> " |
| : ar == targetTopActivity ? " [ top ]=> " |
| : ar == targetRecord ? " [target]=> " |
| : " => ") |
| + ar |
| + " :: visible=" + ar.isVisible() |
| + ", finishing=" + ar.isFinishing() |
| + ", alwaysOnTop=" + ar.isAlwaysOnTop() |
| + ", taskFragment=" + ar.getTaskFragment(); |
| }; |
| |
| StringJoiner joiner = new StringJoiner("\n"); |
| joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------"); |
| joiner.add(prefix + "Block Enabled: " + blockActivityStartAndFeatureEnabled); |
| joiner.add(prefix + "ASM Version: " + ActivitySecurityModelFeatureFlags.ASM_VERSION); |
| |
| boolean targetTaskMatchesSourceTask = targetTask != null |
| && sourceRecord != null && sourceRecord.getTask() == targetTask; |
| |
| if (sourceRecord == null) { |
| joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage); |
| String realCallingPackage = mService.mContext.getPackageManager().getNameForUid( |
| realCallingUid); |
| joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage); |
| } else { |
| joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord)); |
| if (targetTaskMatchesSourceTask) { |
| joiner.add(prefix + "Source/Target Task: " + sourceRecord.getTask()); |
| joiner.add(prefix + "Source/Target Task Stack: "); |
| } else { |
| joiner.add(prefix + "Source Task: " + sourceRecord.getTask()); |
| joiner.add(prefix + "Source Task Stack: "); |
| } |
| sourceRecord.getTask().forAllActivities((Consumer<ActivityRecord>) |
| ar -> joiner.add(prefix + recordToString.apply(ar))); |
| } |
| |
| joiner.add(prefix + "Target Task Top: " + recordToString.apply(targetTopActivity)); |
| if (!targetTaskMatchesSourceTask) { |
| joiner.add(prefix + "Target Task: " + targetTask); |
| if (targetTask != null) { |
| joiner.add(prefix + "Target Task Stack: "); |
| targetTask.forAllActivities((Consumer<ActivityRecord>) |
| ar -> joiner.add(prefix + recordToString.apply(ar))); |
| } |
| } |
| |
| joiner.add(prefix + "Target Record: " + recordToString.apply(targetRecord)); |
| joiner.add(prefix + "Intent: " + targetRecord.intent); |
| joiner.add(prefix + "TaskToFront: " + taskToFront); |
| joiner.add(prefix + "BalCode: " + balCodeToString(balCode)); |
| |
| joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------"); |
| return joiner.toString(); |
| } |
| |
| private static boolean isSystemExemptFlagEnabled() { |
| return DeviceConfig.getBoolean( |
| NAMESPACE_WINDOW_MANAGER, |
| /* name= */ "system_exempt_from_activity_bg_start_restriction_enabled", |
| /* defaultValue= */ true); |
| } |
| |
| private BalVerdict statsLog(BalVerdict finalVerdict, BalState state) { |
| if (finalVerdict.blocks() && mService.isActivityStartsLoggingEnabled()) { |
| // log aborted activity start to TRON |
| mSupervisor |
| .getActivityMetricsLogger() |
| .logAbortedBgActivityStart( |
| state.mIntent, |
| state.mCallerApp, |
| state.mCallingUid, |
| state.mCallingPackage, |
| state.mCallingUidProcState, |
| state.mCallingUidHasAnyVisibleWindow, |
| state.mRealCallingUid, |
| state.mRealCallingUidProcState, |
| state.mRealCallingUidHasAnyVisibleWindow, |
| (state.mOriginatingPendingIntent != null)); |
| } |
| |
| @BalCode int code = finalVerdict.getCode(); |
| int callingUid = state.mCallingUid; |
| int realCallingUid = state.mRealCallingUid; |
| Intent intent = state.mIntent; |
| |
| if (code == BAL_ALLOW_PENDING_INTENT |
| && (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) { |
| String activityName = |
| intent != null ? intent.getComponent().flattenToShortString() : ""; |
| FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, |
| activityName, |
| BAL_ALLOW_PENDING_INTENT, |
| callingUid, |
| realCallingUid); |
| } |
| if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND |
| || code == BAL_ALLOW_SAW_PERMISSION) { |
| // We don't need to know which activity in this case. |
| FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, |
| /*activityName*/ "", |
| code, |
| callingUid, |
| realCallingUid); |
| } |
| return finalVerdict; |
| } |
| |
| /** |
| * Called whenever an activity finishes. Stores the record, so it can be used by ASM grace |
| * period checks. |
| */ |
| void onActivityRequestedFinishing(@NonNull ActivityRecord finishActivity) { |
| // We only update the entry if the passed in activity |
| // 1. Has been chained less than a set max AND |
| // 2. Is visible or top |
| FinishedActivityEntry entry = |
| mTaskIdToFinishedActivity.get(finishActivity.getTask().mTaskId); |
| if (entry != null && finishActivity.isUid(entry.mUid) |
| && entry.mLaunchCount > ASM_GRACEPERIOD_MAX_REPEATS) { |
| return; |
| } |
| |
| if (!finishActivity.mVisibleRequested |
| && finishActivity != finishActivity.getTask().getTopMostActivity()) { |
| return; |
| } |
| |
| FinishedActivityEntry newEntry = new FinishedActivityEntry(finishActivity); |
| mTaskIdToFinishedActivity.put(finishActivity.getTask().mTaskId, newEntry); |
| if (finishActivity.getTask().mVisibleRequested) { |
| mTopFinishedActivity = newEntry; |
| } |
| } |
| |
| /** |
| * Called whenever an activity starts. Updates the record so the activity is no longer |
| * considered for ASM grace period checks |
| */ |
| void onNewActivityLaunched(ActivityRecord activityStarted) { |
| if (activityStarted.getTask() == null) { |
| return; |
| } |
| |
| if (activityStarted.getTask().mVisibleRequested) { |
| mTopFinishedActivity = null; |
| } |
| |
| FinishedActivityEntry entry = |
| mTaskIdToFinishedActivity.get(activityStarted.getTask().mTaskId); |
| if (entry != null && activityStarted.getTask().isTaskId(entry.mTaskId)) { |
| mTaskIdToFinishedActivity.remove(entry.mTaskId); |
| } |
| } |
| |
| static class BlockActivityStart { |
| // We should block if feature flag is enabled |
| private final boolean mBlockActivityStartIfFlagEnabled; |
| // Used for logging/toasts. Would we block if target sdk was V and feature was |
| // enabled? |
| private final boolean mWouldBlockActivityStartIgnoringFlag; |
| |
| BlockActivityStart(boolean shouldBlockActivityStart, |
| boolean wouldBlockActivityStartIgnoringFlags) { |
| this.mBlockActivityStartIfFlagEnabled = shouldBlockActivityStart; |
| this.mWouldBlockActivityStartIgnoringFlag = wouldBlockActivityStartIgnoringFlags; |
| } |
| } |
| |
| private class FinishedActivityEntry { |
| int mUid; |
| int mTaskId; |
| int mLaunchCount; |
| |
| FinishedActivityEntry(ActivityRecord ar) { |
| FinishedActivityEntry entry = mTaskIdToFinishedActivity.get(ar.getTask().mTaskId); |
| int taskId = ar.getTask().mTaskId; |
| this.mUid = ar.getUid(); |
| this.mTaskId = taskId; |
| this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1; |
| |
| mService.mH.postDelayed(() -> { |
| synchronized (mService.mGlobalLock) { |
| if (mTaskIdToFinishedActivity.get(taskId) == this) { |
| mTaskIdToFinishedActivity.remove(taskId); |
| } |
| |
| if (mTopFinishedActivity == this) { |
| mTopFinishedActivity = null; |
| } |
| } |
| }, ASM_GRACEPERIOD_TIMEOUT_MS); |
| } |
| } |
| } |