/*
 * Copyright (C) 2015 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.car.pm;

import static android.Manifest.permission.QUERY_ALL_PACKAGES;
import static android.car.Car.PERMISSION_MANAGE_DISPLAY_COMPATIBILITY;
import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME;
import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID;
import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_DISPLAY_ID;
import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO;
import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;

import static com.android.car.CarServiceUtils.checkCalledByPackage;
import static com.android.car.CarServiceUtils.getHandlerThread;
import static com.android.car.CarServiceUtils.isEventOfType;
import static com.android.car.CarServiceUtils.releaseHandlerThread;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.PendingIntent;
import android.app.TaskInfo;
import android.car.Car;
import android.car.CarOccupantZoneManager;
import android.car.CarVersion;
import android.car.ICarOccupantZoneCallback;
import android.car.builtin.app.ActivityManagerHelper;
import android.car.builtin.app.TaskInfoHelper;
import android.car.builtin.content.pm.PackageManagerHelper;
import android.car.builtin.os.BuildHelper;
import android.car.builtin.os.ServiceManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.content.pm.AppBlockingPackageInfo;
import android.car.content.pm.CarAppBlockingPolicy;
import android.car.content.pm.CarAppBlockingPolicyService;
import android.car.content.pm.CarPackageManager;
import android.car.content.pm.ICarBlockingUiCommandListener;
import android.car.content.pm.ICarPackageManager;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.ICarUxRestrictionsChangeListener;
import android.car.hardware.power.CarPowerPolicy;
import android.car.hardware.power.CarPowerPolicyFilter;
import android.car.hardware.power.ICarPowerPolicyListener;
import android.car.hardware.power.PowerComponent;
import android.car.user.CarUserManager.UserLifecycleListener;
import android.car.user.UserLifecycleEventFilter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.Signature;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.SparseLongArray;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
import android.view.accessibility.AccessibilityEvent;

import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.CarOccupantZoneService;
import com.android.car.CarServiceBase;
import com.android.car.CarServiceHelperWrapper;
import com.android.car.CarUxRestrictionsManagerService;
import com.android.car.R;
import com.android.car.am.CarActivityService;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.DebugUtils;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.car.internal.util.LocalLog;
import com.android.car.internal.util.Sets;
import com.android.car.power.CarPowerManagementService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;

/**
 * Package manager service for cars.
 */
