/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.healthconnect.migration;

import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_ALLOWED;
import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_APP_UPGRADE_REQUIRED;
import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_COMPLETE;
import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IDLE;
import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IN_PROGRESS;
import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_MODULE_UPGRADE_REQUIRED;

import static com.android.server.healthconnect.migration.MigrationConstants.ALLOWED_STATE_START_TIME_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.CURRENT_STATE_START_TIME_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.HAVE_RESET_MIGRATION_STATE_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.HC_PACKAGE_NAME_CONFIG_NAME;
import static com.android.server.healthconnect.migration.MigrationConstants.HC_RELEASE_CERT_CONFIG_NAME;
import static com.android.server.healthconnect.migration.MigrationConstants.IDLE_TIMEOUT_REACHED_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.IN_PROGRESS_TIMEOUT_REACHED_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_COMPLETE_JOB_NAME;
import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_PAUSE_JOB_NAME;
import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_STARTS_COUNT_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_STATE_PREFERENCE_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.MIN_DATA_MIGRATION_SDK_EXTENSION_VERSION_KEY;
import static com.android.server.healthconnect.migration.MigrationConstants.PREMATURE_MIGRATION_TIMEOUT_DATE;
import static com.android.server.healthconnect.migration.MigrationUtils.filterIntent;
import static com.android.server.healthconnect.migration.MigrationUtils.filterPermissions;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.health.connect.Constants;
import android.health.connect.HealthConnectDataState;
import android.health.connect.HealthConnectManager;
import android.os.Build;
import android.os.UserHandle;
import android.os.ext.SdkExtensions;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.server.healthconnect.HealthConnectThreadScheduler;
import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * A database operations helper for migration states management.
 *
 * @hide
 */
public final class MigrationStateManager {

    private static final String TAG = "MigrationStateManager";
    private final PreferenceHelper mPreferenceHelper;

    @GuardedBy("mLock")
    private final Set<StateChangedListener> mStateChangedListeners = new CopyOnWriteArraySet<>();

    private final Object mLock = new Object();
    private final MigrationBroadcastScheduler mMigrationBroadcastScheduler;
    private final HealthConnectThreadScheduler mThreadScheduler;
    private UserHandle mUserHandle;

    public MigrationStateManager(
            UserHandle userHandle,
            PreferenceHelper preferenceHelper,
            MigrationBroadcastScheduler migrationBroadcastScheduler,
            HealthConnectThreadScheduler threadScheduler) {
        mUserHandle = userHandle;
        mPreferenceHelper = preferenceHelper;
        mMigrationBroadcastScheduler = migrationBroadcastScheduler;
        mThreadScheduler = threadScheduler;
    }

    /** Re-initialize this class instance with the new user */
    public void shutDownCurrentUser(Context context) {
        synchronized (mLock) {
            MigrationStateChangeJob.cancelAllJobs(context);
        }
    }

    /** Re-initialize this class instance with the new user */
    public void setupForUser(UserHandle userHandle) {
        synchronized (mLock) {
            mUserHandle = userHandle;
        }
    }

    /** Registers {@link StateChangedListener} for observing migration state changes. */
    public void addStateChangedListener(StateChangedListener listener) {
        synchronized (mLock) {
            mStateChangedListeners.add(listener);
        }
    }

    /**
     * Adds the min data migration sdk and updates the migration state to pending.
     *
     * @param minVersion the desired sdk version.
     */
    public void setMinDataMigrationSdkExtensionVersion(Context context, int minVersion) {
        synchronized (mLock) {
            if (minVersion <= getUdcSdkExtensionVersion()) {
                updateMigrationState(context, MIGRATION_STATE_ALLOWED);
                return;
            }
            mPreferenceHelper.insertOrReplacePreference(
                    MIN_DATA_MIGRATION_SDK_EXTENSION_VERSION_KEY, String.valueOf(minVersion));
            updateMigrationState(context, MIGRATION_STATE_MODULE_UPGRADE_REQUIRED);
        }
    }

