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

package com.android.server.pm;

import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;

import static com.android.internal.pm.pkg.parsing.ParsingPackageUtils.PARSE_APK_IN_APEX;
import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_DATA_APP_AVG_SCAN_TIME;
import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_SYSTEM_APP_AVG_SCAN_TIME;
import static com.android.server.pm.PackageManagerService.SCAN_AS_APK_IN_APEX;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
import static com.android.server.pm.PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE;
import static com.android.server.pm.PackageManagerService.SCAN_INITIAL;
import static com.android.server.pm.PackageManagerService.SCAN_NO_DEX;
import static com.android.server.pm.PackageManagerService.SCAN_REQUIRE_KNOWN;
import static com.android.server.pm.PackageManagerService.SYSTEM_PARTITIONS;
import static com.android.server.pm.PackageManagerService.TAG;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Environment;
import android.os.SystemClock;
import android.os.Trace;
import android.system.ErrnoException;
import android.system.Os;
import android.util.ArrayMap;
import android.util.EventLog;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.om.OverlayConfig;
import com.android.internal.pm.parsing.PackageParser2;
import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.EventLogTags;
import com.android.server.pm.parsing.PackageCacher;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.utils.WatchedArrayMap;

import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;

/**
 * Part of PackageManagerService that handles init and system packages. This class still needs
 * further cleanup and eventually all the installation/scanning related logic will go to another
 * class.
 */
final class InitAppsHelper {
    private final PackageManagerService mPm;
    private final List<ScanPartition> mDirsToScanAsSystem;
    private final int mScanFlags;
    private final int mSystemParseFlags;
    private final int mSystemScanFlags;
    private final InstallPackageHelper mInstallPackageHelper;
    private final ApexManager mApexManager;
    private final ExecutorService mExecutorService;
    /* Tracks how long system scan took */
    private long mSystemScanTime;
    /* Track of the number of cached system apps */
    private int mCachedSystemApps;
    /* Track of the number of system apps */
    private int mSystemPackagesCount;
    private final boolean mIsDeviceUpgrading;
    private final List<ScanPartition> mSystemPartitions;

    /**
     * Tracks new system packages [received in an OTA] that we expect to
     * find updated user-installed versions. Keys are package name, values
     * are package location.
     */
    private final ArrayMap<String, File> mExpectingBetter = new ArrayMap<>();
    /* Tracks of any system packages that no longer exist that needs to be pruned. */
    private final List<String> mPossiblyDeletedUpdatedSystemApps = new ArrayList<>();
    // Tracks of stub packages that must either be replaced with full versions in the /data
    // partition or be disabled.
    private final List<String> mStubSystemApps = new ArrayList<>();

    // TODO(b/198166813): remove PMS dependency
    InitAppsHelper(PackageManagerService pm, ApexManager apexManager,
            InstallPackageHelper installPackageHelper,
            List<ScanPartition> systemPartitions) {
        mPm = pm;
        mApexManager = apexManager;
        mInstallPackageHelper = installPackageHelper;
        mSystemPartitions = systemPartitions;
        mDirsToScanAsSystem = getSystemScanPartitions();
        mIsDeviceUpgrading = mPm.isDeviceUpgrading();
        // Set flag to monitor and not change apk file paths when scanning install directories.
        int scanFlags = SCAN_BOOTING | SCAN_INITIAL;
        if (mIsDeviceUpgrading || mPm.isFirstBoot()) {
            mScanFlags = scanFlags | SCAN_FIRST_BOOT_OR_UPGRADE;
        } else {
            mScanFlags = scanFlags;
        }
        mSystemParseFlags = mPm.getDefParseFlags() | ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
        mSystemScanFlags = mScanFlags | SCAN_AS_SYSTEM;
        mExecutorService = ParallelPackageParser.makeExecutorService();
    }

    private List<ScanPartition> getSystemScanPartitions() {
        final List<ScanPartition> scanPartitions = new ArrayList<>();
        scanPartitions.addAll(mSystemPartitions);
        scanPartitions.addAll(getApexScanPartitions());
        Slog.d(TAG, "Directories scanned as system partitions: " + scanPartitions);
        return scanPartitions;
    }

