/*
 * 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.app.ActivityManager.PROCESS_STATE_TOP;
import static android.app.ActivityOptions.BackgroundActivityStartMode;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION_CODES.BAKLAVA;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static android.os.Process.INVALID_PID;
import static android.os.Process.INVALID_UID;
import static android.os.Process.ROOT_UID;
import static android.os.Process.SYSTEM_UID;
import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
import static android.security.Flags.asmOptSystemIntoEnforcement;

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.ACTIVITY_BG_START_GRACE_PERIOD_MS;
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.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg;

import static java.lang.annotation.RetentionPolicy.SOURCE;
import static java.util.Objects.requireNonNull;

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.IBackgroundActivityLaunchCallback;
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.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DebugUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.WindowManager;
import android.widget.Toast;

import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.server.UiThread;
import com.android.server.am.PendingIntentRecord;
import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration;

import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
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 String DOC_LINK = "go/android-asm";

    /** Used to determine which version of the ASM logic was used in logs while we iterate */
    private static final int ASM_VERSION = 12;
    private static final int NO_PROCESS_UID = -1;
    private static final int NO_GRACE_PERIOD = -1;

    private static final BalCheckConfiguration BAL_CHECK_FOREGROUND = new BalCheckConfiguration(
            /* isCheckingForFgsStarts */ false,
            /* checkVisibility */ true,
            /* checkOtherExemptions */ false,
            NO_GRACE_PERIOD);
    private static final BalCheckConfiguration BAL_CHECK_BACKGROUND = new BalCheckConfiguration(
            /* isCheckingForFgsStarts */ false,
            /* checkVisibility */ false,
            /* checkOtherExemptions */ true,
            NO_GRACE_PERIOD);
    private static final BalCheckConfiguration BAL_CHECK_GRACE_PERIOD = new BalCheckConfiguration(
            /* isCheckingForFgsStarts */ false,
            /* checkVisibility */ false,
            /* checkOtherExemptions */ false,
            ACTIVITY_BG_START_GRACE_PERIOD_MS);

    static final String AUTO_OPT_IN_NOT_PENDING_INTENT = "notPendingIntent";
    static final String AUTO_OPT_IN_CALL_FOR_RESULT = "callForResult";
    static final String AUTO_OPT_IN_SAME_UID = "sameUid";
    static final String AUTO_OPT_IN_COMPAT = "compatibility";

    /** 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;
    /**  Feature flag for go/activity-security rules */
    @ChangeId
    @EnabledAfter(targetSdkVersion = BAKLAVA)
    static final long ASM_RESTRICTIONS = 230590090L;
    public static final ActivityOptions ACTIVITY_OPTIONS_SYSTEM_DEFINED =
            ActivityOptions.makeBasic()
                    .setPendingIntentBackgroundActivityStartMode(
                            MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED)
                    .setPendingIntentCreatorBackgroundActivityStartMode(
                            MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);

    private final ActivityTaskManagerService mService;

    private final ActivityTaskSupervisor mSupervisor;
    @GuardedBy("mStrictModeBalCallbacks")
    private final SparseArray<ArrayMap<IBinder, IBackgroundActivityLaunchCallback>>
            mStrictModeBalCallbacks = new SparseArray<>();


    // TODO(b/263368846) Rename when ASM logic is moved in
    @Retention(SOURCE)
    @IntDef({
            BAL_ALLOW_ALLOWLISTED_COMPONENT,
            BAL_ALLOW_ALLOWLISTED_UID,
            BAL_ALLOW_BOUND_BY_FOREGROUND,
            BAL_ALLOW_DEFAULT,
            BAL_ALLOW_FOREGROUND,
            BAL_ALLOW_GRACE_PERIOD,
            BAL_ALLOW_PENDING_INTENT,
            BAL_ALLOW_PERMISSION,
            BAL_ALLOW_SAW_PERMISSION,
            BAL_ALLOW_SDK_SANDBOX,
            BAL_ALLOW_TOKEN,
            BAL_ALLOW_VISIBLE_WINDOW,
            BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
            BAL_ALLOW_WALLPAPER,
            BAL_ALLOW_NOTIFICATION_TOKEN,
            BAL_BLOCK
    })
    public @interface BalCode {}

    static final int BAL_BLOCK = FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_BLOCKED;

    static final int BAL_ALLOW_DEFAULT =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_DEFAULT;

    // Following codes are in order of precedence

    /** Important UIDs which should be always allowed to launch activities */
    static final int BAL_ALLOW_ALLOWLISTED_UID =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_ALLOWLISTED_UID;

    /** Apps that fulfill a certain role that can can always launch new tasks */
    static final int BAL_ALLOW_ALLOWLISTED_COMPONENT =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_ALLOWLISTED_COMPONENT;

    /**
     * Apps which currently have a visible window or are bound by a service with a visible
     * window
     */
    static final int BAL_ALLOW_VISIBLE_WINDOW =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_VISIBLE_WINDOW;

    /** Allowed due to the PendingIntent sender */
    static final int BAL_ALLOW_PENDING_INTENT =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_PENDING_INTENT;

    /**
     * App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges
     * granted to it
     */
    static final int BAL_ALLOW_PERMISSION =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_BAL_PERMISSION;

    /** Process has SYSTEM_ALERT_WINDOW permission granted to it */
    static final int BAL_ALLOW_SAW_PERMISSION =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_SAW_PERMISSION;

    /** App is in grace period after an activity was started or finished */
    static final int BAL_ALLOW_GRACE_PERIOD =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_GRACE_PERIOD;

    /** App is in a foreground task or bound to a foreground service (but not itself visible) */
    static final int BAL_ALLOW_FOREGROUND =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_FOREGROUND;

    /** Process belongs to a SDK sandbox */
    static final int BAL_ALLOW_SDK_SANDBOX =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_SDK_SANDBOX;

    /** Process belongs to a SDK sandbox */
    static final int BAL_ALLOW_NON_APP_VISIBLE_WINDOW =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_NON_APP_VISIBLE_WINDOW;

    /** Process belongs to a SDK sandbox */
    static final int BAL_ALLOW_TOKEN =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_TOKEN;

    /** Process belongs to a SDK sandbox */
    static final int BAL_ALLOW_BOUND_BY_FOREGROUND =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_BOUND_BY_FOREGROUND;

    static final int BAL_ALLOW_NOTIFICATION_TOKEN =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_NOTIFICATION_TOKEN;

    static final int BAL_ALLOW_WALLPAPER =
            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_WALLPAPER;

    static String balCodeToString(@BalCode int balCode) {
        return switch (balCode) {
            case BAL_ALLOW_ALLOWLISTED_COMPONENT -> "BAL_ALLOW_ALLOWLISTED_COMPONENT";
            case BAL_ALLOW_ALLOWLISTED_UID -> "BAL_ALLOW_ALLOWLISTED_UID";
            case BAL_ALLOW_BOUND_BY_FOREGROUND -> "BAL_ALLOW_BOUND_BY_FOREGROUND";
            case BAL_ALLOW_DEFAULT -> "BAL_ALLOW_DEFAULT";
            case BAL_ALLOW_FOREGROUND -> "BAL_ALLOW_FOREGROUND";
            case BAL_ALLOW_GRACE_PERIOD -> "BAL_ALLOW_GRACE_PERIOD";
            case BAL_ALLOW_NON_APP_VISIBLE_WINDOW -> "BAL_ALLOW_NON_APP_VISIBLE_WINDOW";
            case BAL_ALLOW_PENDING_INTENT -> "BAL_ALLOW_PENDING_INTENT";
            case BAL_ALLOW_PERMISSION -> "BAL_ALLOW_PERMISSION";
            case BAL_ALLOW_SAW_PERMISSION -> "BAL_ALLOW_SAW_PERMISSION";
            case BAL_ALLOW_SDK_SANDBOX -> "BAL_ALLOW_SDK_SANDBOX";
            case BAL_ALLOW_TOKEN -> "BAL_ALLOW_TOKEN";
            case BAL_ALLOW_VISIBLE_WINDOW -> "BAL_ALLOW_VISIBLE_WINDOW";
            case BAL_ALLOW_NOTIFICATION_TOKEN -> "BAL_ALLOW_NOTIFICATION_TOKEN";
            case BAL_ALLOW_WALLPAPER -> "BAL_ALLOW_WALLPAPER";
            case BAL_BLOCK -> "BAL_BLOCK";
            default -> throw new IllegalArgumentException("Unexpected value: " + balCode);
        };
    }

    static String balStartModeToString(@BackgroundActivityStartMode int startMode) {
        return switch (startMode) {
            case MODE_BACKGROUND_ACTIVITY_START_ALLOWED -> "MODE_BACKGROUND_ACTIVITY_START_ALLOWED";
            case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED ->
                    "MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED";
            case MODE_BACKGROUND_ACTIVITY_START_COMPAT -> "MODE_BACKGROUND_ACTIVITY_START_COMPAT";
            case MODE_BACKGROUND_ACTIVITY_START_DENIED -> "MODE_BACKGROUND_ACTIVITY_START_DENIED";
            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS ->
                    "MODE_BACKGROUND_ACTIVITY_START_ALWAYS";
            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE ->
                    "MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE";
            default -> "MODE_BACKGROUND_ACTIVITY_START_ALLOWED(" + startMode + ")";
        };
    }

    @GuardedBy("mService.mGlobalLock")
    private final 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 ActivityTaskManagerService getService() {
        return mService;
    }

    private ActivityTaskSupervisor getSupervisor() {
        return mSupervisor;
    }

    private boolean isHomeApp(int uid, @Nullable String packageName) {
        WindowProcessController homeProcess = getService().mHomeProcess;
        if (homeProcess != null && (homeProcess.mUid != SYSTEM_UID || packageName == null)) {
            // Fast check (skip if sharing system UID and we have a package name)
            return uid == homeProcess.mUid;
        }
        if (packageName == null) {
            return false;
        }
        ComponentName activity =
                getService().getPackageManagerInternalLocked()
                        .getDefaultHomeActivity(UserHandle.getUserId(uid));
        return activity != null && packageName.equals(activity.getPackageName());
    }

    @VisibleForTesting class BalState {

        private final String mCallingPackage;
        private final int mCallingUid;
        private final int mCallingPid;
        private final @ActivityTaskManagerService.AppSwitchState int mAppSwitchState;
        private final boolean mCallingUidHasVisibleActivity;
        private final boolean mCallingUidHasVisibleNotPinnedActivity;
        private final boolean mCallingUidHasNonAppVisibleWindow;
        private final @ActivityManager.ProcessState int mCallingUidProcState;
        private final boolean mIsCallingUidPersistentSystemProcess;
        final BackgroundStartPrivileges mBalAllowedByPiSender;
        final BackgroundStartPrivileges mBalAllowedByPiCreator;
        private final String mRealCallingPackage;
        private final int mRealCallingUid;
        private final int mRealCallingPid;
        private final boolean mRealCallingUidHasVisibleActivity;
        private final boolean mRealCallingUidHasVisibleNotPinnedActivity;
        private final boolean mRealCallingUidHasNonAppVisibleWindow;
        private final @ActivityManager.ProcessState int mRealCallingUidProcState;
        private final boolean mIsRealCallingUidPersistentSystemProcess;
        private final PendingIntentRecord mOriginatingPendingIntent;
        private final boolean mAllowBalExemptionForSystemProcess;
        private final Intent mIntent;
        private final WindowProcessController mCallerApp;
        private final WindowProcessController mRealCallerApp;
        private final boolean mIsCallForResult;
        private final ActivityOptions mCheckedOptions;
        final String mAutoOptInReason;
        private final boolean mAutoOptInCaller;
        private BalVerdict mResultForCaller;
        private BalVerdict mResultForRealCaller;

        @VisibleForTesting BalState(int callingUid, int callingPid, final String callingPackage,
                 int realCallingUid, int realCallingPid,
                 WindowProcessController callerApp,
                 PendingIntentRecord originatingPendingIntent,
                 boolean allowBalExemptionForSystemProcess,
                 ActivityRecord resultRecord,
                 Intent intent,
                 ActivityOptions checkedOptions) {
            this.mCallingPackage = callingPackage;
            mCallingUid = callingUid;
            mCallingPid = callingPid;
            mRealCallingUid = realCallingUid;
            mRealCallingPid = realCallingPid;
            mCallerApp = callerApp;
            mAllowBalExemptionForSystemProcess = allowBalExemptionForSystemProcess;
            mOriginatingPendingIntent = originatingPendingIntent;
            mIntent = intent;
            mRealCallingPackage = getService().getPackageNameIfUnique(realCallingUid,
                    realCallingPid);
            mIsCallForResult = resultRecord != null;
            mCheckedOptions = checkedOptions;
            @BackgroundActivityStartMode int callerBackgroundActivityStartMode =
                    checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode();
            @BackgroundActivityStartMode int realCallerBackgroundActivityStartMode =
                    checkedOptions.getPendingIntentBackgroundActivityStartMode();

            if (originatingPendingIntent == null) {
                mAutoOptInReason = AUTO_OPT_IN_NOT_PENDING_INTENT;
                mAutoOptInCaller = true;
            } else if (mIsCallForResult) {
                mAutoOptInReason = AUTO_OPT_IN_CALL_FOR_RESULT;
                mAutoOptInCaller = false;
            } else if (callingUid == realCallingUid) {
                mAutoOptInReason = AUTO_OPT_IN_SAME_UID;
                mAutoOptInCaller = false;
            } else if (realCallerBackgroundActivityStartMode
                    == MODE_BACKGROUND_ACTIVITY_START_COMPAT) {
                mAutoOptInReason = AUTO_OPT_IN_COMPAT;
                mAutoOptInCaller = false;
            } else {
                mAutoOptInReason = null;
                mAutoOptInCaller = false;
            }

            if (mAutoOptInCaller) {
                // grant BAL privileges unless explicitly opted out
                mBalAllowedByPiCreator =
                        callerBackgroundActivityStartMode == MODE_BACKGROUND_ACTIVITY_START_DENIED
                                ? BackgroundStartPrivileges.NONE
                                : BackgroundStartPrivileges.ALLOW_BAL;
            } else {
                // for PendingIntents we restrict BAL based on target_sdk
                mBalAllowedByPiCreator = getBackgroundStartPrivilegesAllowedByCreator(
                        callingUid, callingPackage, checkedOptions);
            }

            if (mAutoOptInReason != null) {
                // grant BAL privileges unless explicitly opted out
                mBalAllowedByPiSender = realCallerBackgroundActivityStartMode
                        == MODE_BACKGROUND_ACTIVITY_START_DENIED
                        ? BackgroundStartPrivileges.NONE
                        : BackgroundStartPrivileges.ALLOW_BAL;
            } else {
                // for PendingIntents we restrict BAL based on target_sdk
                mBalAllowedByPiSender =
                        PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
                                checkedOptions, realCallingUid, mRealCallingPackage);
            }

            mAppSwitchState = getService().getBalAppSwitchesState();
            mCallingUidProcState = getService().mActiveUids.getUidState(callingUid);
            mIsCallingUidPersistentSystemProcess =
                    mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
            mCallingUidHasVisibleActivity =
                    getService().mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
            mCallingUidHasVisibleNotPinnedActivity = getService().mVisibleActivityProcessTracker
                    .hasVisibleNotPinnedActivity(callingUid);
            mCallingUidHasNonAppVisibleWindow = getService().mActiveUids.hasNonAppVisibleWindow(
                    callingUid);
            if (realCallingUid == NO_PROCESS_UID) {
                // no process provided
                mRealCallingUidProcState = PROCESS_STATE_NONEXISTENT;
                mRealCallingUidHasVisibleActivity = false;
                mRealCallingUidHasNonAppVisibleWindow = false;
                mRealCallingUidHasVisibleNotPinnedActivity = false;
                mRealCallerApp = null;
                mIsRealCallingUidPersistentSystemProcess = false;
            } else if (callingUid == realCallingUid) {
                mRealCallingUidProcState = mCallingUidProcState;
                mRealCallingUidHasVisibleActivity = mCallingUidHasVisibleActivity;
                mRealCallingUidHasVisibleNotPinnedActivity = mCallingUidHasVisibleNotPinnedActivity;
                mRealCallingUidHasNonAppVisibleWindow = mCallingUidHasNonAppVisibleWindow;
                // In the PendingIntent case callerApp is not passed in, so resolve it ourselves.
                mRealCallerApp = callerApp == null
                        ? getService().getProcessController(realCallingPid, realCallingUid)
                        : callerApp;
                mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess;
            } else {
                mRealCallingUidProcState = getService().mActiveUids.getUidState(realCallingUid);
                mRealCallingUidHasVisibleActivity =
                        getService().mVisibleActivityProcessTracker.hasVisibleActivity(
                                realCallingUid);
                mRealCallingUidHasVisibleNotPinnedActivity =
                        getService().mVisibleActivityProcessTracker.hasVisibleNotPinnedActivity(
                                realCallingUid);
                mRealCallingUidHasNonAppVisibleWindow =
                        getService().mActiveUids.hasNonAppVisibleWindow(realCallingUid);
                mRealCallerApp = getService().getProcessController(realCallingPid, realCallingUid);
                mIsRealCallingUidPersistentSystemProcess =
                        mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
            }
        }

        private BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCreator(
                int callingUid, String callingPackage, ActivityOptions checkedOptions) {
            switch (checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()) {
                case MODE_BACKGROUND_ACTIVITY_START_ALLOWED:
                case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE:
                case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS:
                    return BackgroundStartPrivileges.ALLOW_BAL;
                case MODE_BACKGROUND_ACTIVITY_START_DENIED:
                    return BackgroundStartPrivileges.NONE;
                case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
                    // no explicit choice by the app - let us decide what to do
                    if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) {
                        // root and system must always opt in explicitly
                        return BackgroundStartPrivileges.NONE;
                    }
                    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 = getService().getPackageManagerInternalLocked().getNameForUid(uid);
            if (name == null) {
                name = "uid=" + uid;
            }
            return name + "[debugOnly]";
        }

        private boolean hasRealCaller() {
            return mRealCallingUid != NO_PROCESS_UID;
        }

        boolean isPendingIntent() {
            return mOriginatingPendingIntent != null && hasRealCaller();
        }

        private boolean callerIsRealCaller() {
            return mCallingUid == mRealCallingUid;
        }

        public void setResultForCaller(BalVerdict resultForCaller) {
            Preconditions.checkState(mResultForCaller == null,
                    "mResultForCaller can only be set once");
            this.mResultForCaller = resultForCaller;
        }

        public void setResultForRealCaller(BalVerdict resultForRealCaller) {
            Preconditions.checkState(mResultForRealCaller == null,
                    "mResultForRealCaller can only be set once");
            this.mResultForRealCaller = resultForRealCaller;
        }

        public boolean callerExplicitOptInOrAutoOptIn() {
            if (mAutoOptInCaller) {
                return !callerExplicitOptOut();
            }
            return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
                    != MODE_BACKGROUND_ACTIVITY_START_DENIED
                    && mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
                    != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
        }

        public boolean realCallerExplicitOptInOrAutoOptIn() {
            if (mAutoOptInReason != null) {
                return !realCallerExplicitOptOut();
            }
            return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                    != MODE_BACKGROUND_ACTIVITY_START_DENIED
                    && mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                    != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
        }

        public boolean callerExplicitOptOut() {
            return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
                    == MODE_BACKGROUND_ACTIVITY_START_DENIED;
        }

        public boolean realCallerExplicitOptOut() {
            return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                    == MODE_BACKGROUND_ACTIVITY_START_DENIED;
        }

        public boolean callerExplicitOptInOrOut() {
            return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
                    != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
        }

        public boolean realCallerExplicitOptInOrOut() {
            return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                    != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
        }

        @Override
        public String toString() {
            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("; callingUidHasVisibleActivity: ").append(mCallingUidHasVisibleActivity);
            sb.append("; callingUidHasVisibleNotPinnedActivity: ")
                    .append(mCallingUidHasVisibleNotPinnedActivity);
            sb.append("; callingUidHasNonAppVisibleWindow: ").append(
                    mCallingUidHasNonAppVisibleWindow);
            sb.append("; callingUidProcState: ").append(DebugUtils.valueToString(
                    ActivityManager.class, "PROCESS_STATE_", mCallingUidProcState));
            sb.append("; isCallingUidPersistentSystemProcess: ")
                    .append(mIsCallingUidPersistentSystemProcess);
            sb.append("; allowBalExemptionForSystemProcess: ")
                    .append(mAllowBalExemptionForSystemProcess);
            sb.append("; intent: ").append(mIntent);
            sb.append("; callerApp: ").append(mCallerApp);
            if (mCallerApp != null) {
                sb.append("; inVisibleTask: ").append(mCallerApp.hasActivityInVisibleTask());
            }
            sb.append("; balAllowedByPiCreator: ")
                    .append(mBalAllowedByPiCreator);
            if (mResultForCaller != null) {
                sb.append("; resultIfPiCreatorAllowsBal: ")
                        .append(balCodeToString(mResultForCaller.mCode));
            }
            sb.append("; callerStartMode: ").append(balStartModeToString(
                    mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()));
            sb.append("; hasRealCaller: ").append(hasRealCaller());
            sb.append("; isCallForResult: ").append(mIsCallForResult);
            sb.append("; isPendingIntent: ").append(isPendingIntent());
            sb.append("; autoOptInReason: ").append(mAutoOptInReason);
            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("; realCallingUidHasVisibleActivity: ")
                        .append(mRealCallingUidHasVisibleActivity);
                sb.append("; realCallingUidHasVisibleNotPinnedActivity: ")
                        .append(mRealCallingUidHasVisibleNotPinnedActivity);
                sb.append("; realCallingUidHasNonAppVisibleWindow: ")
                        .append(mRealCallingUidHasNonAppVisibleWindow);
                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);
                if (mResultForRealCaller != null) {
                    sb.append("; resultIfPiSenderAllowsBal: ")
                            .append(balCodeToString(mResultForRealCaller.mCode));
                }
                sb.append("; realCallerStartMode: ").append(balStartModeToString(
                        mCheckedOptions.getPendingIntentBackgroundActivityStartMode()));
            }
            // features
            sb.append("; balDontBringExistingBackgroundTaskStackToFg: ")
                    .append(balDontBringExistingBackgroundTaskStackToFg());
            sb.append("]");
            return sb.toString();
        }
    }

    static class BalVerdict {
        static final BalVerdict BLOCK = new BalVerdict(BAL_BLOCK, "Blocked", true);
        static final BalVerdict ALLOW_BY_DEFAULT =
                new BalVerdict(BAL_ALLOW_DEFAULT, "Default", true);
        // Careful using this - it will bypass all ASM checks.
        static final BalVerdict ALLOW_PRIVILEGED =
                new BalVerdict(BAL_ALLOW_ALLOWLISTED_UID, "PRIVILEGED", true);
        private final @BalCode int mCode;
        private final String mMessage;
        private final boolean mImmutable;
        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;
        private boolean mAllowlisteUid;
        private boolean mInGracePeriod;
        private boolean mAllowsNewTask;
        private boolean mVisibleOrForeground;
        private SparseBooleanArray mAdditionalBalCodes;

        BalVerdict(@BalCode int balCode, String message, boolean immutable) {
            mCode = balCode;
            mMessage = message;
            mImmutable = immutable;
            mAllowlisteUid = balCode == BAL_ALLOW_ALLOWLISTED_UID;
            mInGracePeriod = balCode == BAL_ALLOW_GRACE_PERIOD;
        }

        BalVerdict(@BalCode int balCode, String message) {
            this(balCode, message, false);
        }

        public BalVerdict withProcessInfo(String msg, WindowProcessController process) {
            if (mImmutable) {
                throw new IllegalStateException(this + " is marked immutable");
            }
            mProcessInfo = msg + " (uid=" + process.mUid + ",pid=" + process.getPid() + ")";
            return this;
        }

        boolean blocks() {
            return mCode == BAL_BLOCK;
        }

        boolean allows() {
            return !blocks();
        }

        boolean isImmutable() {
            return mImmutable;
        }

        void setOnlyCreatorAllows(boolean onlyCreatorAllows) {
            if (mImmutable) {
                throw new IllegalStateException(this + " is marked immutable");
            }
            mOnlyCreatorAllows = onlyCreatorAllows;
        }

        boolean onlyCreatorAllows() {
            return mOnlyCreatorAllows;
        }

        @VisibleForTesting
        BalVerdict setBasedOnRealCaller() {
            if (mImmutable) {
                throw new IllegalStateException(this + " is marked immutable");
            }
            mBasedOnRealCaller = true;
            return this;
        }

        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append(balCodeToString(mCode));
            if (!this.isImmutable()) {
                if (mOnlyCreatorAllows) {
                    builder.append(" [onlyCaller]");
                } else if (mBasedOnRealCaller) {
                    builder.append(" [realCaller]");
                }
                if (DEBUG_ACTIVITY_STARTS) {
                    builder.append(" (").append(mMessage);
                    if (mProcessInfo != null) {
                        builder.append("; ");
                        builder.append(mProcessInfo);
                    }
                    if (mInGracePeriod) {
                        builder.append("; ").append(balCodeToString(BAL_ALLOW_GRACE_PERIOD));
                    }
                    if (mAllowlisteUid) {
                        builder.append("; ").append(balCodeToString(BAL_ALLOW_ALLOWLISTED_UID));
                    }
                    if (mAdditionalBalCodes != null) {
                        for (int i = 0; i < mAdditionalBalCodes.size(); i++) {
                            builder.append("; ")
                                    .append(balCodeToString(mAdditionalBalCodes.keyAt(i)));
                        }
                    }
                    builder.append(")");
                }
            }
            return builder.toString();
        }

        public @BalCode int getCode() {
            return mCode;
        }

        void setAllowlisteUid(boolean allowlisteUid) {
            if (mImmutable) {
                return;
            }
            mAllowlisteUid = allowlisteUid; }

        boolean isAllowlistedUid() {
            return mAllowlisteUid;
        }

        public void setInGracePeriod(boolean inGracePeriod) {
            if (mImmutable) {
                return;
            }
            mInGracePeriod = inGracePeriod; }

        public boolean isInGracePeriod() {
            return mInGracePeriod;
        }

        public boolean isBasedOnRealCaller() {
            return mBasedOnRealCaller;
        }

        public boolean isBasedOnCaller() {
            return !mBasedOnRealCaller;
        }

        public void addAdditionalVerdict(BalVerdict verdict) {
            if (mImmutable) {
                return;
            }
            if (mAdditionalBalCodes == null) {
                mAdditionalBalCodes = new SparseBooleanArray();
            }
            mAdditionalBalCodes.put(verdict.getCode(), true);
        }

        public BalVerdict allowNewTask() {
            if (mImmutable) {
                throw new IllegalStateException();
            }
            this.mAllowsNewTask = true;
            return this;
        }

        public boolean allowsNewTask() {
            return mAllowsNewTask;
        }

        public BalVerdict setVisibleOrForeground() {
            if (mImmutable) {
                throw new IllegalStateException();
            }
            this.mVisibleOrForeground = true;
            return this;
        }

        public boolean isVisibleOrForeground() {
            return mVisibleOrForeground;
        }
    }

    /**
     * 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 allowBalExemptionForSystemProcess If set to true, the
     *        PendingIntent's sender will allow additional exemptions.
     *        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,
            boolean allowBalExemptionForSystemProcess,
            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,
                allowBalExemptionForSystemProcess, 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 (getService().hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
                state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX,
                        "uid in SDK sandbox has visible (non-toast) window").allowNewTask());
                return allowBasedOnRealCaller(state);
            }
        }

        BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state);
        state.setResultForCaller(resultForCaller);

        if (!state.hasRealCaller()) {
            if (resultForCaller.allows()) {
                return allowBasedOnCaller(state);
            }
            return abortLaunch(state);
        }

        // 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
                : checkBackgroundActivityStartAllowedByRealCaller(state);
        state.setResultForRealCaller(resultForRealCaller);

        if (state.isPendingIntent() && resultForCaller.allows() && resultForRealCaller.blocks()) {
            resultForCaller.setOnlyCreatorAllows(true);
        }

        // Handle cases with explicit opt-in
        if (resultForCaller.allows() && state.callerExplicitOptInOrAutoOptIn()) {
            return allowBasedOnCaller(state);
        }
        if (resultForRealCaller.allows() && state.realCallerExplicitOptInOrAutoOptIn()) {
            return allowBasedOnRealCaller(state);
        }
        // Handle PendingIntent cases with default behavior next
        boolean callerCanAllow = resultForCaller.allows() && !state.callerExplicitOptOut();
        boolean realCallerCanAllow = resultForRealCaller.allows()
                && !state.realCallerExplicitOptOut();
        if (callerCanAllow) {
            // Allowed before V by creator
            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+!"
                        + " goo.gle/android-bal"
                        + " (missing opt in by PI creator)!" + state);
                return allowBasedOnCaller(state);
            }
        }
        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+! "
                        + " goo.gle/android-bal"
                        + " (missing opt in by PI sender)!" + state);
                return allowBasedOnRealCaller(state);
            }
        }
        // caller or real caller could start the activity, but would need to explicitly opt in
        if (callerCanAllow || realCallerCanAllow) {
            Slog.w(TAG, "Without BAL hardening this activity start would be allowed");
        }
        // neither the caller not the realCaller can allow or have explicitly opted out
        return abortLaunch(state);
    }

    private BalVerdict allowBasedOnCaller(BalState state) {
        if (DEBUG_ACTIVITY_STARTS) {
            Slog.d(TAG, "Background activity launch allowed based on caller. " + state);
        }
        return statsLog(state.mResultForCaller, state);
    }

    private BalVerdict allowBasedOnRealCaller(BalState state) {
        if (DEBUG_ACTIVITY_STARTS) {
            Slog.d(TAG, "Background activity launch allowed based on real caller. " + state);
        }
        return statsLog(state.mResultForRealCaller, state);
    }

    private BalVerdict abortLaunch(BalState state) {
        Slog.wtf(TAG, "Background activity launch blocked! goo.gle/android-bal "
                + state);
        if (Build.IS_DEBUGGABLE
                && (state.mResultForCaller.allows() || state.mResultForRealCaller.allows())) {
            // only show a toast if either caller or real caller could launch if they opted in
            showToast("BAL blocked. goo.gle/android-bal");
        }
        BalVerdict verdict = statsLog(BalVerdict.BLOCK, state);
        String abortDebugMessage;
        if (state.isPendingIntent()) {
            abortDebugMessage =
                    "PendingIntent Activity start blocked in " + state.mRealCallingPackage
                            + ". "
                            + "PendingIntent was created in " + state.mCallingPackage
                            + ". "
                            + (state.mResultForRealCaller.allows()
                            ? state.mRealCallingPackage
                            + " could opt in to grant BAL privileges when sending. "
                            : "")
                            + (state.mResultForCaller.allows()
                            ? state.mCallingPackage
                            + " could opt in to grant BAL privileges when creating."
                            : "")
                            + "The intent would have started " + state.mIntent.getComponent();
        } else {
            abortDebugMessage = "Activity start blocked. "
                    + "The intent would have started " + state.mIntent.getComponent();
        }
        strictModeLaunchAborted(state.mCallingUid, abortDebugMessage);
        if (!state.callerIsRealCaller()) {
            strictModeLaunchAborted(state.mRealCallingUid, abortDebugMessage);
        }
        return verdict;
    }

    /**
     * Retrieve a registered strict mode callback for BAL.
     * @param uid the uid of the app.
     * @return the callback if it exists, returns <code>null</code> otherwise.
     */
    @Nullable
    Map<IBinder, IBackgroundActivityLaunchCallback> getStrictModeBalCallbacks(int uid) {
        ArrayMap<IBinder, IBackgroundActivityLaunchCallback> callbackMap;
        synchronized (mStrictModeBalCallbacks) {
            callbackMap =
                    mStrictModeBalCallbacks.get(uid);
            if (callbackMap == null) {
                return null;
            }
            return new ArrayMap<>(callbackMap);
        }
    }

    /**
     * Add strict mode callback for BAL.
     *
     * @param uid      the UID for which the binder is registered.
     * @param callback the {@link IBackgroundActivityLaunchCallback} binder to call when BAL is
     *                 blocked.
     * @return {@code true} if the callback has been successfully added.
     */
    boolean addStrictModeCallback(int uid, IBinder callback) {
        IBackgroundActivityLaunchCallback balCallback =
                IBackgroundActivityLaunchCallback.Stub.asInterface(callback);
        synchronized (mStrictModeBalCallbacks) {
            ArrayMap<IBinder, IBackgroundActivityLaunchCallback> callbackMap =
                    mStrictModeBalCallbacks.get(uid);
            if (callbackMap == null) {
                callbackMap = new ArrayMap<>();
                mStrictModeBalCallbacks.put(uid, callbackMap);
            }
            if (callbackMap.containsKey(callback)) {
                return false;
            }
            callbackMap.put(callback, balCallback);
        }
        try {
            callback.linkToDeath(() -> removeStrictModeCallback(uid, callback), 0);
        } catch (RemoteException e) {
            removeStrictModeCallback(uid, callback);
        }
        return true;
    }

    /**
     * Remove strict mode callback for BAL.
     *
     * @param uid      the UID for which the binder is registered.
     * @param callback the {@link IBackgroundActivityLaunchCallback} binder to call when BAL is
     *                 blocked.
     */
    void removeStrictModeCallback(int uid, IBinder callback) {
        synchronized (mStrictModeBalCallbacks) {
            Map<IBinder, IBackgroundActivityLaunchCallback> callbackMap =
                    mStrictModeBalCallbacks.get(uid);
            if (callback == null || !callbackMap.containsKey(callback)) {
                return;
            }
            callbackMap.remove(callback);
            if (callbackMap.isEmpty()) {
                mStrictModeBalCallbacks.remove(uid);
            }
        }
    }

    private void strictModeLaunchAborted(int callingUid, String message) {
        Map<IBinder, IBackgroundActivityLaunchCallback> strictModeBalCallbacks =
                getStrictModeBalCallbacks(callingUid);
        if (strictModeBalCallbacks == null) {
            return;
        }
        for (Map.Entry<IBinder, IBackgroundActivityLaunchCallback> callbackEntry :
                strictModeBalCallbacks.entrySet()) {
            try {
                callbackEntry.getValue().onBackgroundActivityLaunchAborted(message);
            } catch (RemoteException e) {
                removeStrictModeCallback(callingUid, callbackEntry.getKey());
            }
        }
    }

    /**
     * @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) {
        boolean evaluateVisibleOnly =
                state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
                        == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
        boolean basedOnRealCaller = false;
        if (evaluateVisibleOnly) {
            return evaluateChain(state, basedOnRealCaller, mCheckCallerVisible,
                    mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground);
        }
        if (state.isPendingIntent()) {
            // PendingIntents should mostly be allowed by the sender (real caller) or a permission
            // the creator of the PendingIntent has. Visibility should be the exceptional case, so
            // test it last (this does not change the result, just the bal code).
            return evaluateChain(state, basedOnRealCaller, mCheckCallerIsAllowlistedUid,
                    mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
                    mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
                    mCheckCallerProcessAllowsBackground, mCheckCallerVisible,
                    mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground,
                    mCheckCallerProcessInGracePeriod);
        }
        return evaluateChain(state, basedOnRealCaller, mCheckCallerVisible,
                mCheckCallerNonAppVisible, mCheckCallerNonActivityTop,
                mCheckCallerProcessAllowsForeground, mCheckCallerIsAllowlistedUid,
                mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
                mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
                mCheckCallerProcessAllowsBackground, mCheckCallerProcessInGracePeriod);
    }

    interface BalExemptionCheck {
        BalVerdict evaluate(BalState state);
    }

    private BalVerdict evaluateChain(BalState state, boolean basedOnRealCaller,
            BalExemptionCheck... checks) {
        BalVerdict result = BalVerdict.BLOCK;
        for (BalExemptionCheck check : checks) {
            if (!result.allows()) {
                result = check.evaluate(state);
            } else {
                // we already have a result, but may need to fill in a few missing parts
                if (check == mCheckCallerProcessInGracePeriod
                        || check == mCheckRealCallerProcessInGracePeriod) {
                    result.setInGracePeriod(check.evaluate(state).allows());
                } else if (check == mCheckCallerIsAllowlistedUid
                        || check == mCheckRealCallerAllowlistedUid) {
                    result.setAllowlisteUid(check.evaluate(state).allows());
                } else if (DEBUG_ACTIVITY_STARTS) {
                    result.addAdditionalVerdict(check.evaluate(state));
                }
            }
        }
        if (basedOnRealCaller && !result.isImmutable()) {
            result.setBasedOnRealCaller();
        }
        return result;
    }

    private final BalExemptionCheck mCheckCallerVisible = state -> {
        // This is used to block background activity launch even if the app is still
        // visible to user after user clicking home button.

        // 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.
        // The home app can start apps even if app switches are usually disallowed.
        final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
                || state.mAppSwitchState == APP_SWITCH_FG_ONLY
                || isHomeApp(state.mCallingUid, state.mCallingPackage);
        if (appSwitchAllowedOrFg && state.mCallingUidHasVisibleNotPinnedActivity) {
            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
                    "callingUid has visible non-pinned window")
                    .allowNewTask().setVisibleOrForeground();
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckCallerNonActivityTop = state -> {
        // Allows if the UID process state is at the top and the app is not yet hosting any
        // activities. This is possible if the application starts an activity within its
        // `Application#onCreate` method when the process started, identifying a visible activity
        // from that app is not possible because the application process has not yet fully attached.
        if (state.mCallerApp != null && !state.mCallerApp.hasActivities()
                && state.mCallingUidProcState == PROCESS_STATE_TOP) {
            return new BalVerdict(BAL_ALLOW_FOREGROUND, "callingUid is the current top");
        }
        return BalVerdict.BLOCK;
    };

    private BalVerdict checkNonAppVisibleWindow(int uid, boolean hasNonAppVisibleWindow) {
        if (hasNonAppVisibleWindow) {
            SparseIntArray nonAppVisibleWindowDetails =
                    getService().mActiveUids.getNonAppVisibleWindowDetails(uid);
            if (nonAppVisibleWindowDetails.size() == 1 && nonAppVisibleWindowDetails.get(
                    WindowManager.LayoutParams.TYPE_WALLPAPER) > 0) {
                return new BalVerdict(BAL_ALLOW_WALLPAPER,
                        "uid has wallpaper window").allowNewTask().setVisibleOrForeground();
            }
            return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                    "uid has non-app visible window " + nonAppVisibleWindowDetails)
                    .allowNewTask().setVisibleOrForeground();
        }
        return BalVerdict.BLOCK;
    }

    private final BalExemptionCheck mCheckCallerNonAppVisible = state ->
            checkNonAppVisibleWindow(state.mCallingUid, state.mCallingUidHasNonAppVisibleWindow);

    private final BalExemptionCheck mCheckCallerIsAllowlistedUid = state -> {
        // don't abort for the most important UIDs
        final int callingAppId = UserHandle.getAppId(state.mCallingUid);
        if (state.mCallingUid == Process.ROOT_UID
                || callingAppId == Process.SYSTEM_UID
                || callingAppId == Process.NFC_UID) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_UID, "Important callingUid");
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckCallerIsAllowlistedComponent = state -> {
        // Always allow home application to start activities.
        if (isHomeApp(state.mCallingUid, state.mCallingPackage)) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, "Home app").allowNewTask();
        }

        final int callingAppId = UserHandle.getAppId(state.mCallingUid);
        // IME should always be allowed to start activity, like IME settings.
        final WindowState imeWindow =
                getService().mRootWindowContainer.getCurrentInputMethodWindow();
        if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, "Active ime").allowNewTask();
        }

        // don't abort if the callingUid is a persistent system process
        if (state.mIsCallingUidPersistentSystemProcess) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                    "callingUid is persistent system process").allowNewTask();
        }

        // don't abort if the caller has the same uid as the recents component
        if (getSupervisor().mRecentTasks.isCallerRecents(state.mCallingUid)) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, "Recents Component")
                    .allowNewTask();
        }
        // don't abort if the callingUid is the device owner
        if (getService().isDeviceOwner(state.mCallingUid)) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, "Device Owner").allowNewTask();
        }
        // don't abort if the callingUid is a affiliated profile owner
        if (getService().isAffiliatedProfileOwner(state.mCallingUid)) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, "Affiliated Profile Owner")
                    .allowNewTask();
        }
        // don't abort if the callingUid has companion device
        final int callingUserId = UserHandle.getUserId(state.mCallingUid);
        if (getService().isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, "Companion App").allowNewTask();
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckCallerHasBackgroundPermission = state -> {
        // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
        if (hasBalPermission(state.mCallingUid, state.mCallingPid)) {
            return new BalVerdict(BAL_ALLOW_PERMISSION,
                    "START_ACTIVITIES_FROM_BACKGROUND permission granted").allowNewTask();
        }
        return BalVerdict.BLOCK;
    };
    private final BalExemptionCheck mCheckCallerHasSawPermission = state -> {
        // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
        if (getService().hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
                state.mCallingPackage)) {
            return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
                    "SYSTEM_ALERT_WINDOW permission is granted").allowNewTask();
        }
        return BalVerdict.BLOCK;
    };
    private final BalExemptionCheck mCheckCallerHasBgStartAppOp = state -> {
        // don't abort if the callingUid and callingPackage have the
        // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
        if (isSystemExemptFlagEnabled() && getService().getAppOpsManager().checkOpNoThrow(
                AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
                state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
            return new BalVerdict(BAL_ALLOW_PERMISSION,
                    "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted")
                    .allowNewTask();
        }
        return BalVerdict.BLOCK;
    };


    // Don't abort if the callerApp or other processes of that uid are considered to be in the
    // foreground.
    private final BalExemptionCheck mCheckCallerProcessAllowsForeground =
            state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_FOREGROUND);
    // Don't abort if the callerApp or other processes of that uid are allowed in any way.
    private final BalExemptionCheck mCheckCallerProcessAllowsBackground =
            state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_BACKGROUND);
    private final BalExemptionCheck mCheckCallerProcessInGracePeriod =
            state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_GRACE_PERIOD);

    /**
     * @return A code denoting which BAL rule allows an activity to be started,
     * or {@link #BAL_BLOCK} if the launch should be blocked
     */
    BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) {
        boolean evaluateVisibleOnly =
                state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                        == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
        boolean basedOnRealCaller = true;
        if (evaluateVisibleOnly) {
            return evaluateChain(state, basedOnRealCaller, mCheckRealCallerVisible,
                    mCheckRealCallerNonAppVisible, mCheckRealCallerProcessAllowsBalForeground);
        }
        return evaluateChain(state, basedOnRealCaller, mCheckRealCallerVisible,
                mCheckRealCallerNonAppVisible, mCheckRealCallerProcessAllowsBalForeground,
                mCheckRealCallerBalPermission, mCheckRealCallerSawPermission,
                mCheckRealCallerAllowlistedUid, mCheckRealCallerAllowlistedComponent,
                mCheckRealCallerProcessAllowsBalBackground, mCheckRealCallerProcessInGracePeriod);
    }

    private final BalExemptionCheck mCheckRealCallerVisible = state -> {
        // 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.
        // The home app can start apps even if app switches are usually disallowed.
        final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
                || state.mAppSwitchState == APP_SWITCH_FG_ONLY
                || isHomeApp(state.mRealCallingUid, state.mRealCallingPackage);
        if (appSwitchAllowedOrFg && state.mRealCallingUidHasVisibleNotPinnedActivity) {
            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
                    "realCallingUid has visible non-pinned window")
                    .allowNewTask().setVisibleOrForeground();
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckRealCallerNonAppVisible =
            state -> checkNonAppVisibleWindow(state.mRealCallingUid,
                    state.mRealCallingUidHasNonAppVisibleWindow);

    // Don't abort if the realCallerApp or other processes of that uid are considered to be in
    // the foreground.
    private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground =
            state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);

    // don't abort if the callerApp or other processes of that uid are allowed in any way
    private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground =
            state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
    private final BalExemptionCheck mCheckRealCallerProcessInGracePeriod =
            state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_GRACE_PERIOD);

    private final BalExemptionCheck mCheckRealCallerBalPermission = state -> {
        boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
        if (allowAlways
                && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
            return new BalVerdict(BAL_ALLOW_PERMISSION, "realCallingUid has BAL permission.");
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckRealCallerSawPermission = state -> {
        boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
        // don't abort if the realCallingUid has SYSTEM_ALERT_WINDOW permission
        if (allowAlways
                && getService().hasSystemAlertWindowPermission(state.mRealCallingUid,
                state.mRealCallingPid, state.mRealCallingPackage)) {
            return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
                    "SYSTEM_ALERT_WINDOW permission is granted").allowNewTask();
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckRealCallerAllowlistedUid = state -> {
        boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
        // if the realCallingUid is a persistent system process, abort if the IntentSender
        // wasn't allowed to start an activity
        if ((allowAlways || state.mAllowBalExemptionForSystemProcess)
                && state.mIsRealCallingUidPersistentSystemProcess) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_UID,
                    "realCallingUid is persistent system process AND intent "
                            + "sender forced to allow.");
        }
        return BalVerdict.BLOCK;
    };

    private final BalExemptionCheck mCheckRealCallerAllowlistedComponent = state -> {
        // don't abort if the realCallingUid is an associated companion app
        if (getService().isAssociatedCompanionApp(
                UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) {
            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                    "realCallingUid is a companion app.").allowNewTask();
        }
        return BalVerdict.BLOCK;
    };

    @VisibleForTesting boolean hasBalPermission(int uid, int pid) {
        return ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
                pid, uid) == PERMISSION_GRANTED;
    }

    /**
     * Check if the app allows BAL.
     * <p>
     * See {@link BackgroundLaunchProcessController#areBackgroundActivityStartsAllowed(int, int,
     * String, int, boolean, boolean, boolean, long, long, long)} for details on the
     * exceptions.
     */
    @VisibleForTesting BalVerdict checkProcessAllowsBal(WindowProcessController app,
            BalState state, BalCheckConfiguration balCheckConfiguration) {
        if (app == null) {
            return BalVerdict.BLOCK;
        }
        // first check the original calling process
        final BalVerdict balAllowedForCaller = app
                .areBackgroundActivityStartsAllowed(state.mAppSwitchState, balCheckConfiguration);
        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 =
                    getService().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, balCheckConfiguration);
                        if (balAllowedForUid.allows()) {
                            return balAllowedForUid.withProcessInfo("process", proc);
                        }
                    }
                }
            }
        }
        return BalVerdict.BLOCK;
    }

    /**
     * Check 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, boolean avoidMoveTaskToFront,
            @Nullable Task targetTask, int launchFlags, BalVerdict balVerdict, int callingUid,
            int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) {
        // BAL Exception allowed in all cases
        if (balVerdict.isBasedOnCaller() && balVerdict.isAllowlistedUid()) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[ASM] allowed from allowlisted UID. verdict: " + balVerdict);
            }
            return true;
        }
        if (!android.security.Flags.asmIgnoreGracePeriodExemption() && balVerdict.isBasedOnCaller()
                && balVerdict.isInGracePeriod() && balVerdict.getCode() == BAL_ALLOW_GRACE_PERIOD) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[ASM] allowed based on grace period. verdict: " + balVerdict);
            }
            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 (balVerdict.isBasedOnRealCaller()) {
                if (DEBUG_ACTIVITY_STARTS) {
                    Slog.d(TAG, "[ASM] new task allowed for PendingIntent " + balVerdict);
                }
                return true;
            }
            if (balVerdict.allowsNewTask()) {
                    if (DEBUG_ACTIVITY_STARTS) {
                        Slog.d(TAG, "[ASM] new task allowed based on " + balVerdict);
                    }
                    return true;
            }
        }

        BlockActivityStart bas = new BlockActivityStart();
        if (sourceRecord != null) {
            Task sourceTask = sourceRecord.getTask();

            Task taskToCheck = taskToFront ? sourceTask : targetTask;
            bas = checkTopActivityForAsm(taskToCheck, sourceRecord.getUid(),
                    sourceRecord, bas);

            // 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 && bas.mTopActivityMatchesSource) {
                bas.mTopActivityMatchesSource = (sourceTask != null
                        && (sourceTask.isVisible() || sourceTask == targetTask));
            }
        } else if (targetTask != null && (!taskToFront || avoidMoveTaskToFront)) {
            // We don't have a sourceRecord, and we're launching into an existing task.
            // Allow if callingUid is top of stack.
            bas = checkTopActivityForAsm(targetTask, callingUid,
                    /*sourceRecord*/null, bas);
        } else {
            // We're launching from a non-visible activity. Has any visible app opted in?
            TaskDisplayArea displayArea = targetTask != null && targetTask.getDisplayArea() != null
                    ? targetTask.getDisplayArea()
                    : preferredTaskDisplayArea;
            if (displayArea != null) {
                ArrayList<Task> visibleTasks = displayArea.getVisibleTasks();
                for (int i = 0; i < visibleTasks.size(); i++) {
                    Task task = visibleTasks.get(i);
                    if (!android.security.Flags.asmIgnoreGracePeriodExemption()) {
                        bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas);
                    } else {
                        if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
                            bas.optedIn(task.getTopMostActivity());
                        } else {
                            bas = checkTopActivityForAsm(
                                task, callingUid, /*sourceRecord*/null, bas);
                        }
                    }
                }
            }
        }

        if (bas.mTopActivityMatchesSource) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[ASM] allowed as top activity matches source");
            }
            return true;
        }

        // ASM rules have failed. Log why
        return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid,
                newTask, avoidMoveTaskToFront, targetTask, targetRecord, balVerdict, launchFlags,
                bas, taskToFront);
    }

    private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid,
            int realCallingUid, boolean newTask, boolean avoidMoveTaskToFront, Task targetTask,
            ActivityRecord targetRecord, BalVerdict balVerdict, 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 enforceBlock = bas.mTopActivityOptedIn
                && shouldRestrictActivitySwitch(callingUid);

        boolean allowedByGracePeriod = allowedByAsmGracePeriod(callingUid, sourceRecord, targetTask,
                balVerdict, taskToFront, avoidMoveTaskToFront);

        String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord,
                targetRecord, targetTask, targetTopActivity, realCallingUid, balVerdict,
                enforceBlock, taskToFront, avoidMoveTaskToFront, allowedByGracePeriod,
                bas.mActivityOptedIn);

        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 */
                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 */
                balVerdict.isBasedOnCaller() ? balVerdict.getCode() : BAL_ALLOW_PENDING_INTENT,
                /* debug_info */
                asmDebugInfo
        );

        String launchedFromPackageName = targetRecord.launchedFromPackage;
        if (shouldShowToast(callingUid)) {
            String toastText = DOC_LINK
                    + (enforceBlock ? " blocked " : " would block ")
                    + getApplicationLabel(getService().mContext.getPackageManager(),
                    launchedFromPackageName);
            showToast(toastText);

            Slog.i(TAG, asmDebugInfo);
        }

        if (enforceBlock) {
            Slog.e(TAG, "[ASM] Abort Launching r: " + targetRecord
                    + " as source: "
                    + (sourceRecord != null ? sourceRecord : launchedFromPackageName)
                    + " is in background. New task: " + newTask
                    + ". Top activity: " + targetTopActivity
                    + ". BAL Verdict: " + balVerdict);

            return false;
        }

        if (DEBUG_ACTIVITY_STARTS) {
            Slog.d(TAG, "[ASM] Allow Launching r: " + targetRecord
                    + " as source: "
                    + (sourceRecord != null ? sourceRecord : launchedFromPackageName)
                    + " is in background. New task: " + newTask
                    + ". Top activity: " + targetTopActivity
                    + ". BAL Verdict: " + balVerdict);
        }
        return true;
    }

    @VisibleForTesting void showToast(String toastText) {
        UiThread.getHandler().post(() -> Toast.makeText(getService().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, BalVerdict balVerdict) {
        if ((launchFlags & FLAG_ACTIVITY_NEW_TASK) != FLAG_ACTIVITY_NEW_TASK
                || (balVerdict.isBasedOnCaller() && balVerdict.isAllowlistedUid())) {
            // Launch is from the same task, (a top or privileged UID), or is directly privileged.
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[ASM] no clear top needed. "
                        + "sameTask: " + ((launchFlags & FLAG_ACTIVITY_NEW_TASK)
                        != FLAG_ACTIVITY_NEW_TASK) + "; "
                        + "verdict: " + balVerdict);
            }
            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)) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[ASM] no clear top needed. "
                        + "targetTaskTop: " + targetTaskTop);
            }
            return;
        }

        // Find the first activity which matches a safe UID and is not finishing. Clear everything
        // above it
        int[] finishCount = new int[1];
        boolean shouldBlockActivityStart = shouldRestrictActivitySwitch(callingUid);
        BlockActivityStart bas = checkCrossUidActivitySwitchFromBelow(
                targetTaskTop, callingUid, new BlockActivityStart());
        if (shouldBlockActivityStart && bas.mTopActivityOptedIn) {
            ActivityRecord activity = targetTask.getActivity(isLaunchingOrLaunched);
            if (activity == null) {
                // mStartActivity is not in task, so clear everything
                activity = targetRecord;
            }

            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 (shouldShowToast(callingUid)
                && (!shouldBlockActivityStart || finishCount[0] > 0)) {
            showToast((shouldBlockActivityStart
                    ? "Top activities cleared by "
                    : "Top activities would be cleared by ")
                    + DOC_LINK);

            Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord,
                    targetTask, targetTaskTop, realCallingUid, balVerdict, shouldBlockActivityStart,
                    /* taskToFront */ true, /* avoidMoveTaskToFront */ false,
                    /* allowedByAsmGracePeriod */ false, bas.mActivityOptedIn));
        }
    }

    /**
     * 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, int callingPid,
            @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;
        }

        String packageName =  getService().mContext.getPackageManager().getNameForUid(callingUid);
        BalState state = new BalState(callingUid, callingPid, packageName, INVALID_UID,
                INVALID_PID, null, null, false, null, null, ActivityOptions.makeBasic());
        BalVerdict verdict = evaluateChain(state, false,
                mCheckCallerIsAllowlistedUid,
                mCheckCallerIsAllowlistedComponent,
                mCheckCallerHasBackgroundPermission,
                mCheckCallerHasSawPermission,
                mCheckCallerHasBgStartAppOp,
                mCheckCallerVisible,
                mCheckCallerNonAppVisible,
                mCheckCallerProcessAllowsForeground);
        if (verdict.allows()) {
            return;
        }

        TaskDisplayArea displayArea = task.getTaskDisplayArea();
        if (displayArea == null) {
            // If there is no associated display area, we can not return home.
            return;
        }

        BlockActivityStart bas = checkTopActivityForAsm(task, callingUid, null,
                new BlockActivityStart());
        if (bas.mTopActivityMatchesSource) {
            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 */
                ASM_VERSION,
                /* multi_window */
                false,
                /* bal_code */
                -1,
                /* debug_info */
                null
        );

        boolean restrictActivitySwitch = shouldRestrictActivitySwitch(callingUid)
                && bas.mTopActivityOptedIn;

        PackageManager pm = getService().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 (shouldShowToast(callingUid)) {
            showToast((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'
     * <p>
     */
    private BlockActivityStart checkTopActivityForAsm(@NonNull Task task,
            int uid, @Nullable ActivityRecord sourceRecord, BlockActivityStart bas) {
        // If the source is visible, consider it 'top'.
        if (sourceRecord != null && sourceRecord.isVisibleRequested()) {
            return bas.matchesSource();
        }

        // Always allow actual top activity
        ActivityRecord topActivity = task.getTopMostActivity();
        if (topActivity == null) {
            Slog.wtf(TAG, "Activities for task: " + task + " not found.");
            return bas.optedIn(topActivity);
        }

        bas = checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas);
        if (bas.mTopActivityMatchesSource) {
            return bas;
        }

        // If UID is visible in target task, allow launch
        if (task.forAllActivities((Predicate<ActivityRecord>)
                ar -> ar.isUid(uid) && ar.isVisibleRequested())) {
            return bas.matchesSource();
        }

        // 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 bas;
        }

        bas = checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas);
        if (bas.mTopActivityMatchesSource) {
            return bas;
        }

        // 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 bas;
        }

        if (!taskFragment.hasAdjacentTaskFragment()) {
            return bas;
        }

        // Check the adjacent fragment.
        final BlockActivityStart[] out = { bas };
        taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> {
            final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate);
            if (top != null) {
                out[0] = checkCrossUidActivitySwitchFromBelow(top, uid, out[0]);
            }
        });
        return out[0];
    }

    /**
     * Determines if a source is allowed to add or remove activities from the task,
     * if the current ActivityRecord is above it in the stack
     * <p>
     * A transition is blocked 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. The current activity target V+
     * 3. The current app has set
     * {@link R.styleable#AndroidManifestApplication_allowCrossUidActivitySwitchFromBelow}
     * to {@code false}
     * 4. The current activity has not set
     * {@link ActivityRecord#setAllowCrossUidActivitySwitchFromBelow(boolean)} to {@code true}
     * </pre>
     *
     *
     * @param sourceUid The source (s) activity performing the state change
     */
    private BlockActivityStart checkCrossUidActivitySwitchFromBelow(ActivityRecord ar,
            int sourceUid, BlockActivityStart bas) {
        if (ar.isUid(sourceUid)) {
            return bas.matchesSource();
        }

        // We don't need to check package level if activity has opted out.
        if (ar.mAllowCrossUidActivitySwitchFromBelow) {
            bas.mTopActivityOptedIn = false;
            return bas.matchesSource();
        }

        if (ar.isUid(SYSTEM_UID)) {
            if (asmOptSystemIntoEnforcement()) {
                return bas.optedIn(ar);
            } else {
                return bas;
            }
        }

        if (!CompatChanges.isChangeEnabled(ASM_RESTRICTIONS, ar.getUid())) {
            return bas;
        }


        String packageName = ar.packageName;
        if (packageName == null) {
            Slog.wtf(TAG, "Package name: " + ar + " not found.");
            return bas.optedIn(ar);
        }

        PackageManager pm = getService().mContext.getPackageManager();
        ApplicationInfo applicationInfo;

        final int sourceUserId = UserHandle.getUserId(sourceUid);
        try {
            applicationInfo = pm.getApplicationInfoAsUser(packageName, /* flags= */ 0,
                    sourceUserId);
        } catch (PackageManager.NameNotFoundException e) {
            Slog.wtf(TAG, "Package name: " + packageName + " not found for user "
                    + sourceUserId);
            return bas.optedIn(ar);
        }

        return applicationInfo.allowCrossUidActivitySwitchFromBelow ? bas : bas.optedIn(ar);
    }

    /**
     * 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, BalVerdict balVerdict,
            boolean enforceBlock, boolean taskToFront,
            boolean avoidMoveTaskToFront, boolean allowedByGracePeriod,
            ActivityRecord activityOptedIn) {
        final String prefix = "[ASM] ";
        Function<ActivityRecord, String> recordToString = (ar) -> {
            if (ar == null) {
                return null;
            }

            return (ar == sourceRecord ?        " [source]=> "
                    : ar == targetTopActivity ? " [ top  ]=> "
                    : ar == targetRecord ?      " [target]=> "
                    :                           "         => ")
                    + getDebugStringForActivityRecord(ar);
        };

        StringJoiner joiner = new StringJoiner("\n");
        joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------");
        joiner.add(prefix + "Block Enabled: " + enforceBlock);
        if (!enforceBlock) {
            joiner.add(prefix + "Restrictions Enabled: " + android.security
                    .Flags.asmRestrictionsV2());
        }
        joiner.add(prefix + "ASM Version: " + ASM_VERSION);
        joiner.add(prefix + "System Time: " + SystemClock.uptimeMillis());
        joiner.add(prefix + "Activity Opted In: " + recordToString.apply(activityOptedIn));

        boolean targetTaskMatchesSourceTask = targetTask != null
                && sourceRecord != null && sourceRecord.getTask() == targetTask;

        if (sourceRecord == null) {
            joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage);
            String realCallingPackage = getService().mContext.getPackageManager().getNameForUid(
                    realCallingUid);
            joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
        } else {
            joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord));
            joiner.add(prefix + "Source Launch Package: " + sourceRecord.launchedFromPackage);
            joiner.add(prefix + "Source Launch Intent: " + sourceRecord.intent);
            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 + "AvoidMoveToFront: " + avoidMoveTaskToFront);
        joiner.add(prefix + "BalCode: " + balVerdict);
        joiner.add(prefix + "BalVerdict.isAllowlistedUid: " + balVerdict.isAllowlistedUid());
        joiner.add(prefix + "BalVerdict.isInGracePeriod: " + balVerdict.isInGracePeriod());
        joiner.add(prefix + "BalVerdict.isBasedOnRealCaller: " + balVerdict.isBasedOnRealCaller());
        joiner.add(
                prefix + "BalVerdict.isVisibleOrForeground: " + balVerdict.isVisibleOrForeground());
        if (balVerdict.mAdditionalBalCodes != null && balVerdict.mAdditionalBalCodes.size() > 0) {
            joiner.add(prefix + "BalVerdict.additionalBalCodes: " + balVerdict.mAdditionalBalCodes);
        }
        joiner.add(prefix + "BalVerdict.allowsNewTask: " + balVerdict.allowsNewTask());
        joiner.add(prefix + "Allowed By Grace Period: " + allowedByGracePeriod);
        joiner.add(prefix + "LastResumedActivity: "
                       + recordToString.apply(getService().mLastResumedActivity));
        joiner.add(prefix + "System opted into enforcement: " + asmOptSystemIntoEnforcement());

        if (mTopFinishedActivity != null) {
            joiner.add(prefix + "TopFinishedActivity: " + mTopFinishedActivity.mDebugInfo);
        }

        if (!mTaskIdToFinishedActivity.isEmpty()) {
            joiner.add(prefix + "TaskIdToFinishedActivity: ");
            mTaskIdToFinishedActivity.values().forEach(
                    (fae) -> joiner.add(prefix + "  " + fae.mDebugInfo));
        }

        if (balVerdict.isVisibleOrForeground()) {
            Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask;
            if (task != null && task.getDisplayArea() != null) {
                joiner.add(prefix + "Tasks: ");
                task.getDisplayArea().forAllTasks((Consumer<Task>)
                        t -> joiner.add(prefix + "   T: " + t.toFullString()));
            }
        }

        joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------");
        return joiner.toString();
    }

    private boolean allowedByAsmGracePeriod(int callingUid, @Nullable ActivityRecord sourceRecord,
            @Nullable Task targetTask, BalVerdict balVerdict, boolean taskToFront,
            boolean avoidMoveTaskToFront) {
        if (balVerdict.isBasedOnCaller() && balVerdict.isInGracePeriod()) {
            // Allow if launching into new task, and caller matches most recently finished activity
            if (taskToFront && mTopFinishedActivity != null
                    && mTopFinishedActivity.mUid == callingUid) {
                return true;
            }

            // Launching into existing task - allow if matches most recently finished activity
            // within the task.
            // We can reach here multiple ways:
            // 1. activity in fg fires intent (taskToFront = false, sourceRecord is available)
            // 2. activity in bg fires intent (taskToFront = false, sourceRecord is available)
            // 3. activity in bg fires intent with NEW_FLAG (taskToFront = true,
            //         avoidMoveTaskToFront = true, sourceRecord is available)
            // 4. activity in bg fires PI (taskToFront = true, avoidMoveTaskToFront = true,
            //         sourceRecord is not available, targetTask may be available)
            if (!taskToFront || avoidMoveTaskToFront) {
                if (targetTask != null) {
                    FinishedActivityEntry finishedEntry =
                            mTaskIdToFinishedActivity.get(targetTask.mTaskId);
                    if (finishedEntry != null && finishedEntry.mUid == callingUid) {
                        return true;
                    }
                }

                if (sourceRecord != null) {
                    FinishedActivityEntry finishedEntry =
                            mTaskIdToFinishedActivity.get(sourceRecord.getTask().mTaskId);
                    return finishedEntry != null && finishedEntry.mUid == callingUid;
                }
            }
        }
        return false;
    }

    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() && getService().isActivityStartsLoggingEnabled()) {
            // log aborted activity start to TRON
            mSupervisor
                    .getActivityMetricsLogger()
                    .logAbortedBgActivityStart(
                            state.mIntent,
                            state.mCallerApp,
                            state.mCallingUid,
                            state.mCallingPackage,
                            state.mCallingUidProcState,
                            state.mCallingUidHasVisibleActivity
                                    || state.mCallingUidHasNonAppVisibleWindow,
                            state.mRealCallingUid,
                            state.mRealCallingUidProcState,
                            state.mRealCallingUidHasVisibleActivity
                                    || state.mRealCallingUidHasNonAppVisibleWindow,
                            (state.mOriginatingPendingIntent != null));
        }

        if (logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_GRACE_PERIOD)) {
            String abortDebugMessage = "Activity start is only allowed by grace period. "
                    + "This may stop working in the future. "
                    + "intent: " + state.mIntent;
            strictModeLaunchAborted(state.mRealCallingUid, abortDebugMessage);
        }
        logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_NON_APP_VISIBLE_WINDOW);
        logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_NOTIFICATION_TOKEN);
        logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_WALLPAPER);

        if (shouldLogStats(finalVerdict, state)) {
            String activityName;
            if (shouldLogIntentActivity(finalVerdict, state)) {
                Intent intent = state.mIntent;
                activityName = intent == null ? "noIntent" // should never happen
                        : requireNonNull(intent.getComponent()).flattenToShortString();
            } else {
                activityName = "";
            }
            writeBalAllowedLog(activityName, finalVerdict.getCode(), state);
        } else {
            writeBalAllowedLogMinimal(state, finalVerdict.getCode());
        }
        return finalVerdict;
    }

    /**
     * Logs details about the activity starts if the only reason it is allowed is the provided
     * {@code balCode}.
     */
    private static boolean logIfOnlyAllowedBy(BalVerdict finalVerdict, BalState state,
            int balCode) {
        if (finalVerdict.getCode() == balCode) {
            if (state.realCallerExplicitOptInOrAutoOptIn()
                    && state.mResultForRealCaller != null
                    && state.mResultForRealCaller.allows()
                    && state.mResultForRealCaller.getCode() != balCode) {
                // real caller could allow with a different exemption
                return false;
            } else if (state.callerExplicitOptInOrAutoOptIn()
                    && state.mResultForCaller != null
                    && state.mResultForCaller.allows()
                    && state.mResultForCaller.getCode() != balCode) {
                // caller could allow with a different exemption
                return false;
            } else {
                // log to determine grace period length distribution
                if (Build.IS_DEBUGGABLE) {
                    Slog.wtf(TAG, "Activity start ONLY allowed by " + balCodeToString(balCode) + " "
                            + finalVerdict.mMessage + ": " + state);
                }
                return true;
            }
        }
        return false;
    }

    @VisibleForTesting
    boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
        if (finalVerdict.getCode() == BAL_ALLOW_VISIBLE_WINDOW) {
            if (!state.isPendingIntent()) {
                // regular activity start by visible app
                return false;
            }
            if (finalVerdict.isBasedOnRealCaller()) {
                // PendingIntent started by visible app
                return false;
            }
        }
        return true;
    }

    @VisibleForTesting
    boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
        return finalVerdict.isBasedOnRealCaller()
                ? state.mRealCallingUid < Process.FIRST_APPLICATION_UID
                : state.mCallingUid < Process.FIRST_APPLICATION_UID;
    }

    @VisibleForTesting void writeBalAllowedLog(String activityName, int code, BalState state) {
        FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
                activityName,
                code,
                state.mCallingUid,
                state.mRealCallingUid,
                state.mResultForCaller == null ? BAL_BLOCK : state.mResultForCaller.getCode(),
                state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts(),
                state.callerExplicitOptInOrOut(),
                state.mResultForRealCaller == null ? BAL_BLOCK
                        : state.mResultForRealCaller.getCode(),
                state.mBalAllowedByPiSender.allowsBackgroundActivityStarts(),
                state.realCallerExplicitOptInOrOut(),
                getTargetSdk(state.mCallingPackage),
                getTargetSdk(state.mRealCallingPackage)
        );
    }

    @VisibleForTesting void writeBalAllowedLogMinimal(BalState state, int code) {
        FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
                "",
                BAL_ALLOW_DEFAULT,
                NO_PROCESS_UID,
                NO_PROCESS_UID,
                state.mResultForCaller == null ? BAL_BLOCK : state.mResultForCaller.getCode(),
                state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts(),
                state.callerExplicitOptInOrOut(),
                state.mResultForRealCaller == null ? BAL_BLOCK
                        : state.mResultForRealCaller.getCode(),
                state.mBalAllowedByPiSender.allowsBackgroundActivityStarts(),
                state.realCallerExplicitOptInOrOut(),
                getTargetSdk(state.mCallingPackage),
                getTargetSdk(state.mRealCallingPackage)
        );
    }

    /**
     * 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.isVisibleRequested()
                && 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);
        }
    }

    private static class BlockActivityStart {
        private boolean mTopActivityOptedIn;
        private boolean mTopActivityMatchesSource;
        private ActivityRecord mActivityOptedIn;

        BlockActivityStart optedIn(ActivityRecord activity) {
            mTopActivityOptedIn = true;
            if (mActivityOptedIn == null) {
                mActivityOptedIn = activity;
            }
            return this;
        }

        BlockActivityStart matchesSource() {
            mTopActivityMatchesSource = true;
            return this;
        }
    }

    private static String getDebugStringForActivityRecord(ActivityRecord ar) {
        return ar
                + " :: visible=" + ar.isVisible()
                + ", visibleRequested=" + ar.isVisibleRequested()
                + ", finishing=" + ar.finishing
                + ", alwaysOnTop=" + ar.isAlwaysOnTop()
                + ", lastLaunchTime=" + ar.lastLaunchTime
                + ", lastVisibleTime=" + ar.lastVisibleTime
                + ", taskFragment=" + ar.getTaskFragment();
    }

    /** @return valid targetSdk or <code>-1</code> */
    private int getTargetSdk(String packageName) {
        if (packageName == null) {
            return -1;
        }
        try {
            PackageManager pm = getService().mContext.getPackageManager();
            return pm.getTargetSdkVersion(packageName);
        } catch (Exception e) {
            return -1;
        }
    }

    private class FinishedActivityEntry {
        int mUid;
        int mTaskId;
        int mLaunchCount;
        String mDebugInfo;

        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;
            this.mDebugInfo = getDebugStringForActivityRecord(ar);

            getService().mH.postDelayed(() -> {
                synchronized (getService().mGlobalLock) {
                    if (mTaskIdToFinishedActivity.get(taskId) == this) {
                        mTaskIdToFinishedActivity.remove(taskId);
                    }

                    if (mTopFinishedActivity == this) {
                        mTopFinishedActivity = null;
                    }
                }
            }, ASM_GRACEPERIOD_TIMEOUT_MS);
        }
    }

    static boolean shouldShowToast(int uid) {
        return android.security.Flags.asmToastsEnabled();
    }

    static boolean shouldRestrictActivitySwitch(int uid) {
        return android.security.Flags.asmRestrictionsV2()
                && CompatChanges.isChangeEnabled(ASM_RESTRICTIONS, uid);
    }
}