    /**
     * @return true when the migration state is in_progress.
     */
    public boolean isMigrationInProgress() {
        return getMigrationState() == MIGRATION_STATE_IN_PROGRESS;
    }

    @HealthConnectDataState.DataMigrationState
    public int getMigrationState() {
        String migrationState = mPreferenceHelper.getPreference(MIGRATION_STATE_PREFERENCE_KEY);
        if (Objects.isNull(migrationState)) {
            return MIGRATION_STATE_IDLE;
        }

        return Integer.parseInt(migrationState);
    }

    public void switchToSetupForUser(Context context) {
        synchronized (mLock) {
            resetMigrationStateIfNeeded(context);
            MigrationStateChangeJob.cancelAllJobs(context);
            reconcilePackageChangesWithStates(context);
            reconcileStateChangeJob(context);
        }
    }

    /** Updates the migration state. */
    public void updateMigrationState(
            Context context, @HealthConnectDataState.DataMigrationState int state) {
        synchronized (mLock) {
            updateMigrationStateGuarded(context, state, false);
        }
    }

    /**
     * Updates the migration state and the timeout reached.
     *
     * @param timeoutReached Whether the previous state has timed out.
     */
    void updateMigrationState(
            Context context,
            @HealthConnectDataState.DataMigrationState int state,
            boolean timeoutReached) {
        synchronized (mLock) {
            updateMigrationStateGuarded(context, state, timeoutReached);
        }
    }