public final class CarPackageManagerService extends ICarPackageManager.Stub
        implements CarServiceBase {

    @VisibleForTesting
    static final String TAG = CarLog.tagFor(CarPackageManagerService.class);

    static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);

    // Delimiters to parse packages and activities in the configuration XML resource.
    private static final String PACKAGE_DELIMITER = ",";
    private static final String PACKAGE_ACTIVITY_DELIMITER = "/";
    private static final int LOG_SIZE = 20;
    private static final String[] WINDOW_DUMP_ARGUMENTS = new String[]{"windows"};
    private static final String HANDLER_THREAD_NAME = CarPackageManagerService.class
            .getSimpleName();

    private static final String PROPERTY_RO_DRIVING_SAFETY_REGION =
            "ro.android.car.drivingsafetyregion";
    private static final int ABA_LAUNCH_TIMEOUT_MS = 1_000;

    private final Context mContext;
    private final CarActivityService mActivityService;
    private final PackageManager mPackageManager;
    private final ActivityManager mActivityManager;
    private final IBinder mWindowManagerBinder;

    private final HandlerThread mHandlerThread = getHandlerThread(HANDLER_THREAD_NAME);
    private final PackageHandler mHandler = new PackageHandler(mHandlerThread.getLooper(), this);
    private final Object mLock = new Object();

    // For dumpsys logging.
    private final LocalLog mBlockedActivityLogs = new LocalLog(LOG_SIZE);
    private final BlockingUiCommandListenerMediator mBlockingUiCommandListenerMediator;

    // Store the allowlist and blocklist strings from the resource file.
    private String mConfiguredAllowlist;
    private String mConfiguredSystemAllowlist;
    private String mConfiguredBlocklist;
    @GuardedBy("mLock")
    private Map<String, Set<String>> mConfiguredAllowlistMap;
    @GuardedBy("mLock")
    private Map<String, Set<String>> mConfiguredBlocklistMap;

    private final List<String> mAllowedAppInstallSources;

    @GuardedBy("mLock")
    private final SparseArray<ComponentName> mTopActivityWithDialogPerDisplay = new SparseArray<>();

    /**
     * Hold policy set from policy service or client.
     * Key: packageName of policy service
     */
    @GuardedBy("mLock")
    private final HashMap<String, ClientPolicy> mClientPolicies = new HashMap<>();
    @GuardedBy("mLock")
    private HashMap<String, AppBlockingPackageInfoWrapper> mActivityAllowlistMap = new HashMap<>();
    @GuardedBy("mLock")
    private  HashSet<String> mActivityDenylistPackages = new HashSet<String>();

    @GuardedBy("mLock")
    private LinkedList<AppBlockingPolicyProxy> mProxies;

    @GuardedBy("mLock")
    private final LinkedList<CarAppBlockingPolicy> mWaitingPolicies = new LinkedList<>();

    @GuardedBy("mLock")
    private String mCurrentDrivingSafetyRegion = CarPackageManager.DRIVING_SAFETY_REGION_ALL;
    // Package name + '/' + className format
    @GuardedBy("mLock")
    private final ArraySet<String> mTempAllowedActivities = new ArraySet<>();

    private final CarUxRestrictionsManagerService mCarUxRestrictionsService;
    private final CarOccupantZoneService mCarOccupantZoneService;
    private final boolean mEnableActivityBlocking;

    private final ComponentName mActivityBlockingActivity;
    // Memorize the target of ABA to defend bypassing it with launching two Activities continuously.
    private final SparseArray<TaskInfo> mBlockingActivityTargets = new SparseArray<>();
    private final SparseLongArray mBlockingActivityLaunchTimes = new SparseLongArray();
    private final boolean mPreventTemplatedAppsFromShowingDialog;
    private final String mTemplateActivityClassName;

    private final ActivityListener mActivityListener = new ActivityListener();

    // K: (logical) display id of a physical display, V: UXR change listener of this display.
    // For multi-display, monitor UXR change on each display.
    @GuardedBy("mLock")
    private final SparseArray<UxRestrictionsListener> mUxRestrictionsListeners =
            new SparseArray<>();
    // K: (logical) display id of a display, V: Task info of the blocking ui of this display.
    // For multi-display, monitor blocking ui task information for each display.
    @GuardedBy("mLock")
    private final SparseArray<TaskInfo> mBlockingUiTaskInfoPerDisplay = new SparseArray<>();
    private final VendorServiceController mVendorServiceController;

    // Information related to when the installed packages should be parsed for building a allow and
    // block list
    private final Set<String> mPackageManagerActions = Sets.newArraySet(
            Intent.ACTION_PACKAGE_ADDED,
            Intent.ACTION_PACKAGE_CHANGED,
            Intent.ACTION_PACKAGE_REMOVED,
            Intent.ACTION_PACKAGE_REPLACED);

    private final PackageParsingEventReceiver mPackageParsingEventReceiver =
            new PackageParsingEventReceiver();
    /**
     * Mapping between the task ID and the last known display ID.
     */
    @GuardedBy("mLock")
    private final SparseIntArray mLastKnownDisplayIdForTask = new SparseIntArray();

    private final UserLifecycleListener mUserLifecycleListener = event -> {
        if (!isEventOfType(TAG, event, USER_LIFECYCLE_EVENT_TYPE_SWITCHING)) {
            return;
        }

        synchronized (mLock) {
            resetTempAllowedActivitiesLocked();
        }
    };

    private final ICarPowerPolicyListener mDisplayPowerPolicyListener =
            new ICarPowerPolicyListener.Stub() {
                @Override
                public void onPolicyChanged(CarPowerPolicy policy,
                        CarPowerPolicy accumulatedPolicy) {
                    if (!policy.isComponentEnabled(PowerComponent.DISPLAY)) {
                        synchronized (mLock) {
                            resetTempAllowedActivitiesLocked();
                        }
                    }
                }
            };

    public CarPackageManagerService(Context context,
            CarUxRestrictionsManagerService uxRestrictionsService,
            CarActivityService activityService, CarOccupantZoneService carOccupantZoneService) {
        mContext = context;
        mCarUxRestrictionsService = uxRestrictionsService;
        mActivityService = activityService;
        mCarOccupantZoneService = carOccupantZoneService;
        mPackageManager = mContext.getPackageManager();
        mActivityManager = mContext.getSystemService(ActivityManager.class);
        mWindowManagerBinder = ServiceManagerHelper.getService(Context.WINDOW_SERVICE);
        Resources res = context.getResources();
        mEnableActivityBlocking = res.getBoolean(R.bool.enableActivityBlockingForSafety);
        String blockingActivity = res.getString(R.string.activityBlockingActivity);
        mActivityBlockingActivity = ComponentName.unflattenFromString(blockingActivity);
        if (mEnableActivityBlocking && mActivityBlockingActivity == null) {
            Slogf.wtf(TAG, "mActivityBlockingActivity can't be null when enabled");
        }
        mAllowedAppInstallSources = Arrays.asList(
                res.getStringArray(R.array.allowedAppInstallSources));
        mVendorServiceController = new VendorServiceController(
                mContext, mHandler.getLooper());
        mPreventTemplatedAppsFromShowingDialog =
                res.getBoolean(R.bool.config_preventTemplatedAppsFromShowingDialog);
        mTemplateActivityClassName = res.getString(R.string.config_template_activity_class_name);
        mBlockingUiCommandListenerMediator = new BlockingUiCommandListenerMediator();
    }

    @Override
    public void destroy() {
        try {
            releaseHandlerThread(HANDLER_THREAD_NAME);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public void setAppBlockingPolicy(String packageName, CarAppBlockingPolicy policy, int flags) {
        if (DBG) {
            Slogf.d(TAG, "policy setting from binder call, client:" + packageName);
        }
        doSetAppBlockingPolicy(packageName, policy, flags);
    }

    /**
     * Restarts the requested task. If task with {@code taskId} does not exist, do nothing.
     */
    @Override
    public void restartTask(int taskId) {
        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.REAL_GET_TASKS)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException(
                    "requires permission " + android.Manifest.permission.REAL_GET_TASKS);
        }
        mActivityService.restartTask(taskId);
    }

    @Override
    public String getCurrentDrivingSafetyRegion() {
        assertAppBlockingOrDrivingStatePermission();
        synchronized (mLock) {
            return mCurrentDrivingSafetyRegion;
        }
    }

    private String getComponentNameString(String packageName, String className) {
        return packageName + '/' + className;
    }

    @Override
    public void controlOneTimeActivityBlockingBypassingAsUser(String packageName,
            String activityClassName, boolean bypass, @UserIdInt int userId) {
        assertAppBlockingPermission();
        if (!callerCanQueryPackage(packageName)) {
            throw new SecurityException("cannot query other package");
        }
        try {
            // Read this to check the validity of pkg / activity name. Not checking this can allow
            // bad apps to be allowed later.
            CarAppMetadataReader.getSupportedDrivingSafetyRegionsForActivityAsUser(mContext,
                    packageName, activityClassName, userId);
        } catch (NameNotFoundException e) {
            throw new ServiceSpecificException(CarPackageManager.ERROR_CODE_NO_PACKAGE,
                    e.getMessage());
        }
        String componentName = getComponentNameString(packageName, activityClassName);
        synchronized (mLock) {
            if (bypass) {
                mTempAllowedActivities.add(componentName);
            } else {
                mTempAllowedActivities.remove(componentName);
            }
        }
        if (!bypass) { // block top activities if bypassing is disabled.
            mHandler.post(this::blockTopActivitiesOnAllDisplaysIfNecessary);
        }
    }

    @GuardedBy("mLock")
    private void resetTempAllowedActivitiesLocked() {
        mTempAllowedActivities.clear();
    }

    @Override
    public List<String> getSupportedDrivingSafetyRegionsForActivityAsUser(String packageName,
            String activityClassName, @UserIdInt int userId) {
        assertAppBlockingOrDrivingStatePermission();
        if (!callerCanQueryPackage(packageName)) {
            throw new SecurityException("cannot query other package");
        }
        long token = Binder.clearCallingIdentity();
        try {
            return CarAppMetadataReader.getSupportedDrivingSafetyRegionsForActivityAsUser(mContext,
                    packageName, activityClassName, userId);
        } catch (NameNotFoundException e) {
            throw new ServiceSpecificException(CarPackageManager.ERROR_CODE_NO_PACKAGE,
                    e.getMessage());
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private void assertAppBlockingOrDrivingStatePermission() {
        if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CONTROL_APP_BLOCKING)
                != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission(
                Car.PERMISSION_CAR_DRIVING_STATE) != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException(
                    "requires permission " + Car.PERMISSION_CONTROL_APP_BLOCKING + " or "
                            + Car.PERMISSION_CAR_DRIVING_STATE);
        }
    }

    private void assertAppBlockingPermission() {
        if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CONTROL_APP_BLOCKING)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException(
                    "requires permission " + Car.PERMISSION_CONTROL_APP_BLOCKING);
        }
    }

    private void doSetAppBlockingPolicy(String packageName, CarAppBlockingPolicy policy,
            int flags) {
        assertAppBlockingPermission();
        checkCalledByPackage(mContext, packageName);
        if (policy == null) {
            throw new IllegalArgumentException("policy cannot be null");
        }
        if ((flags & CarPackageManager.FLAG_SET_POLICY_ADD) != 0 &&
                (flags & CarPackageManager.FLAG_SET_POLICY_REMOVE) != 0) {
            throw new IllegalArgumentException(
                    "Cannot set both FLAG_SET_POLICY_ADD and FLAG_SET_POLICY_REMOVE flag");
        }
        synchronized (mLock) {
            if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
                mWaitingPolicies.add(policy);
            }
        }
        mHandler.requestUpdatingPolicy(packageName, policy, flags);
        if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
            synchronized (mLock) {
                try {
                    while (mWaitingPolicies.contains(policy)) {
                        mLock.wait();
                    }
                } catch (InterruptedException e) {
                    // Pass it over binder call
                    throw new IllegalStateException(
                            "Interrupted while waiting for policy completion", e);
                }
            }
        }
    }

    @Override
    public boolean isActivityDistractionOptimized(String packageName, String className) {
        if (!callerCanQueryPackage(packageName)) return false;
        assertPackageAndClassName(packageName, className);
        synchronized (mLock) {
            if (DBG) {
                Slogf.d(TAG, "isActivityDistractionOptimized" + dumpPoliciesLocked(false));
            }

            if (mTempAllowedActivities.contains(getComponentNameString(packageName,
                    className))) {
                return true;
            }

            for (int i = mTopActivityWithDialogPerDisplay.size() - 1; i >= 0; i--) {
                ComponentName activityWithDialog = mTopActivityWithDialogPerDisplay.get(
                        mTopActivityWithDialogPerDisplay.keyAt(i));
                if (activityWithDialog.getClassName().equals(className)
                        && activityWithDialog.getPackageName().equals(packageName)) {
                    return false;
                }
            }

            if (searchFromClientPolicyBlocklistsLocked(packageName)) {
                return false;
            }

            if (isActivityInClientPolicyAllowlistsLocked(packageName, className)) {
                return true;
            }

            // Check deny and allow list
            boolean packageBlocked = mActivityDenylistPackages.contains(packageName);
            AppBlockingPackageInfoWrapper infoWrapper = mActivityAllowlistMap.get(packageName);
            if (!packageBlocked && infoWrapper == null) {
                // Update cache
                updateActivityAllowlistAndDenylistMap(packageName);
                packageBlocked = mActivityDenylistPackages.contains(packageName);
                infoWrapper = mActivityAllowlistMap.get(packageName);
            }

            return !packageBlocked
                    && isActivityInMapAndMatching(infoWrapper, packageName, className);
        }
    }

    @VisibleForTesting
    boolean callerCanQueryPackage(String packageName) {
        int callingUid = Binder.getCallingUid();
        if (hasPermissionGranted(QUERY_ALL_PACKAGES, callingUid)) {
            return true;
        }
        String[] packages = mPackageManager.getPackagesForUid(callingUid);
        if (packages != null && packages.length > 0) {
            for (int i = 0; i < packages.length; i++) {
                if (Objects.equals(packageName, packages[i])) {
                    return true;
                }
            }
        }

        Slogf.w(TAG, QUERY_ALL_PACKAGES + " permission is needed to query other packages.");

        return false;
    }

    private static boolean hasPermissionGranted(String permission, int uid) {
        return ActivityManagerHelper.checkComponentPermission(permission, uid,
                /* owningUid= */ Process.INVALID_UID,
                /* exported= */ true) == PackageManager.PERMISSION_GRANTED;
    }

    @Override
    public boolean isPendingIntentDistractionOptimized(PendingIntent pendingIntent) {
        if (!pendingIntent.isActivity()) {
            Slogf.d(TAG, "isPendingIntentDistractionOptimized: Activity not set on the "
                    + "pending intent.");
            return false;
        }
        List<ResolveInfo> infos = pendingIntent.queryIntentComponents(
                PackageManager.MATCH_DEFAULT_ONLY);
        if (infos.isEmpty()) {
            Slogf.d(TAG, "isPendingIntentDistractionOptimized: No intent component found for "
                    + "the pending intent.");
            return false;
        }
        if (infos.size() > 1) {
            Slogf.d(TAG, "isPendingIntentDistractionOptimized: More than one intent component"
                    + " found for the pending intent. Considering the first one.");
        }
        ActivityInfo activityInfo = infos.get(0).activityInfo;
        return isActivityDistractionOptimized(activityInfo.packageName, activityInfo.name);
    }

    @Override
    public boolean isServiceDistractionOptimized(String packageName, String className) {
        if (!callerCanQueryPackage(packageName)) return false;

        if (packageName == null) {
            throw new IllegalArgumentException("Package name null");
        }
        synchronized (mLock) {
            if (DBG) {
                Slogf.d(TAG, "isServiceDistractionOptimized" + dumpPoliciesLocked(false));
            }

            if (searchFromClientPolicyBlocklistsLocked(packageName)) {
                return false;
            }

            if (searchFromClientPolicyAllowlistsLocked(packageName)) {
                return true;
            }

            // Check deny and allow list
            boolean packageBlocked = mActivityDenylistPackages.contains(packageName);
            AppBlockingPackageInfoWrapper infoWrapper = mActivityAllowlistMap.get(packageName);
            if (!packageBlocked && infoWrapper == null) {
                // Update cache
                updateActivityAllowlistAndDenylistMap(packageName);
                packageBlocked = mActivityDenylistPackages.contains(packageName);
                infoWrapper = mActivityAllowlistMap.get(packageName);
            }

            return !packageBlocked && infoWrapper != null && infoWrapper.info != null;
        }
    }

    @Override
    public boolean isActivityBackedBySafeActivity(ComponentName activityName) {
        if (activityName == null) return false;
        if (!callerCanQueryPackage(activityName.getPackageName())) return false;

        TaskInfo info = mActivityService.getTaskInfoForTopActivity(activityName);
        if (DBG) {
            Slogf.d(TAG, "isActivityBackedBySafeActivity: info=%s",
                    TaskInfoHelper.toString(info));
        }
        if (info == null) { // not top in focused stack
            return true;
        }
        if (!isUxRestrictedOnDisplay(TaskInfoHelper.getDisplayId(info))) {
            return true;
        }
        if (info.baseActivity == null
                || info.baseActivity.equals(activityName)) {  // nothing below this.
            return false;
        }
        return isActivityDistractionOptimized(info.baseActivity.getPackageName(),
                info.baseActivity.getClassName());
    }

    private void assertPackageAndClassName(String packageName, String className) {
        if (packageName == null) {
            throw new IllegalArgumentException("Package name null");
        }
        if (className == null) {
            throw new IllegalArgumentException("Class name null");
        }
    }

    @GuardedBy("mLock")
    private boolean searchFromClientPolicyBlocklistsLocked(String packageName) {
        for (ClientPolicy policy : mClientPolicies.values()) {
            AppBlockingPackageInfoWrapper wrapper = policy.mBlocklistsMap.get(packageName);
            if (wrapper != null && wrapper.isMatching && wrapper.info != null) {
                return true;
            }
        }

        return false;
    }

    @GuardedBy("mLock")
    private boolean searchFromClientPolicyAllowlistsLocked(String packageName) {
        for (ClientPolicy policy : mClientPolicies.values()) {
            AppBlockingPackageInfoWrapper wrapper = policy.mAllowlistsMap.get(packageName);
            if (wrapper != null && wrapper.isMatching && wrapper.info != null) {
                return true;
            }
        }
        return false;
    }

    @GuardedBy("mLock")
    private boolean isActivityInClientPolicyAllowlistsLocked(String packageName, String className) {
        for (ClientPolicy policy : mClientPolicies.values()) {
            if (isActivityInMapAndMatching(policy.mAllowlistsMap.get(packageName), packageName,
                    className)) {
                return true;
            }
        }
        return false;
    }

    private boolean isActivityInMapAndMatching(AppBlockingPackageInfoWrapper wrapper,
            String packageName, String className) {
        if (wrapper == null || !wrapper.isMatching) {
            if (DBG) {
                Slogf.d(TAG, "Pkg not in allowlist:" + packageName);
            }
            return false;
        }
        return wrapper.info.isActivityCovered(className);
    }

    @Override
    public void init() {
        String safetyRegion = SystemProperties.get(PROPERTY_RO_DRIVING_SAFETY_REGION, "");
        synchronized (mLock) {
            setDrivingSafetyRegionWithCheckLocked(safetyRegion);
            mHandler.requestInit();
        }
        UserLifecycleEventFilter userSwitchingEventFilter = new UserLifecycleEventFilter.Builder()
                .addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING).build();
        CarLocalServices.getService(CarUserService.class).addUserLifecycleListener(
                userSwitchingEventFilter, mUserLifecycleListener);
        CarLocalServices.getService(CarPowerManagementService.class).addPowerPolicyListener(
                new CarPowerPolicyFilter.Builder().setComponents(PowerComponent.DISPLAY).build(),
                mDisplayPowerPolicyListener);
    }

    @Override
    public void release() {
        CarLocalServices.getService(CarPowerManagementService.class).removePowerPolicyListener(
                mDisplayPowerPolicyListener);
        CarLocalServices.getService(CarUserService.class).removeUserLifecycleListener(
                mUserLifecycleListener);
        synchronized (mLock) {
            mHandler.requestRelease();
            // wait for release do be done. This guarantees that init is done.
            try {
                mLock.wait();
            } catch (InterruptedException e) {
                Slogf.e(TAG, "Interrupted wait during release");
                Thread.currentThread().interrupt();
            }
            mActivityAllowlistMap.clear();
            mActivityDenylistPackages.clear();
            mClientPolicies.clear();
            mBlockingUiTaskInfoPerDisplay.clear();
            if (mProxies != null) {
                for (AppBlockingPolicyProxy proxy : mProxies) {
                    proxy.disconnect();
                }
                mProxies.clear();
            }
            mWaitingPolicies.clear();
            resetTempAllowedActivitiesLocked();
            mLock.notifyAll();
        }
        mContext.unregisterReceiver(mPackageParsingEventReceiver);
        mActivityService.unregisterActivityListener(mActivityListener);
        synchronized (mLock) {
            for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
                UxRestrictionsListener listener = mUxRestrictionsListeners.valueAt(i);
                mCarUxRestrictionsService.unregisterUxRestrictionsChangeListener(listener);
            }
        }
    }

    @GuardedBy("mLock")
    private void setDrivingSafetyRegionWithCheckLocked(String region) {
        if (region.isEmpty()) {
            mCurrentDrivingSafetyRegion = CarPackageManager.DRIVING_SAFETY_REGION_ALL;
        } else {
            mCurrentDrivingSafetyRegion = region;
        }
    }

    /**
     * Reset driving stat and all dynamically added allow list so that region information for
     * all packages are reset. This also resets one time allow list.
     */
    public void resetDrivingSafetyRegion(@NonNull String region) {
        synchronized (mLock) {
            setDrivingSafetyRegionWithCheckLocked(region);
            resetTempAllowedActivitiesLocked();
            mActivityAllowlistMap.clear();
            mActivityDenylistPackages.clear();
        }
    }

    // run from HandlerThread
    private void doHandleInit() {
        startAppBlockingPolicies();
        IntentFilter pkgParseIntent = new IntentFilter();
        for (String action : mPackageManagerActions) {
            pkgParseIntent.addAction(action);
        }
        pkgParseIntent.addDataScheme("package");
        mContext.registerReceiverForAllUsers(mPackageParsingEventReceiver, pkgParseIntent,
                /* broadcastPermission= */ null, /* scheduler= */ null,
                Context.RECEIVER_NOT_EXPORTED);

        // Listen to UxR changes on all displays.
        List<CarOccupantZoneManager.OccupantZoneInfo> occupantZoneInfos =
                mCarOccupantZoneService.getAllOccupantZones();
        synchronized (mLock) {
            for (int i = 0; i < occupantZoneInfos.size(); i++) {
                CarOccupantZoneManager.OccupantZoneInfo occupantZoneInfo =
                        occupantZoneInfos.get(i);
                int zoneId = occupantZoneInfo.zoneId;
                int[] displayIds = mCarOccupantZoneService.getAllDisplaysForOccupantZone(zoneId);
                for (int j = 0; j < displayIds.length; j++) {
                    int displayId = displayIds[j];
                    UxRestrictionsListener listener = new UxRestrictionsListener(
                            mCarUxRestrictionsService, displayId);
                    mUxRestrictionsListeners.put(displayId, listener);
                    mCarUxRestrictionsService.registerUxRestrictionsChangeListener(
                            listener, displayId);
                }
            }
        }

        // Update UxR listeners upon display changes.
        mCarOccupantZoneService.registerCallback(mOccupantZoneCallback);

        mVendorServiceController.init();
        mActivityService.registerActivityListener(mActivityListener);
    }

    private final ICarOccupantZoneCallback mOccupantZoneCallback =
            new ICarOccupantZoneCallback.Stub() {
                @Override
                public void onOccupantZoneConfigChanged(int flags) throws RemoteException {
                    // Respond to display changes.
                    if ((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) == 0) {
                        return;
                    }
                    if (DBG) {
                        String flagString = DebugUtils.flagsToString(
                                CarOccupantZoneManager.class, "ZONE_CONFIG_CHANGE_FLAG_",
                                flags);
                        Slogf.d(TAG, "onOccupantZoneConfigChanged: display zone change flag=%s",
                                    flagString);
                    }
                    ArraySet<Integer> updatedDisplayIds = new ArraySet<>();
                    List<CarOccupantZoneManager.OccupantZoneInfo> occupantZoneInfos =
                            mCarOccupantZoneService.getAllOccupantZones();

                    synchronized (mLock) {
                        for (int i = 0; i < occupantZoneInfos.size(); i++) {
                            CarOccupantZoneManager.OccupantZoneInfo occupantZoneInfo =
                                    occupantZoneInfos.get(i);
                            int zoneId = occupantZoneInfo.zoneId;
                            int[] displayIds =
                                    mCarOccupantZoneService.getAllDisplaysForOccupantZone(zoneId);
                            for (int j = 0; j < displayIds.length; j++) {
                                int displayId = displayIds[j];
                                updatedDisplayIds.add(displayId);
                                // Register UxR listener for newly added displays.
                                if (mUxRestrictionsListeners.get(displayId) == null) {
                                    Slogf.d(TAG, "adding UxR listener for display %d", displayId);
                                    UxRestrictionsListener listener = new UxRestrictionsListener(
                                            mCarUxRestrictionsService, displayId);
                                    mUxRestrictionsListeners.put(displayId, listener);
                                    mCarUxRestrictionsService.registerUxRestrictionsChangeListener(
                                            listener, displayId);
                                }
                            }
                        }

                        // Remove UxR listener for removed displays.
                        for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
                            int displayId = mUxRestrictionsListeners.keyAt(i);
                            if (!updatedDisplayIds.contains(displayId)) {
                                UxRestrictionsListener listener =
                                        mUxRestrictionsListeners.valueAt(i);
                                mCarUxRestrictionsService.unregisterUxRestrictionsChangeListener(
                                        listener);
                                mUxRestrictionsListeners.remove(displayId);
                            }
                        }
                    }
                }
            };

    private void doParseInstalledPackage(String packageName) {
        // Delete the package from allowlist and denylist mapping
        synchronized (mLock) {
            mActivityDenylistPackages.remove(packageName);
            mActivityAllowlistMap.remove(packageName);
        }

        // Generate allowlist and denylist mapping for the package
        updateActivityAllowlistAndDenylistMap(packageName);
        mHandler.post(this::blockTopActivitiesOnAllDisplaysIfNecessary);
    }

    private void doHandleRelease() {
        synchronized (mLock) {
            mVendorServiceController.release();
            mLock.notifyAll();
        }
    }

    private void doUpdatePolicy(String packageName, CarAppBlockingPolicy policy, int flags) {
        if (DBG) {
            Slogf.d(TAG, "setting policy from:" + packageName + ",policy:" + policy + ",flags:0x"
                    + Integer.toHexString(flags));
        }
        AppBlockingPackageInfoWrapper[] blocklistWrapper = verifyList(policy.blacklists);
        AppBlockingPackageInfoWrapper[] allowlistWrapper = verifyList(policy.whitelists);
        synchronized (mLock) {
            ClientPolicy clientPolicy = mClientPolicies.get(packageName);
            if (clientPolicy == null) {
                clientPolicy = new ClientPolicy();
                mClientPolicies.put(packageName, clientPolicy);
            }
            if ((flags & CarPackageManager.FLAG_SET_POLICY_ADD) != 0) {
                clientPolicy.addToBlocklists(blocklistWrapper);
                clientPolicy.addToAllowlists(allowlistWrapper);
            } else if ((flags & CarPackageManager.FLAG_SET_POLICY_REMOVE) != 0) {
                clientPolicy.removeBlocklists(blocklistWrapper);
                clientPolicy.removeAllowlists(allowlistWrapper);
            } else { //replace.
                clientPolicy.replaceBlocklists(blocklistWrapper);
                clientPolicy.replaceAllowlists(allowlistWrapper);
            }
            if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
                mWaitingPolicies.remove(policy);
                mLock.notifyAll();
            }
            if (DBG) {
                Slogf.d(TAG, "policy set:" + dumpPoliciesLocked(false));
            }
        }
        mHandler.post(this::blockTopActivitiesOnAllDisplaysIfNecessary);
    }

    @Nullable
    private AppBlockingPackageInfoWrapper[] verifyList(@Nullable AppBlockingPackageInfo[] list) {
        if (list == null) {
            return null;
        }
        LinkedList<AppBlockingPackageInfoWrapper> wrappers = new LinkedList<>();
        for (int i = 0; i < list.length; i++) {
            AppBlockingPackageInfo info = list[i];
            if (info == null) {
                continue;
            }
            boolean isMatching = isInstalledPackageMatching(info);
            wrappers.add(new AppBlockingPackageInfoWrapper(info, isMatching));
        }
        return wrappers.toArray(new AppBlockingPackageInfoWrapper[wrappers.size()]);
    }

    boolean isInstalledPackageMatching(AppBlockingPackageInfo info) {
        PackageInfo packageInfo;
        try {
            packageInfo = mPackageManager.getPackageInfo(info.packageName,
                    PackageManager.GET_SIGNATURES);
        } catch (NameNotFoundException e) {
            return false;
        }
        if (packageInfo == null) {
            return false;
        }
        // if it is system app and client specified the flag, do not check signature
        if ((info.flags & AppBlockingPackageInfo.FLAG_SYSTEM_APP) == 0 ||
                (!PackageManagerHelper.isSystemApp(packageInfo.applicationInfo)
                        && !PackageManagerHelper.isUpdatedSystemApp(packageInfo.applicationInfo))) {
            Signature[] signatures = packageInfo.signatures;
            if (!isAnySignatureMatching(signatures, info.signatures)) {
                return false;
            }
        }
        int version = packageInfo.versionCode;
        if (info.minRevisionCode == 0) {
            if (info.maxRevisionCode == 0) { // all versions
                return true;
            } else { // only max version matters
                return info.maxRevisionCode > version;
            }
        } else { // min version matters
            if (info.maxRevisionCode == 0) {
                return info.minRevisionCode < version;
            } else {
                return (info.minRevisionCode < version) && (info.maxRevisionCode > version);
            }
        }
    }

    /**
     * Any signature from policy matching with package's signatures is treated as matching.
     */
    boolean isAnySignatureMatching(Signature[] fromPackage, Signature[] fromPolicy) {
        if (fromPackage == null) {
            return false;
        }
        if (fromPolicy == null) {
            return false;
        }
        ArraySet<Signature> setFromPackage = new ArraySet<Signature>();
        for (Signature sig : fromPackage) {
            setFromPackage.add(sig);
        }
        for (Signature sig : fromPolicy) {
            if (setFromPackage.contains(sig)) {
                return true;
            }
        }
        return false;
    }

    private AppBlockingPackageInfoWrapper getPackageInfoWrapperForUser(String packageName,
            @UserIdInt int userId, Map<String, Set<String>> configAllowlist,
            Map<String, Set<String>> configBlocklist) {
        PackageInfo info;
        try {
            info = PackageManagerHelper.getPackageInfoAsUser(mPackageManager, packageName,
                    PackageManager.GET_SIGNATURES | PackageManager.GET_ACTIVITIES
                            | PackageManager.MATCH_DIRECT_BOOT_AWARE
                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                            | PackageManager.MATCH_DISABLED_COMPONENTS,
                    userId);
        } catch (NameNotFoundException e) {
            Slogf.w(TAG, packageName + " not installed! User Id: " + userId);
            return null;
        }


        if (info == null || info.applicationInfo == null) {
            return null;
        }

        int flags = 0;
        Set<String> activities = new ArraySet<>();

        if (PackageManagerHelper.isSystemApp(info.applicationInfo)
                || PackageManagerHelper.isUpdatedSystemApp(info.applicationInfo)) {
            flags = AppBlockingPackageInfo.FLAG_SYSTEM_APP;
        }

        /* 1. Check if all or some of this app is in the <activityAllowlist> or
              <systemActivityAllowlist> in config.xml */
        Set<String> configActivitiesForPackage = configAllowlist.get(info.packageName);
        if (configActivitiesForPackage != null) {
            if (DBG) {
                Slogf.d(TAG, info.packageName + " allowlisted");
            }

            if (configActivitiesForPackage.isEmpty()) {
                // Whole Pkg has been allowlisted
                flags |= AppBlockingPackageInfo.FLAG_WHOLE_ACTIVITY;
                // Add all activities to the allowlist
                List<String> activitiesForPackage = getActivitiesInPackage(info);
                if (activitiesForPackage != null) {
                    activities.addAll(activitiesForPackage);
                } else {
                    if (DBG) {
                        Slogf.d(TAG, info.packageName + ": Activities null");
                    }
                }
            } else {
                if (DBG) {
                    Slogf.d(TAG, "Partially Allowlisted. WL Activities: "
                            + configActivitiesForPackage);
                }
                activities.addAll(configActivitiesForPackage);
            }
        }
        /* 2. If app is not listed in the config.xml check their Manifest meta-data to see if
        they have any Distraction Optimized(DO) activities. For non system apps, we check if the
        app install source was a permissible source. This prevents side-loaded apps to fake DO.
        Bypass the check for debug builds for development convenience. The check for activities
        which are already allowlisted in the config can be bypassed since the config can only be
        modified by the OEMs, so side-loading does not matter for such a case. */
        if (!isDebugBuild() && configActivitiesForPackage == null
                && !PackageManagerHelper.isSystemApp(info.applicationInfo)
                && !PackageManagerHelper.isUpdatedSystemApp(info.applicationInfo)) {
            try {
                if (mAllowedAppInstallSources != null) {
                    String installerName = mPackageManager.getInstallerPackageName(
                            info.packageName);
                    if (installerName == null || (installerName != null
                            && !mAllowedAppInstallSources.contains(installerName))) {
                        Slogf.w(TAG, info.packageName + " not installed from permitted sources "
                                + (installerName == null ? "NULL" : installerName));
                        return null;
                    }
                }
            } catch (IllegalArgumentException e) {
                Slogf.w(TAG, info.packageName + " not installed!");
                return null;
            }
        }

        try {
            String[] doActivities = findDistractionOptimizedActivitiesAsUser(info.packageName,
                    userId);
            if (doActivities != null) {
                // Some of the activities in this app are Distraction Optimized.
                if (DBG) {
                    for (String activity : doActivities) {
                        Slogf.d(TAG, "adding " + activity + " from " + info.packageName
                                + " to allowlist");
                    }
                }

                activities.addAll(Arrays.asList(doActivities));
            }
        } catch (NameNotFoundException e) {
            Slogf.w(TAG, "Error reading metadata: " + info.packageName);
            return null;
        }

        // Nothing to add to allowlist
        if (activities.isEmpty()) {
            return null;
        }

        /* 3. Check if parsed activity is in <activityBlocklist> in config.xml. Anything
              in blocklist should not be allowlisted, either as D.O. or by config. */
        if (configBlocklist.containsKey(info.packageName)) {
            Set<String> configBlocklistActivities = configBlocklist.get(info.packageName);
            if (configBlocklistActivities.isEmpty()) {
                // Whole package should be blocklisted.
                return null;
            }
            activities.removeAll(configBlocklistActivities);
        }

        Signature[] signatures;
        signatures = info.signatures;
        AppBlockingPackageInfo appBlockingInfo = new AppBlockingPackageInfo(info.packageName,
                /* minRevisionCode = */ 0, /* maxRevisionCode = */ 0, flags, signatures,
                activities.toArray(new String[activities.size()]));
        AppBlockingPackageInfoWrapper wrapper = new AppBlockingPackageInfoWrapper(
                appBlockingInfo, true);
        return wrapper;
    }

    /**
     * Update map of allowlisted packages and activities of the form {pkgName, Allowlisted
     * activities} and set of denylisted packages. The information can come from a configuration XML
     * resource or from the apps marking their activities as distraction optimized.
     */
    private void updateActivityAllowlistAndDenylistMap(String packageName) {
        int userId = mActivityManager.getCurrentUser();
        Slogf.d(TAG, "Updating allowlist and denylist mapping for package: " + packageName
                + " for UserId: " + userId);
        // Get the apps/activities that are allowlisted in the configuration XML resources.
        Map<String, Set<String>> configAllowlist = generateConfigAllowlist();
        Map<String, Set<String>> configBlocklist = generateConfigBlocklist();

        AppBlockingPackageInfoWrapper wrapper =
                getPackageInfoWrapperForUser(packageName, userId, configAllowlist, configBlocklist);

        if (wrapper == null && userId != UserHandle.SYSTEM.getIdentifier()) {
            Slogf.d(TAG, "Updating allowlist and denylist mapping for package: " + packageName
                    + " for UserId: " + UserHandle.SYSTEM.getIdentifier());
            // check package for system user, in case package is disabled for current user
            wrapper = getPackageInfoWrapperForUser(packageName, UserHandle.SYSTEM.getIdentifier(),
                    configAllowlist, configBlocklist);
        }

        synchronized (mLock) {
            if (wrapper != null) {
                if (DBG) {
                    Slogf.d(TAG, "Package: " + packageName + " added in allowlist.");
                }
                mActivityAllowlistMap.put(packageName, wrapper);
            } else {
                if (DBG) {
                    Slogf.d(TAG, "Package: " + packageName + " added in denylist.");
                }
                mActivityDenylistPackages.add(packageName);
            }
        }
    }

    private Map<String, Set<String>> generateConfigAllowlist() {
        synchronized (mLock) {
            if (mConfiguredAllowlistMap != null) return mConfiguredAllowlistMap;

            Map<String, Set<String>> configAllowlist = new HashMap<>();
            mConfiguredAllowlist = mContext.getString(R.string.activityAllowlist);
            if (mConfiguredAllowlist == null) {
                Slogf.w(TAG, "Allowlist is null.");
            }
            parseConfigList(mConfiguredAllowlist, configAllowlist);

            mConfiguredSystemAllowlist = mContext.getString(R.string.systemActivityAllowlist);
            if (mConfiguredSystemAllowlist == null) {
                Slogf.w(TAG, "System allowlist is null.");
            }
            parseConfigList(mConfiguredSystemAllowlist, configAllowlist);

            // Add the blocking overlay activity to the allowlist, since that needs to run in a
            // restricted state to communicate the reason an app was blocked.
            Set<String> defaultActivity = new ArraySet<>();
            if (mActivityBlockingActivity != null) {
                defaultActivity.add(mActivityBlockingActivity.getClassName());
                configAllowlist.put(mActivityBlockingActivity.getPackageName(), defaultActivity);
            }

            mConfiguredAllowlistMap = configAllowlist;
            return configAllowlist;
        }
    }

    private Map<String, Set<String>> generateConfigBlocklist() {
        synchronized (mLock) {
            if (mConfiguredBlocklistMap != null) return mConfiguredBlocklistMap;

            Map<String, Set<String>> configBlocklist = new HashMap<>();
            mConfiguredBlocklist = mContext.getString(R.string.activityDenylist);
            if (mConfiguredBlocklist == null) {
                if (DBG) {
                    Slogf.d(TAG, "Null blocklist in config");
                }
            }
            parseConfigList(mConfiguredBlocklist, configBlocklist);

            mConfiguredBlocklistMap = configBlocklist;
            return configBlocklist;
        }
    }

    private boolean isDebugBuild() {
        return BuildHelper.isUserDebugBuild() || BuildHelper.isEngBuild();
    }

    /**
     * Parses the given resource and updates the input map of packages and activities.
     *
     * Key is package name and value is list of activities. Empty set implies whole package is
     * included.
     *
     * When there are multiple entries regarding one package, the entry with
     * greater scope wins. Namely if there were 2 entries such that one allowlists
     * an activity, and the other allowlists the entire package of the activity,
     * the package is allowlisted, regardless of input order.
     */
    @VisibleForTesting
    /* package */ void parseConfigList(String configList,
            @NonNull Map<String, Set<String>> packageToActivityMap) {
        if (configList == null) {
            return;
        }
        String[] entries = configList.split(PACKAGE_DELIMITER);
        for (String entry : entries) {
            String[] packageActivityPair = entry.split(PACKAGE_ACTIVITY_DELIMITER);
            Set<String> activities = packageToActivityMap.get(packageActivityPair[0]);
            boolean newPackage = false;
            if (activities == null) {
                activities = new ArraySet<>();
                newPackage = true;
                packageToActivityMap.put(packageActivityPair[0], activities);
            }
            if (packageActivityPair.length == 1) { // whole package
                activities.clear();
            } else if (packageActivityPair.length == 2) {
                // add class name only when the whole package is not allowlisted.
                if (newPackage || (activities.size() > 0)) {
                    activities.add(packageActivityPair[1]);
                }
            }
        }
    }

    @Nullable
    private List<String> getActivitiesInPackage(PackageInfo info) {
        if (info == null || info.activities == null) {
            return null;
        }
        List<String> activityList = new ArrayList<>();
        for (ActivityInfo aInfo : info.activities) {
            activityList.add(aInfo.name);
        }
        return activityList;
    }

    /**
     * Checks if there are any {@link CarAppBlockingPolicyService} and creates a proxy to
     * bind to them and retrieve the {@link CarAppBlockingPolicy}
     */
    @VisibleForTesting
    public void startAppBlockingPolicies() {
        Intent policyIntent = new Intent();
        policyIntent.setAction(CarAppBlockingPolicyService.SERVICE_INTERFACE);
        List<ResolveInfo> policyInfos = mPackageManager.queryIntentServices(policyIntent, 0);
        if (policyInfos == null) { //no need to wait for service binding and retrieval.
            return;
        }
        LinkedList<AppBlockingPolicyProxy> proxies = new LinkedList<>();
        for (ResolveInfo resolveInfo : policyInfos) {
            ServiceInfo serviceInfo = resolveInfo.serviceInfo;
            if (serviceInfo == null) {
                continue;
            }
            if (serviceInfo.isEnabled()) {
                if (mPackageManager.checkPermission(Car.PERMISSION_CONTROL_APP_BLOCKING,
                        serviceInfo.packageName) != PackageManager.PERMISSION_GRANTED) {
                    continue;
                }
                Slogf.i(TAG, "found policy holding service:" + serviceInfo);
                AppBlockingPolicyProxy proxy = new AppBlockingPolicyProxy(this, mContext,
                        mHandler.getLooper(), serviceInfo);
                proxy.connect();
                proxies.add(proxy);
            }
        }
        synchronized (mLock) {
            mProxies = proxies;
        }
    }

    public void onPolicyConnectionAndSet(AppBlockingPolicyProxy proxy,
            CarAppBlockingPolicy policy) {
        doHandlePolicyConnection(proxy, policy);
    }

    public void onPolicyConnectionFailure(AppBlockingPolicyProxy proxy) {
        doHandlePolicyConnection(proxy, null);
    }

    private void doHandlePolicyConnection(AppBlockingPolicyProxy proxy,
            CarAppBlockingPolicy policy) {
        synchronized (mLock) {
            if (mProxies == null) {
                proxy.disconnect();
                return;
            }
            mProxies.remove(proxy);
            if (mProxies.size() == 0) {
                mProxies = null;
            }
        }
        try {
            if (policy != null) {
                if (DBG) {
                    Slogf.d(TAG, "policy setting from policy service:" + proxy.getPackageName());
                }
                doSetAppBlockingPolicy(proxy.getPackageName(), policy, 0);
            }
        } finally {
            proxy.disconnect();
        }
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dump(IndentingPrintWriter writer) {
        synchronized (mLock) {
            writer.println("*CarPackageManagerService*");
            writer.println("mEnableActivityBlocking:" + mEnableActivityBlocking);
            writer.println("mPreventTemplatedAppsFromShowingDialog:"
                    + mPreventTemplatedAppsFromShowingDialog);
            writer.println("mTemplateActivityClassName:" + mTemplateActivityClassName);
            List<String> restrictions = new ArrayList<>(mUxRestrictionsListeners.size());
            for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
                int displayId = mUxRestrictionsListeners.keyAt(i);
                UxRestrictionsListener listener = mUxRestrictionsListeners.valueAt(i);
                restrictions.add(String.format("Display %d is %s",
                        displayId, (listener.isRestricted() ? "restricted" : "unrestricted")));
            }
            writer.println("Display Restrictions:\n" + String.join("\n", restrictions));
            writer.println(" Blocked activity log:");
            mBlockedActivityLogs.dump(writer);
            writer.print(dumpPoliciesLocked(true));
            writer.print("mCurrentDrivingSafetyRegion:");
            writer.println(mCurrentDrivingSafetyRegion);
            writer.print("mTempAllowedActivities:");
            writer.println(mTempAllowedActivities);
            writer.println("Car service overlay packages property name: "
                    + PackageManagerHelper.PROPERTY_CAR_SERVICE_OVERLAY_PACKAGES);
            writer.println("Car service overlay packages: "
                    + SystemProperties.get(
                            PackageManagerHelper.PROPERTY_CAR_SERVICE_OVERLAY_PACKAGES,
                            /* default= */ null));
            mVendorServiceController.dump(writer);
            mBlockingUiCommandListenerMediator.dump(writer);
        }
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dumpProto(ProtoOutputStream proto) {}

    @GuardedBy("mLock")
    private String dumpPoliciesLocked(boolean dumpAll) {
        StringBuilder sb = new StringBuilder();
        if (dumpAll) {
            sb.append("**System allowlist**\n");
            for (AppBlockingPackageInfoWrapper wrapper : mActivityAllowlistMap.values()) {
                sb.append(wrapper.toString() + "\n");
            }
        }
        sb.append("**Client Policies**\n");
        for (Entry<String, ClientPolicy> entry : mClientPolicies.entrySet()) {
            sb.append("Client:" + entry.getKey() + "\n");
            sb.append("  allowlists:\n");
            for (AppBlockingPackageInfoWrapper wrapper : entry.getValue().mAllowlistsMap.values()) {
                sb.append(wrapper.toString() + "\n");
            }
            sb.append("  blocklists:\n");
            for (AppBlockingPackageInfoWrapper wrapper : entry.getValue().mBlocklistsMap.values()) {
                sb.append(wrapper.toString() + "\n");
            }
        }
        sb.append("**Unprocessed policy services**\n");
        if (mProxies != null) {
            for (AppBlockingPolicyProxy proxy : mProxies) {
                sb.append(proxy.toString() + "\n");
            }
        }
        sb.append("**Allowlist string in resource**\n");
        sb.append(mConfiguredAllowlist + "\n");

        sb.append("**System allowlist string in resource**\n");
        sb.append(mConfiguredSystemAllowlist + "\n");

        sb.append("**Blocklist string in resource**\n");
        sb.append(mConfiguredBlocklist + "\n");

        sb.append("**Allowlist map from resource**\n");
        sb.append(mConfiguredAllowlistMap + "\n");

        sb.append("**Blocklist from resource**\n");
        sb.append(mConfiguredBlocklist + "\n");

        return sb.toString();
    }

    /**
     * Returns whether UX restrictions is required for the given display.
     *
     * Non-physical display will use restrictions for {@link Display#DEFAULT_DISPLAY}.
     */
    private boolean isUxRestrictedOnDisplay(int displayId) {
        UxRestrictionsListener listenerForTopTaskDisplay;
        synchronized (mLock) {
            if (mUxRestrictionsListeners.indexOfKey(displayId) < 0) {
                Slogf.w(TAG,
                        "Cannot find UxR listener for display %d, using UxR on default display",
                        displayId);
                listenerForTopTaskDisplay = mUxRestrictionsListeners.get(Display.DEFAULT_DISPLAY);
                if (listenerForTopTaskDisplay == null) {
                    // This should never happen.
                    Slogf.e(TAG, "Missing listener for default display.");
                    return true;
                }
            } else {
                listenerForTopTaskDisplay = mUxRestrictionsListeners.get(displayId);
            }
        }

        return listenerForTopTaskDisplay.isRestricted();
    }

    /**
     * Blocks top activities on all displays if necessary.
     */
    private void blockTopActivitiesOnAllDisplaysIfNecessary() {
        List<? extends TaskInfo> visibleTasks = mActivityService.getVisibleTasksInternal();
        ArrayList<Integer> restrictedDisplayIds = new ArrayList<>();
        synchronized (mLock) {
            for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
                int displayId = mUxRestrictionsListeners.keyAt(i);
                UxRestrictionsListener listener = mUxRestrictionsListeners.valueAt(i);
                if (listener.isRestricted()) {
                    if (DBG) {
                        Slogf.d(TAG, "Display %d is in a UxRestricted state.",
                                displayId);
                    }
                    restrictedDisplayIds.add(displayId);
                } else {
                    if (DBG) {
                        Slogf.d(TAG, "Display %d is not in a UxRestricted state, not blocking.",
                                displayId);
                    }
                }
            }
        }

        // Block activities on a display if it is in a Ux restricted state.
        for (int i = 0; i < restrictedDisplayIds.size(); i++) {
            int displayId = restrictedDisplayIds.get(i);
            if (DBG) {
                Slogf.d(TAG, "Initiating activity blocking on display %d.", displayId);
            }
            blockTopActivitiesOnDisplayIfNecessary(visibleTasks, displayId);
        }
    }

    /**
     * Blocks top activities on the given display if necessary.
     */
    private void blockTopActivitiesOnDisplayIfNecessary(List<? extends TaskInfo> visibleTasks,
            int displayId) {
        Set<Integer> rootTasksBlocked = new ArraySet<>();
        for (TaskInfo topTask : visibleTasks) {
            if (topTask == null) {
                Slogf.e(TAG, "Top tasks contains null.");
                continue;
            }

            int displayIdOfTask = TaskInfoHelper.getDisplayId(topTask);
            if (displayIdOfTask != displayId) {
                // Only block activities on the given display. Skip if it's not the given
                // display.
                continue;
            }

            if (rootTasksBlocked.contains(TaskInfoHelper.geParentTaskId(topTask))) {
                // Root task already blocked. No need to launch another ABA
                if (DBG) {
                    Slogf.d(TAG, "Root task %d has already been blocked.",
                            TaskInfoHelper.geParentTaskId(topTask));
                }
                continue;
            }

            boolean blocked = blockTopActivity(topTask);
            if (blocked) {
                if (DBG) {
                    Slogf.d(TAG, "Display %d has been blocked. Root task id: %d",
                            displayIdOfTask, TaskInfoHelper.geParentTaskId(topTask));
                }
                // When root tasks are not used, the value of parent root task would be -1.
                // It is okay to add it to this list as it would help to skip all the tasks
                // which are blocked but not parented to root task.
                rootTasksBlocked.add(TaskInfoHelper.geParentTaskId(topTask));
            }
        }
    }

    /**
     * Blocks the top activity if it's on a Ux restricted display.
     *
     * @return {@code true} if the {@code topTask} was blocked, {@code false} otherwise.
     */
    private boolean blockTopActivityIfNecessary(TaskInfo topTask) {
        int displayId = TaskInfoHelper.getDisplayId(topTask);
        synchronized (mLock) {
            if (!Objects.equals(mActivityBlockingActivity, topTask.topActivity)
                    && mTopActivityWithDialogPerDisplay.contains(displayId)
                    && !topTask.topActivity.equals(
                            mTopActivityWithDialogPerDisplay.get(displayId))) {
                // Clear top activity-with-dialog if the activity has changed on this display.
                mTopActivityWithDialogPerDisplay.remove(displayId);
            }
        }
        if (isUxRestrictedOnDisplay(displayId)) {
            return doBlockTopActivityIfNotAllowed(displayId, topTask);
        }
        return false;
    }

    /**
     * Blocks the top activity if not allowed.
     *
     * @return {@code true} if the {@code topTask} was blocked, {@code false} otherwise.
     */
    private boolean blockTopActivity(TaskInfo topTask) {
        int displayId = TaskInfoHelper.getDisplayId(topTask);
        synchronized (mLock) {
            if (!Objects.equals(mActivityBlockingActivity, topTask.topActivity)
                    && mTopActivityWithDialogPerDisplay.contains(displayId)
                    && !topTask.topActivity.equals(
                            mTopActivityWithDialogPerDisplay.get(displayId))) {
                // Clear top activity-with-dialog if the activity has changed on this display.
                mTopActivityWithDialogPerDisplay.remove(displayId);
            }
        }

        return doBlockTopActivityIfNotAllowed(displayId, topTask);
    }

    /**
     * @return {@code true} if the {@code topTask} was blocked, {@code false} otherwise.
     */
    private boolean doBlockTopActivityIfNotAllowed(int displayId, TaskInfo topTask) {
        if (isBlockingUiTask(topTask)) {
            // If topTask is already ActivityBlockingActivity, treat it as already blocked.
            return true;
        }
        boolean allowed = isActivityAllowed(topTask);
        if (DBG) {
            Slogf.d(TAG, "new activity:" + topTask.toString() + " allowed:" + allowed);
        }
        if (allowed) {
            return false;
        }
        if (!mEnableActivityBlocking) {
            Slogf.d(TAG, "Current activity " + topTask.topActivity
                    + " not allowed, blocking disabled.");
            return false;
        }
        if (DBG) {
            Slogf.d(TAG, "Current activity " + topTask.topActivity
                    + " not allowed, will block.");
        }
        // TaskMonitor based on TaskOrganizer reflects only the actually launched tasks,
        // (TaskStackChangeListener reflects the internal state of ActivityTaskManagerService)
        // So it takes some time to recognize the ActivityBlockingActivity is shown.
        // This guard is to prevent from launching ABA repeatedly until it is shown.
        TaskInfo blockingActivityTarget = mBlockingActivityTargets.get(displayId);
        if (blockingActivityTarget != null && topTask.taskId == blockingActivityTarget.taskId) {
            long blockingActivityLaunchTime = mBlockingActivityLaunchTimes.get(displayId);
            if (SystemClock.uptimeMillis() - blockingActivityLaunchTime < ABA_LAUNCH_TIMEOUT_MS) {
                Slogf.d(TAG, "Waiting for BlockingActivity to be shown: displayId=%d", displayId);
                return false;
            }
        }

        // Figure out the root task of blocked task.
        ComponentName rootTaskActivityName = topTask.baseActivity;

        boolean isRootDO = false;
        if (rootTaskActivityName != null) {
            isRootDO = isActivityDistractionOptimized(
                    rootTaskActivityName.getPackageName(), rootTaskActivityName.getClassName());
        }

        Intent newActivityIntent = createBlockingActivityIntent(
                mActivityBlockingActivity, TaskInfoHelper.getDisplayId(topTask),
                topTask.topActivity.flattenToShortString(), topTask.taskId,
                rootTaskActivityName.flattenToString(), isRootDO);

        // Intent contains all info to debug what is blocked - log into both logcat and dumpsys.
        String log = "Starting blocking activity with intent: " + newActivityIntent.toUri(0);
        if (Slogf.isLoggable(TAG, Log.INFO)) {
            Slogf.i(TAG, log);
        }
        mBlockedActivityLogs.log(log);
        mBlockingActivityLaunchTimes.put(displayId, SystemClock.uptimeMillis());
        mBlockingActivityTargets.put(displayId, topTask);
        mActivityService.blockActivity(topTask, newActivityIntent);
        return true;
    }

    private boolean isActivityAllowed(TaskInfo topTaskInfoContainer) {
        ComponentName activityName = topTaskInfoContainer.topActivity;
        boolean isDistractionOptimized = isActivityDistractionOptimized(
                activityName.getPackageName(),
                activityName.getClassName());
        if (!isDistractionOptimized) {
            return false;
        }
        return !(mPreventTemplatedAppsFromShowingDialog
                && isTemplateActivity(activityName)
                && isActivityShowingADialogOnDisplay(activityName,
                        TaskInfoHelper.getDisplayId(topTaskInfoContainer)));
    }

    private boolean isTemplateActivity(ComponentName activityName) {
        // TODO(b/191263486): Finalise on how to detect the templated activities.
        return activityName.getClassName().equals(mTemplateActivityClassName);
    }

    private boolean isActivityShowingADialogOnDisplay(ComponentName activityName, int displayId) {
        String output = dumpWindows();
        List<WindowDumpParser.Window> appWindows =
                WindowDumpParser.getParsedAppWindows(output, activityName.getPackageName());
        // TODO(b/192354699): Handle case where an activity can have multiple instances on the same
        //  display.
        int totalAppWindows = appWindows.size();
        String firstActivityRecord = null;
        int numTopActivityAppWindowsOnDisplay = 0;
        for (int i = 0; i < totalAppWindows; i++) {
            WindowDumpParser.Window appWindow = appWindows.get(i);
            if (appWindow.getDisplayId() != displayId) {
                continue;
            }
            if (TextUtils.isEmpty(appWindow.getActivityRecord())) {
                continue;
            }
            if (firstActivityRecord == null) {
                firstActivityRecord = appWindow.getActivityRecord();
            }
            if (firstActivityRecord.equals(appWindow.getActivityRecord())) {
                numTopActivityAppWindowsOnDisplay++;
            }
        }
        Slogf.d(TAG, "Top activity =  " + activityName);
        Slogf.d(TAG, "Number of app widows of top activity = " + numTopActivityAppWindowsOnDisplay);
        boolean isShowingADialog = numTopActivityAppWindowsOnDisplay > 1;
        synchronized (mLock) {
            if (isShowingADialog) {
                mTopActivityWithDialogPerDisplay.put(displayId, activityName);
            } else {
                mTopActivityWithDialogPerDisplay.remove(displayId);
            }
        }
        return isShowingADialog;
    }

    private String dumpWindows() {
        try {
            ParcelFileDescriptor[] fileDescriptors = ParcelFileDescriptor.createSocketPair();
            mWindowManagerBinder.dump(
                    fileDescriptors[0].getFileDescriptor(), WINDOW_DUMP_ARGUMENTS);
            fileDescriptors[0].close();
            StringBuilder outputBuilder = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(
                    new FileReader(fileDescriptors[1].getFileDescriptor()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    outputBuilder.append(line).append("\n");
                }
            }
            fileDescriptors[1].close();
            return outputBuilder.toString();
        } catch (IOException | RemoteException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates an intent to start blocking activity.
     *
     * @param blockingActivity the activity to launch
     * @param blockedActivity  the activity being blocked
     * @param blockedTaskId    the blocked task id, which contains the blocked activity
     * @param taskRootActivity root activity of the blocked task
     * @param isRootDo         denotes if the root activity is distraction optimised
     * @return an intent to launch the blocking activity.
     */
    private static Intent createBlockingActivityIntent(ComponentName blockingActivity,
            int displayId, String blockedActivity, int blockedTaskId, String taskRootActivity,
            boolean isRootDo) {
        Intent newActivityIntent = new Intent();
        newActivityIntent.setFlags(
                Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
        newActivityIntent.setComponent(blockingActivity);
        newActivityIntent.putExtra(
                BLOCKING_INTENT_EXTRA_DISPLAY_ID, displayId);
        newActivityIntent.putExtra(
                BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME, blockedActivity);
        newActivityIntent.putExtra(
                BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, blockedTaskId);
        newActivityIntent.putExtra(
                BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME, taskRootActivity);
        newActivityIntent.putExtra(
                BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, isRootDo);

        return newActivityIntent;
    }

    /**
     * Enable/Disable activity blocking by correspondingly enabling/disabling broadcasting UXR
     * changes in {@link CarUxRestrictionsManagerService}. This is only available in
     * engineering builds for development convenience.
     */
    @Override
    public void setEnableActivityBlocking(boolean enable) {
        if (!isDebugBuild()) {
            Slogf.e(TAG, "Cannot enable/disable activity blocking");
            return;
        }

        // Check if the caller has the same signature as that of the car service.
        if (mPackageManager.checkSignatures(Process.myUid(), Binder.getCallingUid())
                != PackageManager.SIGNATURE_MATCH) {
            throw new SecurityException(
                    "Caller " + mPackageManager.getNameForUid(Binder.getCallingUid())
                    + " does not have the right signature");
        }
        mCarUxRestrictionsService.setUxRChangeBroadcastEnabled(enable);
    }

    @Override
    public CarVersion getTargetCarVersion(String packageName) {
        return getTargetCarVersion(Binder.getCallingUserHandle(), packageName);
    }

    @Override
    public CarVersion getSelfTargetCarVersion(String packageName) {
        checkCalledByPackage(mContext, packageName);

        return getTargetCarVersion(Binder.getCallingUserHandle(), packageName);
    }

    /**
     * Public, as it's also used by {@code ICarImpl}.
     */
    public CarVersion getTargetCarVersion(UserHandle user, String packageName) {
        Context context = mContext.createContextAsUser(user, /* flags= */ 0);
        return getTargetCarVersion(context, packageName);
    }

    /**
     * Used by {@code CarShellCommand} as well.
     */
    @Nullable
    public static CarVersion getTargetCarVersion(Context context, String packageName) {
        String permission = android.Manifest.permission.QUERY_ALL_PACKAGES;
        if (context.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
            Slogf.w(TAG, "getTargetCarVersion(%s): UID %d doesn't have %s permission",
                    packageName, Binder.getCallingUid(), permission);
            throw new SecurityException("requires permission " + permission);
        }
        ApplicationInfo info = null;
        try {
            info = context.getPackageManager().getApplicationInfo(packageName,
                    PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA));
        } catch (NameNotFoundException e) {
            if (DBG) {
                Slogf.d(TAG, "getTargetCarVersion(%s, %s): not found: %s", context.getUser(),
                        packageName, e);
            }
            throw new ServiceSpecificException(CarPackageManager.ERROR_CODE_NO_PACKAGE,
                    e.getMessage());
        }
        return CarVersionParser.getTargetCarVersion(info);
    }

    /**
     * Get the distraction optimized activities for the given package.
     *
     * @param pkgName Name of the package
     * @return Array of the distraction optimized activities in the package
     */
    @Nullable
    public String[] getDistractionOptimizedActivities(String pkgName) {
        try {
            return findDistractionOptimizedActivitiesAsUser(pkgName,
                    mActivityManager.getCurrentUser());
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    @Override
    public boolean requiresDisplayCompat(String packageName) {
        if (!callerCanQueryPackage(packageName)) {
            throw new SecurityException("requires permission " + QUERY_ALL_PACKAGES);
        }
        int callingUid = Binder.getCallingUid();
        if (!hasPermissionGranted(PERMISSION_MANAGE_DISPLAY_COMPATIBILITY, callingUid)) {
            throw new SecurityException("requires permission "
                    + PERMISSION_MANAGE_DISPLAY_COMPATIBILITY);
        }
        return CarServiceHelperWrapper.getInstance().requiresDisplayCompat(
                Objects.requireNonNull(packageName, "packageName cannot be Null"));
    }

    @Override
    public boolean requiresDisplayCompatForUser(String packageName, int userId) {
        if (!callerCanQueryPackage(packageName)) {
            throw new SecurityException("requires permission " + QUERY_ALL_PACKAGES);
        }
        int callingUid = Binder.getCallingUid();
        if (!hasPermissionGranted(PERMISSION_MANAGE_DISPLAY_COMPATIBILITY, callingUid)) {
            throw new SecurityException("requires permission "
                    + PERMISSION_MANAGE_DISPLAY_COMPATIBILITY);
        }
        return CarServiceHelperWrapper.getInstance().requiresDisplayCompatForUser(
                Objects.requireNonNull(packageName, "packageName cannot be Null"), userId);
    }

    private String[] findDistractionOptimizedActivitiesAsUser(String pkgName, int userId)
            throws NameNotFoundException {
        String regionString;
        synchronized (mLock) {
            regionString = mCurrentDrivingSafetyRegion;
        }
        return CarAppMetadataReader.findDistractionOptimizedActivitiesAsUser(mContext, pkgName,
                userId, regionString);
    }

    /**
     * Reading policy and setting policy can take time. Run it in a separate handler thread.
     */
    private static final class PackageHandler extends Handler {
        private static final String TAG = CarLog.tagFor(CarPackageManagerService.class);

        private static final int MSG_INIT = 0;
        private static final int MSG_PARSE_PKG = 1;
        private static final int MSG_UPDATE_POLICY = 2;
        private static final int MSG_RELEASE = 3;

        private final WeakReference<CarPackageManagerService> mService;

        private PackageHandler(Looper looper, CarPackageManagerService service) {
            super(looper);
            mService = new WeakReference<CarPackageManagerService>(service);
        }

        private void requestInit() {
            Message msg = obtainMessage(MSG_INIT);
            sendMessage(msg);
        }

        private void requestRelease() {
            removeMessages(MSG_UPDATE_POLICY);
            Message msg = obtainMessage(MSG_RELEASE);
            sendMessage(msg);
        }

        private void requestUpdatingPolicy(String packageName, CarAppBlockingPolicy policy,
                int flags) {
            Pair<String, CarAppBlockingPolicy> pair = new Pair<>(packageName, policy);
            Message msg = obtainMessage(MSG_UPDATE_POLICY, flags, 0, pair);
            sendMessage(msg);
        }

        private void requestParsingInstalledPkg(String packageName) {
            Message msg = obtainMessage(MSG_PARSE_PKG, packageName);
            sendMessage(msg);
        }

        @Override
        public void handleMessage(Message msg) {
            CarPackageManagerService service = mService.get();
            if (service == null) {
                Slogf.i(TAG, "handleMessage null service");
                return;
            }
            switch (msg.what) {
                case MSG_INIT:
                    service.doHandleInit();
                    break;
                case MSG_PARSE_PKG:
                    service.doParseInstalledPackage((String) msg.obj);
                    break;
                case MSG_UPDATE_POLICY:
                    Pair<String, CarAppBlockingPolicy> pair =
                            (Pair<String, CarAppBlockingPolicy>) msg.obj;
                    service.doUpdatePolicy(pair.first, pair.second, msg.arg1);
                    break;
                case MSG_RELEASE:
                    service.doHandleRelease();
                    break;
                default:
                    break;
            }
        }
    }

    private static class AppBlockingPackageInfoWrapper {
        private final AppBlockingPackageInfo info;
        /**
         * Whether the current info is matching with the target package in system. Mismatch can
         * happen for version out of range or signature mismatch.
         */
        private boolean isMatching;

        private AppBlockingPackageInfoWrapper(AppBlockingPackageInfo info, boolean isMatching) {
            this.info = info;
            this.isMatching = isMatching;
        }

        @Override
        public String toString() {
            return "AppBlockingPackageInfoWrapper [info=" + info + ", isMatching=" + isMatching +
                    "]";
        }
    }

    /**
     * Client policy holder per each client. Should be accessed with CarpackageManagerService.this
     * held.
     */
    private static class ClientPolicy {
        private final HashMap<String, AppBlockingPackageInfoWrapper> mAllowlistsMap =
                new HashMap<>();
        private final HashMap<String, AppBlockingPackageInfoWrapper> mBlocklistsMap =
                new HashMap<>();

        private void replaceAllowlists(AppBlockingPackageInfoWrapper[] allowlists) {
            mAllowlistsMap.clear();
            addToAllowlists(allowlists);
        }

        private void addToAllowlists(AppBlockingPackageInfoWrapper[] allowlists) {
            if (allowlists == null) {
                return;
            }
            for (AppBlockingPackageInfoWrapper wrapper : allowlists) {
                if (wrapper != null) {
                    mAllowlistsMap.put(wrapper.info.packageName, wrapper);
                }
            }
        }

        private void removeAllowlists(AppBlockingPackageInfoWrapper[] allowlists) {
            if (allowlists == null) {
                return;
            }
            for (AppBlockingPackageInfoWrapper wrapper : allowlists) {
                if (wrapper != null) {
                    mAllowlistsMap.remove(wrapper.info.packageName);
                }
            }
        }

        private void replaceBlocklists(AppBlockingPackageInfoWrapper[] blocklists) {
            mBlocklistsMap.clear();
            addToBlocklists(blocklists);
        }

        private void addToBlocklists(AppBlockingPackageInfoWrapper[] blocklists) {
            if (blocklists == null) {
                return;
            }
            for (AppBlockingPackageInfoWrapper wrapper : blocklists) {
                if (wrapper != null) {
                    mBlocklistsMap.put(wrapper.info.packageName, wrapper);
                }
            }
        }

        private void removeBlocklists(AppBlockingPackageInfoWrapper[] blocklists) {
            if (blocklists == null) {
                return;
            }
            for (AppBlockingPackageInfoWrapper wrapper : blocklists) {
                if (wrapper != null) {
                    mBlocklistsMap.remove(wrapper.info.packageName);
                }
            }
        }
    }

    private class ActivityListener implements CarActivityService.ActivityListener {
        @Override
        public void onActivityCameOnTop(TaskInfo topTask) {
            if (topTask == null) {
                Slogf.e(TAG, "Received callback with null top task.");
                return;
            }
            boolean isBlockingActivity = Objects.equals(mActivityBlockingActivity,
                    topTask.topActivity);
            synchronized (mLock) {
                if (isBlockingActivity) {
                    // This is to keep track of the blocking ui taskInfo.
                    mBlockingUiTaskInfoPerDisplay.put(TaskInfoHelper.getDisplayId(topTask),
                            topTask);
                }
                mLastKnownDisplayIdForTask.put(topTask.taskId,
                        TaskInfoHelper.getDisplayId(topTask));
            }
            blockTopActivityIfNecessary(topTask);
        }

        // TODO(b/358905871): Verify if onTaskInfoChanged required to trigger finishing of
        //  blocking ui.
        @Override
        public void onTaskVanished(TaskInfo taskInfo) {
            if (taskInfo == null) {
                Slogf.e(TAG, "Received callback with null task info.");
                return;
            }
            int lastKnownDisplayId;
            synchronized (mLock) {
                // Only update the array if display Id for the task is valid since display Id can
                // often be invalid when the task has vanished.
                if (TaskInfoHelper.getDisplayId(taskInfo) != Display.INVALID_DISPLAY) {
                    mLastKnownDisplayIdForTask.put(taskInfo.taskId,
                            TaskInfoHelper.getDisplayId(taskInfo));
                }
                lastKnownDisplayId = mLastKnownDisplayIdForTask.get(taskInfo.taskId);
            }
            // Only finish the blocking ui if it is visible and the activity that is being
            // blocked has vanished which could have crashed due to which there is a need for
            // blocking ui to finish.
            if (isBlockingUiVisible(lastKnownDisplayId) && isBlockedActivityTarget(
                    lastKnownDisplayId, taskInfo)) {
                if (DBG) {
                    Slogf.d(TAG,
                            "Finish blocking ui callback due to task %s which was blocked on "
                                    + "display id %d.", taskInfo.taskId, lastKnownDisplayId);
                }
                mHandler.post(() -> finishBlockingUi(taskInfo));
                cleanUpBlockingUiInformation(lastKnownDisplayId);
            } else if (isBlockingUiTask(taskInfo)) {
                if (DBG) {
                    Slogf.d(TAG, "Blocking ui has vanished on display id %d.", lastKnownDisplayId);
                }
                cleanUpBlockingUiInformation(lastKnownDisplayId);
            }
        }
    }

    /**
     * Cleans up blocking ui information since either the blocking ui itself finished or the task
     * that was being blocked by the blocking ui finished. In both the cases, the blocking ui will
     * be finishing itself, so clean up the information. Since blocking ui is finished, also clean
     * up the {@code mBlockingUiTaskInfoPerDisplay}.
     */
    private void cleanUpBlockingUiInformation(int lastKnownDisplayId) {
        if (mBlockingActivityTargets.contains(lastKnownDisplayId)) {
            mBlockingActivityLaunchTimes.put(lastKnownDisplayId, 0);
            mBlockingActivityTargets.delete(lastKnownDisplayId);
        }
        synchronized (mLock) {
            mBlockingUiTaskInfoPerDisplay.delete(lastKnownDisplayId);
        }
    }

    /**
     * Checks if the {@link TaskInfo} is the blocking ui.
     *
     * @return {@code true} if blocking ui is the {@link TaskInfo}, {@code false} otherwise.
     */
    private boolean isBlockingUiTask(TaskInfo taskInfo) {
        return taskInfo.topActivity != null && taskInfo.topActivity.equals(
                mActivityBlockingActivity);
    }

    /**
     * Checks if blocking ui is visible on that {@code displayId}.
     *
     * @param displayId the display id of the {@link TaskInfo}.
     * @return {@code true} if the blocking ui is visible, {@code false} otherwise.
     */
    private boolean isBlockingUiVisible(int displayId) {
        synchronized (mLock) {
            return mBlockingUiTaskInfoPerDisplay.get(displayId) != null && TaskInfoHelper.isVisible(
                    mBlockingUiTaskInfoPerDisplay.get(displayId));
        }
    }

    /**
     * Check if this stack change came from the blocked {@code taskInfo} in
     * {@code mBlockingActivityTargets} for {@code displayId}. Ignore other activities since they
     * cannot can cause a visibility change for the blocked activity target.
     *
     * <p>Note that {@code mBlockingActivityTargets} for the display id can be null due to a race
     * condition when an NDO activity and the blocking ui is finishing at the same time since
     * {@link #cleanUpBlockingUiInformation} is called from various places.
     *
     * @param displayId the display id of the {@link TaskInfo}.
     * @param taskInfo  {@link TaskInfo} due to which the task stack changed.
     * @return {@code true} if this stack change came from blocked {@code taskInfo} for
     * {@code displayId}, {@code false} otherwise.
     */
    private boolean isBlockedActivityTarget(int displayId, TaskInfo taskInfo) {
        return mBlockingActivityTargets.get(displayId) != null
                && mBlockingActivityTargets.get(displayId).taskId == taskInfo.taskId;
    }

    /**
     * Registers the {@link ICarBlockingUiCommandListener} listening for the commands to control the
     * blocking ui.
     *
     * @param listener  listener to register.
     * @param displayId display Id with which the listener is associated.
     */
    public void registerBlockingUiCommandListener(ICarBlockingUiCommandListener listener,
            int displayId) {
        ensurePermission();
        mBlockingUiCommandListenerMediator.registerBlockingUiCommandListener(listener, displayId);
    }

    /**
     * Unregisters the {@link ICarBlockingUiCommandListener}.
     *
     * @param listener listener to unregister.
     */
    public void unregisterBlockingUiCommandListener(ICarBlockingUiCommandListener listener) {
        ensurePermission();
        mBlockingUiCommandListenerMediator.unregisterBlockingUiCommandListener(listener);
    }

    /**
     * Broadcast the finish command to listeners.
     *
     * @param taskInfo the {@link TaskInfo} due to which finish is broadcast to the listeners.
     */
    public void finishBlockingUi(TaskInfo taskInfo) {
        ensurePermission();
        int displayId = getLastKnownDisplayIdForTask(taskInfo.taskId);
        mBlockingUiCommandListenerMediator.finishBlockingUi(taskInfo, displayId);
        cleanUpLastKnownDisplayIdForTask(taskInfo);
    }

    /**
     * Returns the last known display Id for the given {@link TaskInfo}.
     */
    private int getLastKnownDisplayIdForTask(int taskId) {
        synchronized (mLock) {
            return mLastKnownDisplayIdForTask.get(taskId);
        }
    }

    /**
     * Removes the task from {@code mLastKnownDisplayIdForTask}.
     */
    private void cleanUpLastKnownDisplayIdForTask(TaskInfo taskInfo) {
        synchronized (mLock) {
            // This can happen when the task has not been removed from mLastKnownDisplayIdForTask
            // when the task vanishes in onTaskVanished.
            mLastKnownDisplayIdForTask.delete(taskInfo.taskId);
        }
    }

    /**
     * Get number of registered callbacks for the display ID.
     *
     * @param displayId display Id with which the listener is associated.
     * @return number of registered callbacks for the given {@code displayId}.
     */
    @VisibleForTesting
    public int getCarBlockingUiCommandListenerRegisteredCallbacksForDisplay(int displayId) {
        return mBlockingUiCommandListenerMediator
                .getCarBlockingUiCommandListenerRegisteredCallbacksForDisplay(displayId);
    }

    /** Ensure permission is granted. */
    private void ensurePermission() {
        if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException(
                    "requires permission " + Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY);
        }
    }

    /**
     * Listens to the UX restrictions from {@link CarUxRestrictionsManagerService} and initiates
     * checking if the foreground Activity should be blocked.
     */
    private class UxRestrictionsListener extends ICarUxRestrictionsChangeListener.Stub {
        @GuardedBy("mLock")
        @Nullable
        private CarUxRestrictions mCurrentUxRestrictions;
        private final CarUxRestrictionsManagerService uxRestrictionsService;
        private final int mDisplayId;

        UxRestrictionsListener(CarUxRestrictionsManagerService service, int displayId) {
            uxRestrictionsService = service;
            mDisplayId = displayId;
        }

        @Override
        public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
            if (DBG) {
                Slogf.d(TAG, "Received uxr restrictions: Requires DO? %b : %d on display %d",
                        restrictions.isRequiresDistractionOptimization(),
                        restrictions.getActiveRestrictions(), mDisplayId);
            }

            // Check if top activities need blocking on this display.
            boolean shouldCheck = false;
            synchronized (mLock) {
                mCurrentUxRestrictions = new CarUxRestrictions(restrictions);
                shouldCheck = mCurrentUxRestrictions.isRequiresDistractionOptimization();
            }

            if (DBG) {
                Slogf.d(TAG, "Should check top tasks for blocking on display %d?: " + shouldCheck,
                        mDisplayId);
            }

            if (shouldCheck) {
                // Each UxRestrictionsListener is only responsible for blocking activities on their
                // own display.
                mHandler.post(() -> blockTopActivitiesOnDisplayIfNecessary(
                        mActivityService.getVisibleTasksInternal(), mDisplayId));
            }
        }

        private boolean isRestricted() {
            // If current restrictions is null, try querying the service, once.
            synchronized (mLock) {
                if (mCurrentUxRestrictions == null) {
                    mCurrentUxRestrictions = uxRestrictionsService.getCurrentUxRestrictions(
                            mDisplayId);
                }
                if (mCurrentUxRestrictions != null) {
                    return mCurrentUxRestrictions.isRequiresDistractionOptimization();
                }
            }

            // If restriction information is still not available (could happen during bootup),
            // return not restricted.  This maintains parity with previous implementation but needs
            // a revisit as we test more.
            return false;
        }
    }

    /**
     * Called when a window change event is received by the {@link CarSafetyAccessibilityService}.
     */
    @VisibleForTesting
    void onWindowChangeEvent(@NonNull AccessibilityEvent event) {
        boolean receivedFromActivityBlockingActivity =
                event.getPackageName() != null && event.getClassName() != null
                        && mActivityBlockingActivity.getPackageName().contentEquals(
                        event.getPackageName())
                        && mActivityBlockingActivity.getClassName().contentEquals(
                        event.getClassName());
        if (!receivedFromActivityBlockingActivity) {
            int displayId = event.getDisplayId();
            if (isUxRestrictedOnDisplay(displayId)) {
                if (DBG) {
                    Slogf.d(TAG, "onWindowChange event from package %s on Ux restricted display %d,"
                            + " checking activity blocking", event.getPackageName(), displayId);
                }
                // Schedule activity blocking with mHandler to ensure there is no concurrent
                // activity blocking.
                mHandler.post(() ->
                        blockTopActivitiesOnDisplayIfNecessary(
                            mActivityService.getVisibleTasksInternal(), displayId));
            }
        } else {
            Slogf.d(TAG, "Discarded onWindowChangeEvent received from "
                    + "ActivityBlockingActivity");
        }
    }

    /**
     * Listens to the package install/uninstall events to know when to initiate parsing
     * installed packages.
     */
    private class PackageParsingEventReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null || intent.getAction() == null) {
                return;
            }
            if (DBG) {
                Slogf.d(TAG, "PackageParsingEventReceiver Received " + intent.getAction());
            }
            String action = intent.getAction();
            if (isPackageManagerAction(action)) {
                // send a delayed message so if we received multiple related intents, we parse
                // only once.
                logEventChange(intent);
                String packageName = getPackageName(intent);
                mHandler.requestParsingInstalledPkg(packageName);
            }
        }

        private String getPackageName(Intent intent) {
            // For mPackageManagerActions, data should contain package name.
            String dataString = intent.getDataString();
            if (dataString == null) return null;

            String scheme = intent.getScheme();
            if (!Objects.equals(scheme, "package")) return null;

            String[] splitData = intent.getDataString().split(":");
            if (splitData.length < 2) return null;

            return splitData[1];
        }

        private boolean isPackageManagerAction(String action) {
            return mPackageManagerActions.contains(action);
        }

        /**
         * Convenience log function to log what changed.  Logs only when more debug logs
         * are needed - DBG needs to be true
         */
        private void logEventChange(Intent intent) {
            if (intent == null) {
                return;
            }
            if (DBG) {
                String packageName = intent.getData().getSchemeSpecificPart();
                Slogf.d(TAG, "Pkg Changed:" + packageName);
                String action = intent.getAction();
                if (action == null) {
                    return;
                }
                if (action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
                    Slogf.d(TAG, "Changed components");
                    String[] cc = intent
                            .getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
                    if (cc != null) {
                        for (String c : cc) {
                            Slogf.d(TAG, c);
                        }
                    }
                } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
                        || action.equals(Intent.ACTION_PACKAGE_ADDED)) {
                    Slogf.d(TAG, action + " Replacing?: "
                            + intent.getBooleanExtra(Intent.EXTRA_REPLACING, false));
                }
            }
        }
    }
}