    private List<ScanPartition> getApexScanPartitions() {
        final List<ScanPartition> scanPartitions = new ArrayList<>();
        final List<ApexManager.ActiveApexInfo> activeApexInfos = mApexManager.getActiveApexInfos();
        for (int i = 0; i < activeApexInfos.size(); i++) {
            final ScanPartition scanPartition = resolveApexToScanPartition(activeApexInfos.get(i));
            if (scanPartition != null) {
                scanPartitions.add(scanPartition);
            }
        }
        return scanPartitions;
    }

    private static @Nullable ScanPartition resolveApexToScanPartition(
            ApexManager.ActiveApexInfo apexInfo) {
        for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
            ScanPartition sp = SYSTEM_PARTITIONS.get(i);
            if (apexInfo.preInstalledApexPath.getAbsolutePath().equals(
                    sp.getFolder().getAbsolutePath())
                    || apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
                    sp.getFolder().getAbsolutePath() + File.separator)) {
                return new ScanPartition(apexInfo.apexDirectory, sp, apexInfo);
            }
        }
        return null;
    }

    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private List<ApexManager.ScanResult> scanApexPackagesTraced(PackageParser2 packageParser) {
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanApexPackages");

        try {
            return mInstallPackageHelper.scanApexPackages(mApexManager.getAllApexInfos(),
                    mSystemParseFlags, mSystemScanFlags, packageParser, mExecutorService);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    }

    /**
     * Install apps from system dirs.
     */
    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    public OverlayConfig initSystemApps(PackageParser2 packageParser,
            WatchedArrayMap<String, PackageSetting> packageSettings,
            int[] userIds, long startTime) {
        // Prepare apex package info before scanning APKs, this information is needed when
        // scanning apk in apex.
        final List<ApexManager.ScanResult> apexScanResults = scanApexPackagesTraced(packageParser);
        mApexManager.notifyScanResult(apexScanResults);

        scanSystemDirs(packageParser, mExecutorService);

        // Parse overlay configuration files to set default enable state, mutability, and
        // priority of system overlays.
        final ArrayMap<String, File> apkInApexPreInstalledPaths = new ArrayMap<>();
        for (ApexManager.ActiveApexInfo apexInfo : mApexManager.getActiveApexInfos()) {
            final String apexPackageName = mApexManager.getActivePackageNameForApexModuleName(
                    apexInfo.apexModuleName);
            for (String packageName : mApexManager.getApksInApex(apexPackageName)) {
                apkInApexPreInstalledPaths.put(packageName, apexInfo.preInstalledApexPath);
            }
        }
        final OverlayConfig overlayConfig = OverlayConfig.initializeSystemInstance(
                consumer -> mPm.forEachPackageState(mPm.snapshotComputer(),
                        packageState -> {
                            var pkg = packageState.getPkg();
                            if (pkg != null) {
                                consumer.accept(pkg, packageState.isSystem(),
                                        apkInApexPreInstalledPaths.get(pkg.getPackageName()));
                            }
                        }));

        // do this first before mucking with mPackages for the "expecting better" case
        updateStubSystemAppsList(mStubSystemApps);
        mInstallPackageHelper.prepareSystemPackageCleanUp(packageSettings,
                mPossiblyDeletedUpdatedSystemApps, mExpectingBetter, userIds);

        logSystemAppsScanningTime(startTime);
        return overlayConfig;
    }

    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void logSystemAppsScanningTime(long startTime) {
        mCachedSystemApps = PackageCacher.sCachedPackageReadCount.get();

        // Remove any shared userIDs that have no associated packages
        mPm.mSettings.pruneSharedUsersLPw();
        mSystemScanTime = SystemClock.uptimeMillis() - startTime;
        mSystemPackagesCount = mPm.mPackages.size();
        Slog.i(TAG, "Finished scanning system apps. Time: " + mSystemScanTime
                + " ms, packageCount: " + mSystemPackagesCount
                + " , timePerPackage: "
                + (mSystemPackagesCount == 0 ? 0 : mSystemScanTime / mSystemPackagesCount)
                + " , cached: " + mCachedSystemApps);
        if (mIsDeviceUpgrading && mSystemPackagesCount > 0) {
            //CHECKSTYLE:OFF IndentationCheck
            FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_DURATION_REPORTED,
                    BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_SYSTEM_APP_AVG_SCAN_TIME,
                    mSystemScanTime / mSystemPackagesCount);
            //CHECKSTYLE:ON IndentationCheck
        }
    }

    /**
     * Fix up the previously-installed app directory mode - they can't be readable by non-system
     * users to prevent them from listing the dir to discover installed package names.
     */
    void fixInstalledAppDirMode() {
        try (var files = Files.newDirectoryStream(mPm.getAppInstallDir().toPath())) {
            files.forEach(dir -> {
                try {
                    Os.chmod(dir.toString(), 0771);
                } catch (ErrnoException e) {
                    Slog.w(TAG, "Failed to fix an installed app dir mode", e);
                }
            });
        } catch (Exception e) {
            Slog.w(TAG, "Failed to walk the app install directory to fix the modes", e);
        }
    }

    /**
     * Install apps/updates from data dir and fix system apps that are affected.
     */
    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    public void initNonSystemApps(PackageParser2 packageParser, @NonNull int[] userIds,
            long startTime) {
        EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
                SystemClock.uptimeMillis());

        if ((mScanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) == SCAN_FIRST_BOOT_OR_UPGRADE) {
            fixInstalledAppDirMode();
        }

        scanDirTracedLI(mPm.getAppInstallDir(), 0,
                mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService, null);

        List<Runnable> unfinishedTasks = mExecutorService.shutdownNow();
        if (!unfinishedTasks.isEmpty()) {
            throw new IllegalStateException("Not all tasks finished before calling close: "
                    + unfinishedTasks);
        }
        fixSystemPackages(userIds);
        logNonSystemAppScanningTime(startTime);
        mExpectingBetter.clear();
        mPm.mSettings.pruneRenamedPackagesLPw();
    }

    /**
     * Clean up system packages now that some system package updates have been installed from
     * the data dir. Also install system stub packages as the last step.
     */
    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void fixSystemPackages(@NonNull int[] userIds) {
        mInstallPackageHelper.cleanupDisabledPackageSettings(mPossiblyDeletedUpdatedSystemApps,
                userIds, mScanFlags);
        mInstallPackageHelper.checkExistingBetterPackages(mExpectingBetter,
                mStubSystemApps, mSystemScanFlags, mSystemParseFlags);

        // Uncompress and install any stubbed system applications.
        // This must be done last to ensure all stubs are replaced or disabled.
        mInstallPackageHelper.installSystemStubPackages(mStubSystemApps, mScanFlags);
    }

    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void logNonSystemAppScanningTime(long startTime) {
        final int cachedNonSystemApps = PackageCacher.sCachedPackageReadCount.get()
                - mCachedSystemApps;

        final long dataScanTime = SystemClock.uptimeMillis() - mSystemScanTime - startTime;
        final int dataPackagesCount = mPm.mPackages.size() - mSystemPackagesCount;
        Slog.i(TAG, "Finished scanning non-system apps. Time: " + dataScanTime
                + " ms, packageCount: " + dataPackagesCount
                + " , timePerPackage: "
                + (dataPackagesCount == 0 ? 0 : dataScanTime / dataPackagesCount)
                + " , cached: " + cachedNonSystemApps);
        if (mIsDeviceUpgrading && dataPackagesCount > 0) {
            //CHECKSTYLE:OFF IndentationCheck
            FrameworkStatsLog.write(
                    FrameworkStatsLog.BOOT_TIME_EVENT_DURATION_REPORTED,
                    BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_DATA_APP_AVG_SCAN_TIME,
                    dataScanTime / dataPackagesCount);
            //CHECKSTYLE:OFF IndentationCheck
        }
    }

    static class ScanParams {
        public final @Nullable File scanDir;
        public final int parseFlags;
        public final int scanFlags;
        public final @Nullable ApexManager.ActiveApexInfo apexInfo;

        private ScanParams(File scanDir, int parseFlags, int scanFlags,
                ApexManager.ActiveApexInfo apexInfo) {
            this.scanDir = scanDir;
            this.parseFlags = parseFlags;
            this.scanFlags = scanFlags;
            this.apexInfo = apexInfo;
        }

        public static ScanParams forApexDirScan(int parseFlags, int scanFlags) {
            return new ScanParams(null, parseFlags, scanFlags, null);
        }

        public static ScanParams forApkPartitionScan(File scanDir, int parseFlags, int scanFlags) {
            return new ScanParams(scanDir, parseFlags, scanFlags, null);
        }

        public static ScanParams forApkInApexScan(File scanDir, int parseFlags, int scanFlags,
                ApexManager.ActiveApexInfo apexInfo) {
            // When scanning apk in apexes, we want to check the maxSdkVersion.
            return new ScanParams(scanDir, parseFlags | PARSE_APK_IN_APEX, scanFlags, apexInfo);
        }
    }

    // Helper function for collecting input scan parameters to be processed in parallel later.
    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void collectScanParams(List<ScanParams> scanParamsList, File scanDir,
            int parseFlags, int scanFlags, PackageParser2 packageParser,
            ExecutorService executorService, ApexManager.ActiveApexInfo apexInfo) {
        scanParamsList.add((scanFlags & SCAN_AS_APK_IN_APEX) != 0
                ? ScanParams.forApkInApexScan(scanDir, parseFlags, scanFlags, apexInfo)
                : ScanParams.forApkPartitionScan(scanDir, parseFlags, scanFlags));
    }

    /**
     * First part of init dir scanning
     */
    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void scanSystemDirs(PackageParser2 packageParser, ExecutorService executorService) {
        File frameworkDir = new File(Environment.getRootDirectory(), "framework");
        List<ScanParams> scanParamsList = new ArrayList<>();

        // Collect vendor/product/system_ext overlay packages. (Do this before scanning
        // any apps.)
        // For security and version matching reason, only consider overlay packages if they
        // reside in the right directory.
        for (int i = mDirsToScanAsSystem.size() - 1; i >= 0; i--) {
            final ScanPartition partition = mDirsToScanAsSystem.get(i);
            if (partition.getOverlayFolder() == null) {
                continue;
            }
            collectScanParams(scanParamsList, partition.getOverlayFolder(),
                      mSystemParseFlags, mSystemScanFlags | partition.scanFlag, packageParser,
                      executorService, partition.apexInfo);
        }
        collectScanParams(scanParamsList, frameworkDir, mSystemParseFlags,
                  mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED, packageParser,
                  executorService, null);

        for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
            final ScanPartition partition = mDirsToScanAsSystem.get(i);
            if (partition.getPrivAppFolder() != null) {
                collectScanParams(scanParamsList, partition.getPrivAppFolder(),
                        mSystemParseFlags,
                        mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag,
                        packageParser, executorService, partition.apexInfo);
            }
            collectScanParams(scanParamsList, partition.getAppFolder(), mSystemParseFlags,
                    mSystemScanFlags | partition.scanFlag, packageParser, executorService,
                    partition.apexInfo);
        }

        // Scan all directories with the parameters contained in scanParamsList.
        parallelScanDirTracedLI(scanParamsList, packageParser, executorService);

        if (!mPm.mPackages.containsKey("android")) {
            throw new IllegalStateException(
                    "Failed to load frameworks package; check log for warnings");
        }
    }

    @GuardedBy("mPm.mLock")
    private void updateStubSystemAppsList(List<String> stubSystemApps) {
        final int numPackages = mPm.mPackages.size();
        for (int index = 0; index < numPackages; index++) {
            final AndroidPackage pkg = mPm.mPackages.valueAt(index);
            if (pkg.isStub()) {
                stubSystemApps.add(pkg.getPackageName());
            }
        }
    }

    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void scanDirTracedLI(File scanDir, int parseFlags, int scanFlags,
            PackageParser2 packageParser, ExecutorService executorService,
            @Nullable ApexManager.ActiveApexInfo apexInfo) {
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]");
        try {
            mInstallPackageHelper.installPackagesFromDir(scanDir, parseFlags,
                    scanFlags, packageParser, executorService, apexInfo);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    }

    @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
    private void parallelScanDirTracedLI(List<ScanParams> scanParamsList,
            PackageParser2 packageParser, ExecutorService executorService) {
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallelScanDir");
        try {
            mInstallPackageHelper.parallelInstallPackagesFromDirs(scanParamsList,
                    packageParser, executorService);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    }

    public boolean isExpectingBetter(String packageName) {
        return mExpectingBetter.containsKey(packageName);
    }

    public List<ScanPartition> getDirsToScanAsSystem() {
        return mDirsToScanAsSystem;
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public int getSystemScanFlags() {
        return mSystemScanFlags;
    }
}