    /**
     * Atomically updates the migration state and the timeout reached.
     *
     * @param timeoutReached Whether the previous state has timed out.
     */
    @GuardedBy("mLock")
    private void updateMigrationStateGuarded(
            Context context,
            @HealthConnectDataState.DataMigrationState int state,
            boolean timeoutReached) {

        if (state == getMigrationState()) {
            if (Constants.DEBUG) {
                Slog.d(TAG, "The new state same as the current state.");
            }
            return;
        }

        switch (state) {
            case MIGRATION_STATE_IDLE:
            case MIGRATION_STATE_APP_UPGRADE_REQUIRED:
            case MIGRATION_STATE_MODULE_UPGRADE_REQUIRED:
                MigrationStateChangeJob.cancelAllJobs(context);
                updateMigrationStatePreference(context, state, timeoutReached);
                MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserHandle);
                return;
            case MIGRATION_STATE_IN_PROGRESS:
                MigrationStateChangeJob.cancelAllJobs(context);
                updateMigrationStatePreference(
                        context, MIGRATION_STATE_IN_PROGRESS, timeoutReached);
                MigrationStateChangeJob.scheduleMigrationPauseJob(context, mUserHandle);
                updateMigrationStartsCount();
                return;
            case MIGRATION_STATE_ALLOWED:
                if (hasAllowedStateTimedOut()
                        || getStartMigrationCount()
                                >= MigrationConstants.MAX_START_MIGRATION_CALLS) {
                    updateMigrationState(context, MIGRATION_STATE_COMPLETE);
                    return;
                }
                MigrationStateChangeJob.cancelAllJobs(context);
                updateMigrationStatePreference(context, MIGRATION_STATE_ALLOWED, timeoutReached);
                MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserHandle);
                return;
            case MIGRATION_STATE_COMPLETE:
                updateMigrationStatePreference(context, MIGRATION_STATE_COMPLETE, timeoutReached);
                MigrationStateChangeJob.cancelAllJobs(context);
                return;
            default:
                throw new IllegalArgumentException(
                        "Cannot updated migration state. Unknown state: " + state);
        }
    }

    public void clearCaches(Context context) {
        synchronized (mLock) {
            updateMigrationStatePreference(context, MIGRATION_STATE_IDLE, false);
            mPreferenceHelper.insertOrReplacePreference(
                    MIGRATION_STARTS_COUNT_KEY, String.valueOf(0));
            mPreferenceHelper.removeKey(ALLOWED_STATE_START_TIME_KEY);
        }
    }

    /** Thrown when an illegal migration state is detected. */
    public static final class IllegalMigrationStateException extends Exception {
        public IllegalMigrationStateException(String message) {
            super(message);
        }
    }

    /**
     * Throws {@link IllegalMigrationStateException} if the migration can not be started in the
     * current state. If migration can be started, it will change the state to
     * MIGRATION_STATE_IN_PROGRESS
     */
    public void startMigration(Context context) throws IllegalMigrationStateException {
        synchronized (mLock) {
            validateStartMigrationGuarded();
            updateMigrationStateGuarded(context, MIGRATION_STATE_IN_PROGRESS, false);
        }
    }

    @GuardedBy("mLock")
    private void validateStartMigrationGuarded() throws IllegalMigrationStateException {
        throwIfMigrationIsComplete();
    }

    /** Returns the number of times migration has started. */
    public int getMigrationStartsCount() {
        synchronized (mLock) {
            int res =
                    Integer.parseInt(
                            Optional.ofNullable(
                                            mPreferenceHelper.getPreference(
                                                    MIGRATION_STARTS_COUNT_KEY))
                                    .orElse("0"));
            return res;
        }
    }

    /**
     * Throws {@link IllegalMigrationStateException} if the migration can not be finished in the
     * current state. If migration can be finished, it will change the state to
     * MIGRATION_STATE_COMPLETE
     */
    public void finishMigration(Context context) throws IllegalMigrationStateException {
        synchronized (mLock) {
            throwIfMigrationIsComplete();
            if (getMigrationState() != MIGRATION_STATE_IN_PROGRESS
                    && getMigrationState() != MIGRATION_STATE_ALLOWED) {
                throw new IllegalMigrationStateException("Migration is not started.");
            }
            updateMigrationStateGuarded(context, MIGRATION_STATE_COMPLETE, false);
        }
    }

    /**
     * Throws {@link IllegalMigrationStateException} if the migration can not be performed in the
     * current state.
     */
    public void validateWriteMigrationData() throws IllegalMigrationStateException {
        synchronized (mLock) {
            throwIfMigrationIsComplete();
            if (getMigrationState() != MIGRATION_STATE_IN_PROGRESS) {
                throw new IllegalMigrationStateException("Migration is not started.");
            }
        }
    }

    /**
     * Throws {@link IllegalMigrationStateException} if the sdk extension version can not be set in
     * the current state.
     */
    public void validateSetMinSdkVersion() throws IllegalMigrationStateException {
        synchronized (mLock) {
            throwIfMigrationIsComplete();
            if (getMigrationState() == MIGRATION_STATE_IN_PROGRESS) {
                throw new IllegalMigrationStateException(
                        "Cannot set the sdk extension version. Migration already in progress.");
            }
        }
    }

    void onPackageInstalledOrChanged(Context context, String packageName) {
        synchronized (mLock) {
            onPackageInstalledOrChangedGuarded(context, packageName);
        }
    }

    @GuardedBy("mLock")
    private void onPackageInstalledOrChangedGuarded(Context context, String packageName) {

        String hcMigratorPackage = getDataMigratorPackageName(context);
        if (!Objects.equals(hcMigratorPackage, packageName)) {
            return;
        }

        int migrationState = getMigrationState();
        if ((migrationState == MIGRATION_STATE_IDLE
                        || migrationState == MIGRATION_STATE_APP_UPGRADE_REQUIRED)
                && isMigrationAware(context, packageName)) {

            updateMigrationState(context, MIGRATION_STATE_ALLOWED);
            return;
        }

        if (migrationState == MIGRATION_STATE_IDLE
                && hasMigratorPackageKnownSignerSignature(context, packageName)
                && !MigrationUtils.isPackageStub(context, packageName)) {
            // apk needs to upgrade
            updateMigrationState(context, MIGRATION_STATE_APP_UPGRADE_REQUIRED);
        }

        if (migrationState == MIGRATION_STATE_ALLOWED) {
            for (StateChangedListener listener : mStateChangedListeners) {
                listener.onChanged(migrationState);
            }
        }
    }

    void onPackageRemoved(Context context, String packageName) {
        synchronized (mLock) {
            onPackageRemovedGuarded(context, packageName);
        }
    }

    @GuardedBy("mLock")
    private void onPackageRemovedGuarded(Context context, String packageName) {
        String hcMigratorPackage = getDataMigratorPackageName(context);
        if (!Objects.equals(hcMigratorPackage, packageName)) {
            return;
        }

        if (getMigrationState() != MIGRATION_STATE_COMPLETE) {
            if (Constants.DEBUG) {
                Slog.d(TAG, "Migrator package uninstalled. Marking migration complete.");
            }

            updateMigrationState(context, MIGRATION_STATE_COMPLETE);
        }
    }

    /**
     * Updates the migration state preference and the timeout reached preferences.
     *
     * @param timeoutReached Whether the previous state has timed out.
     */
    @GuardedBy("mLock")
    private void updateMigrationStatePreference(
            Context context,
            @HealthConnectDataState.DataMigrationState int migrationState,
            boolean timeoutReached) {

        @HealthConnectDataState.DataMigrationState int previousMigrationState = getMigrationState();

        HashMap<String, String> preferences =
                new HashMap<>(
                        Map.of(
                                MIGRATION_STATE_PREFERENCE_KEY,
                                String.valueOf(migrationState),
                                CURRENT_STATE_START_TIME_KEY,
                                Instant.now().toString()));

        if (migrationState == MIGRATION_STATE_IN_PROGRESS) {
            // Reset the in progress timeout key reached if we move to In Progress
            preferences.put(IN_PROGRESS_TIMEOUT_REACHED_KEY, String.valueOf(false));
        }

        if (migrationState == MIGRATION_STATE_ALLOWED && timeoutReached) {
            preferences.put(IN_PROGRESS_TIMEOUT_REACHED_KEY, String.valueOf(true));
        }

        if (migrationState == MIGRATION_STATE_COMPLETE
                && previousMigrationState == MIGRATION_STATE_IDLE
                && timeoutReached) {
            preferences.put(IDLE_TIMEOUT_REACHED_KEY, String.valueOf(true));
        }

        // If we are setting the migration state to ALLOWED for the first time.
        if (migrationState == MIGRATION_STATE_ALLOWED && Objects.isNull(getAllowedStateTimeout())) {
            preferences.put(ALLOWED_STATE_START_TIME_KEY, Instant.now().toString());
        }
        mPreferenceHelper.insertOrReplacePreferencesTransaction(preferences);

        //noinspection Convert2Lambda
        mThreadScheduler.scheduleInternalTask(
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mMigrationBroadcastScheduler.scheduleNewJobs(
                                    context, MigrationStateManager.this);
                        } catch (Exception e) {
                            Slog.e(TAG, "Migration broadcast schedule failed", e);
                        }
                    }
                });

        for (StateChangedListener listener : mStateChangedListeners) {
            listener.onChanged(migrationState);
        }
    }

    /**
     * Checks if the original {@link MIGRATION_STATE_ALLOWED} timeout period has passed. We do not
     * want to reset the ALLOWED_STATE timeout everytime state changes to this state, hence
     * persisting the original timeout time.
     */
    boolean hasAllowedStateTimedOut() {
        String allowedStateTimeout = getAllowedStateTimeout();
        if (!Objects.isNull(allowedStateTimeout)
                && Instant.now().isAfter(Instant.parse(allowedStateTimeout))) {
            Slog.e(TAG, "Allowed state period has timed out.");
            return true;
        }
        return false;
    }

    /** Checks if the IN_PROGRESS_TIMEOUT has passed. */
    boolean hasInProgressStateTimedOut() {
        synchronized (mLock) {
            String inProgressTimeoutReached =
                    mPreferenceHelper.getPreference(IN_PROGRESS_TIMEOUT_REACHED_KEY);

            if (!Objects.isNull(inProgressTimeoutReached)) {
                return Boolean.parseBoolean(inProgressTimeoutReached);
            }
            return false;
        }
    }

    /** Checks if the IDLE state has timed out. */
    boolean hasIdleStateTimedOut() {
        synchronized (mLock) {
            String idleStateTimeoutReached =
                    mPreferenceHelper.getPreference(IDLE_TIMEOUT_REACHED_KEY);

            if (!Objects.isNull(idleStateTimeoutReached)) {
                return Boolean.parseBoolean(idleStateTimeoutReached);
            }
            return false;
        }
    }

    /**
     * Reconcile migration state to the current migrator package status in case we missed a package
     * change broadcast.
     */
    @GuardedBy("mLock")
    private void reconcilePackageChangesWithStates(Context context) {
        int migrationState = getMigrationState();
        if (migrationState == MIGRATION_STATE_APP_UPGRADE_REQUIRED
                && existsMigrationAwarePackage(context)) {
            updateMigrationState(context, MIGRATION_STATE_ALLOWED);
            return;
        }

        if (migrationState == MIGRATION_STATE_IDLE) {
            if (existsMigrationAwarePackage(context)) {
                updateMigrationState(context, MIGRATION_STATE_ALLOWED);
                return;
            }

            if (existsMigratorPackage(context)
                    && !MigrationUtils.isPackageStub(
                            context, getDataMigratorPackageName(context))) {
                updateMigrationState(context, MIGRATION_STATE_APP_UPGRADE_REQUIRED);
                return;
            }
        }
        if (migrationState != MIGRATION_STATE_IDLE && migrationState != MIGRATION_STATE_COMPLETE) {
            completeMigrationIfNoMigratorPackageAvailable(context);
        }
    }

    /** Reconcile the current state with its appropriate state change job. */
    @GuardedBy("mLock")
    private void reconcileStateChangeJob(Context context) {
        switch (getMigrationState()) {
            case MIGRATION_STATE_IDLE:
            case MIGRATION_STATE_APP_UPGRADE_REQUIRED:
            case MIGRATION_STATE_ALLOWED:
                if (!MigrationStateChangeJob.existsAStateChangeJob(
                        context, MIGRATION_COMPLETE_JOB_NAME)) {
                    MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserHandle);
                }
                return;
            case MIGRATION_STATE_MODULE_UPGRADE_REQUIRED:
                handleIsUpgradeStillRequired(context);
                return;

            case MIGRATION_STATE_IN_PROGRESS:
                if (!MigrationStateChangeJob.existsAStateChangeJob(
                        context, MIGRATION_PAUSE_JOB_NAME)) {
                    MigrationStateChangeJob.scheduleMigrationPauseJob(context, mUserHandle);
                }
                return;

            case MIGRATION_STATE_COMPLETE:
                MigrationStateChangeJob.cancelAllJobs(context);
        }
    }

    /**
     * Checks if the version set by the migrator apk is the current module version and send a {@link
     * HealthConnectManager.ACTION_HEALTH_CONNECT_MIGRATION_READY intent. If not, re-sync the state
     * update job.}
     */
    @GuardedBy("mLock")
    private void handleIsUpgradeStillRequired(Context context) {
        if (Integer.parseInt(
                        mPreferenceHelper.getPreference(
                                MIN_DATA_MIGRATION_SDK_EXTENSION_VERSION_KEY))
                <= getUdcSdkExtensionVersion()) {
            updateMigrationState(context, MIGRATION_STATE_ALLOWED);
            return;
        }
        if (!MigrationStateChangeJob.existsAStateChangeJob(context, MIGRATION_COMPLETE_JOB_NAME)) {
            MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserHandle);
        }
    }

    @SuppressWarnings("NullAway")
    // TODO(b/317029272): fix this suppression
    String getAllowedStateTimeout() {
        String allowedStateStartTime =
                mPreferenceHelper.getPreference(ALLOWED_STATE_START_TIME_KEY);
        if (allowedStateStartTime != null) {
            return Instant.parse(allowedStateStartTime)
                    .plusMillis(MigrationConstants.NON_IDLE_STATE_TIMEOUT_DAYS.toMillis())
                    .toString();
        }
        return null;
    }

    private void throwIfMigrationIsComplete() throws IllegalMigrationStateException {
        if (getMigrationState() == MIGRATION_STATE_COMPLETE) {
            throw new IllegalMigrationStateException("Migration already marked complete.");
        }
    }

    /**
     * Tracks the number of times migration is started from {@link MIGRATION_STATE_ALLOWED}. If more
     * than 3 times, the migration is marked as complete
     */
    @GuardedBy("mLock")
    private void updateMigrationStartsCount() {
        String migrationStartsCount =
                Optional.ofNullable(mPreferenceHelper.getPreference(MIGRATION_STARTS_COUNT_KEY))
                        .orElse("0");

        mPreferenceHelper.insertOrReplacePreference(
                MIGRATION_STARTS_COUNT_KEY,
                String.valueOf(Integer.parseInt(migrationStartsCount) + 1));
    }

    private String getDataMigratorPackageName(Context context) {
        return context.getString(
                context.getResources().getIdentifier(HC_PACKAGE_NAME_CONFIG_NAME, null, null));
    }

    private void completeMigrationIfNoMigratorPackageAvailable(Context context) {
        if (existsMigrationAwarePackage(context)) {
            if (Constants.DEBUG) {
                Slog.d(TAG, "There is a migration aware package.");
            }
            return;
        }

        if (existsMigratorPackage(context)) {
            if (Constants.DEBUG) {
                Slog.d(TAG, "There is a package with migration known signers certificate.");
            }
            return;
        }

        if (Constants.DEBUG) {
            Slog.d(
                    TAG,
                    "There is no migration aware package or any package with migration known "
                            + "signers certificate. Marking migration as complete.");
        }
        updateMigrationState(context, MIGRATION_STATE_COMPLETE);
    }

    /** Returns whether there exists a package that is aware of migration. */
    public boolean existsMigrationAwarePackage(Context context) {
        List<String> filteredPackages =
                filterIntent(
                        context,
                        filterPermissions(context),
                        PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS);
        String dataMigratorPackageName = getDataMigratorPackageName(context);
        List<String> filteredDataMigratorPackageNames =
                filteredPackages.stream()
                        .filter(packageName -> packageName.equals(dataMigratorPackageName))
                        .toList();

        return filteredDataMigratorPackageNames.size() != 0;
    }

    /**
     * Returns whether there exists a package that is signed with the correct signatures for
     * migration.
     */
    public boolean existsMigratorPackage(Context context) {
        // Search through all packages by known signer certificate.
        List<PackageInfo> allPackages =
                context.getPackageManager()
                        .getInstalledPackages(PackageManager.GET_SIGNING_CERTIFICATES);
        String[] knownSignerCerts = getMigrationKnownSignerCertificates(context);

        for (PackageInfo packageInfo : allPackages) {
            if (hasMatchingSignatures(getPackageSignatures(packageInfo), knownSignerCerts)) {
                return true;
            }
        }
        return false;
    }

    private boolean isMigrationAware(Context context, String packageName) {
        List<String> permissionFilteredPackages = filterPermissions(context);
        List<String> filteredPackages =
                filterIntent(
                        context,
                        permissionFilteredPackages,
                        PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS);
        int numPackages = filteredPackages.size();

        if (numPackages == 0) {
            Slog.i(TAG, "There are no migration aware apps");
        } else if (numPackages == 1) {
            return Objects.equals(filteredPackages.get(0), packageName);
        }
        return false;
    }

    /** Checks whether the APK migration flag is on. */
    boolean doesMigratorHandleInfoIntent(Context context) {
        String packageName = getDataMigratorPackageName(context);
        Intent intent =
                new Intent(HealthConnectManager.ACTION_SHOW_MIGRATION_INFO).setPackage(packageName);
        PackageManager pm = context.getPackageManager();
        List<ResolveInfo> allComponents =
                pm.queryIntentActivities(
                        intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL));
        return !allComponents.isEmpty();
    }

    private static boolean hasMigratorPackageKnownSignerSignature(
            Context context, String packageName) {
        List<String> stringSignatures;
        try {
            stringSignatures =
                    getPackageSignatures(
                            context.getPackageManager()
                                    .getPackageInfo(
                                            packageName, PackageManager.GET_SIGNING_CERTIFICATES));

        } catch (PackageManager.NameNotFoundException e) {
            Slog.i(TAG, "Could not get package signatures. Package not found");
            return false;
        }

        if (stringSignatures.isEmpty()) {
            return false;
        }
        return hasMatchingSignatures(
                stringSignatures, getMigrationKnownSignerCertificates(context));
    }

    private static boolean hasMatchingSignatures(
            List<String> stringSignatures, String[] migrationKnownSignerCertificates) {

        return !Collections.disjoint(
                stringSignatures.stream().map(String::toLowerCase).toList(),
                Arrays.stream(migrationKnownSignerCertificates).map(String::toLowerCase).toList());
    }

    private static String[] getMigrationKnownSignerCertificates(Context context) {
        return context.getResources()
                .getStringArray(
                        Resources.getSystem()
                                .getIdentifier(HC_RELEASE_CERT_CONFIG_NAME, null, null));
    }

    private static List<String> getPackageSignatures(PackageInfo packageInfo) {
        return Arrays.stream(packageInfo.signingInfo.getApkContentsSigners())
                .map(signature -> MigrationUtils.computeSha256DigestBytes(signature.toByteArray()))
                .filter(signature -> signature != null)
                .toList();
    }

    private int getUdcSdkExtensionVersion() {
        return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
    }

    private int getStartMigrationCount() {
        return Integer.parseInt(
                Optional.ofNullable(mPreferenceHelper.getPreference(MIGRATION_STARTS_COUNT_KEY))
                        .orElse("0"));
    }

    /**
     * Resets migration state to IDLE state for early users whose migration might have timed out
     * before they migrate data.
     */
    void resetMigrationStateIfNeeded(Context context) {

        if (!Boolean.parseBoolean(mPreferenceHelper.getPreference(HAVE_RESET_MIGRATION_STATE_KEY))
                && hasMigrationTimedOutPrematurely()) {
            updateMigrationState(context, MIGRATION_STATE_IDLE);
            mPreferenceHelper.insertOrReplacePreference(
                    HAVE_RESET_MIGRATION_STATE_KEY, String.valueOf(true));
        }
    }

    private boolean hasMigrationTimedOutPrematurely() {
        String currentStateStartTime =
                mPreferenceHelper.getPreference(CURRENT_STATE_START_TIME_KEY);

        if (!Objects.isNull(currentStateStartTime)) {
            return getMigrationState() == MIGRATION_STATE_COMPLETE
                    && LocalDate.ofInstant(Instant.parse(currentStateStartTime), ZoneOffset.MIN)
                            .isBefore(PREMATURE_MIGRATION_TIMEOUT_DATE);
        }
        return false;
    }

    /**
     * A listener for observing migration state changes.
     *
     * @see MigrationStateManager#addStateChangedListener(StateChangedListener)
     */
    public interface StateChangedListener {

        /**
         * Called on every migration state change.
         *
         * @param state the new migration state.
         */
        void onChanged(@HealthConnectDataState.DataMigrationState int state);
    }
}
