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

package com.android.server.art;

import static android.os.ParcelFileDescriptor.AutoCloseInputStream;

import static com.android.server.art.ArtManagerLocal.SnapshotProfileException;
import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
import static com.android.server.art.model.ArtFlags.DexoptFlags;
import static com.android.server.art.model.ArtFlags.PriorityClassApi;
import static com.android.server.art.model.DexoptResult.DexContainerFileDexoptResult;
import static com.android.server.art.model.DexoptResult.DexoptResultStatus;
import static com.android.server.art.model.DexoptResult.PackageDexoptResult;
import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.internal.annotations.GuardedBy;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.DeleteResult;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
import com.android.server.art.model.DexoptStatus;
import com.android.server.art.model.OperationProgress;
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;

import libcore.io.Streams;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

/**
 * This class handles ART shell commands.
 *
 * @hide
 */
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public final class ArtShellCommand extends BasicShellCommandHandler {
    private static final String TAG = ArtManagerLocal.TAG;

    /** The default location for profile dumps. */
    private final static String PROFILE_DEBUG_LOCATION = "/data/misc/profman";

    private final ArtManagerLocal mArtManagerLocal;
    private final PackageManagerLocal mPackageManagerLocal;

    @GuardedBy("sCancellationSignalMap")
    @NonNull
    private static final Map<String, CancellationSignal> sCancellationSignalMap = new HashMap<>();

    public ArtShellCommand(@NonNull ArtManagerLocal artManagerLocal,
            @NonNull PackageManagerLocal packageManagerLocal) {
        mArtManagerLocal = artManagerLocal;
        mPackageManagerLocal = packageManagerLocal;
    }

    @Override
    public int onCommand(String cmd) {
        // Apps shouldn't call ART Service shell commands, not even for dexopting themselves.
        enforceRootOrShell();
        PrintWriter pw = getOutPrintWriter();
        try (var snapshot = mPackageManagerLocal.withFilteredSnapshot()) {
            switch (cmd) {
                case "compile":
                    return handleCompile(pw, snapshot);
                case "reconcile-secondary-dex-files":
                    pw.println("Warning: 'pm reconcile-secondary-dex-files' is deprecated. It is "
                            + "now doing nothing");
                    return 0;
                case "force-dex-opt":
                    return handleForceDexopt(pw, snapshot);
                case "bg-dexopt-job":
                    return handleBgDexoptJob(pw, snapshot);
                case "cancel-bg-dexopt-job":
                    pw.println("Warning: 'pm cancel-bg-dexopt-job' is deprecated. It is now an "
                            + "alias of 'pm bg-dexopt-job --cancel'");
                    return handleCancelBgDexoptJob(pw);
                case "delete-dexopt":
                    return handleDeleteDexopt(pw, snapshot);
                case "dump-profiles":
                    return handleDumpProfile(pw, snapshot);
                case "snapshot-profile":
                    return handleSnapshotProfile(pw, snapshot);
                case "art":
                    return handleArtCommand(pw, snapshot);
                default:
                    // Can't happen. Only supported commands are forwarded to ART Service.
                    throw new IllegalArgumentException(
                            String.format("Unexpected command '%s' forwarded to ART Service", cmd));
            }
        } catch (IllegalArgumentException | SnapshotProfileException e) {
            pw.println("Error: " + e.getMessage());
            return 1;
        }
    }

    private int handleArtCommand(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        String subcmd = getNextArgRequired();
        switch (subcmd) {
            case "dexopt-packages": {
                return handleBatchDexopt(pw, snapshot);
            }
            case "cancel": {
                String jobId = getNextArgRequired();
                CancellationSignal signal;
                synchronized (sCancellationSignalMap) {
                    signal = sCancellationSignalMap.getOrDefault(jobId, null);
                }
                if (signal == null) {
                    pw.println("Job not found");
                    return 1;
                }
                signal.cancel();
                pw.println("Job cancelled");
                return 0;
            }
            case "dump": {
                String packageName = getNextArg();
                if (packageName != null) {
                    mArtManagerLocal.dumpPackage(pw, snapshot, packageName);
                } else {
                    mArtManagerLocal.dump(pw, snapshot);
                }
                return 0;
            }
            case "cleanup": {
                return handleCleanup(pw, snapshot);
            }
            case "clear-app-profiles": {
                mArtManagerLocal.clearAppProfiles(snapshot, getNextArgRequired());
                pw.println("Profiles cleared");
                return 0;
            }
            default:
                pw.printf("Error: Unknown 'art' sub-command '%s'\n", subcmd);
                pw.println("See 'pm help' for help");
                return 1;
        }
    }

    private int handleCompile(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        @DexoptFlags int scopeFlags = 0;
        String reason = null;
        String compilerFilter = null;
        @PriorityClassApi int priorityClass = ArtFlags.PRIORITY_NONE;
        String splitArg = null;
        boolean force = false;
        boolean reset = false;
        boolean forAllPackages = false;
        boolean legacyClearProfile = false;
        boolean verbose = false;

        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-a":
                    forAllPackages = true;
                    break;
                case "-r":
                    reason = getNextArgRequired();
                    break;
                case "-m":
                    compilerFilter = getNextArgRequired();
                    break;
                case "-p":
                    priorityClass = parsePriorityClass(getNextArgRequired());
                    break;
                case "-f":
                    force = true;
                    break;
                case "--primary-dex":
                    scopeFlags |= ArtFlags.FLAG_FOR_PRIMARY_DEX;
                    break;
                case "--secondary-dex":
                    scopeFlags |= ArtFlags.FLAG_FOR_SECONDARY_DEX;
                    break;
                case "--include-dependencies":
                    scopeFlags |= ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
                    break;
                case "--full":
                    scopeFlags |= ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
                            | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
                    break;
                case "--split":
                    splitArg = getNextArgRequired();
                    break;
                case "--reset":
                    reset = true;
                    break;
                case "-c":
                    pw.println("Warning: Flag '-c' is deprecated and usually produces undesired "
                            + "results. Please use one of the following commands instead.");
                    pw.println("- To clear the local profiles only, use "
                            + "'pm art clear-app-profiles PACKAGE_NAME'. (The existing dexopt "
                            + "artifacts will be kept, even if they are derived from the "
                            + "profiles.)");
                    pw.println("- To clear the local profiles and also clear the dexopt artifacts "
                            + "that are derived from them, use 'pm compile --reset PACKAGE_NAME'. "
                            + "(The package will be reset to the initial state as if it's newly "
                            + "installed, which means the package will be re-dexopted if "
                            + "necessary, and cloud profiles will be used if exist.)");
                    pw.println("- To re-dexopt the package with no profile, use "
                            + "'pm compile -m verify -f PACKAGE_NAME'. (The local profiles "
                            + "will be kept but not used during the dexopt. The dexopt artifacts "
                            + "are guaranteed to have no compiled code.)");
                    legacyClearProfile = true;
                    break;
                case "--check-prof":
                    getNextArgRequired();
                    pw.println("Warning: Ignoring obsolete flag '--check-prof'. It is "
                            + "unconditionally enabled now");
                    break;
                case "-v":
                    verbose = true;
                    break;
                default:
                    pw.println("Error: Unknown option: " + opt);
                    return 1;
            }
        }

        List<String> packageNames = forAllPackages
                ? List.copyOf(snapshot.getPackageStates().keySet())
                : List.of(getNextArgRequired());

        var paramsBuilder = new DexoptParams.Builder(ReasonMapping.REASON_CMDLINE);
        if (reason != null) {
            if (reason.equals(ReasonMapping.REASON_INACTIVE)) {
                pw.println("Warning: '-r inactive' produces undesired results.");
            }
            if (compilerFilter == null) {
                paramsBuilder.setCompilerFilter(ReasonMapping.getCompilerFilterForReason(reason));
            }
            if (priorityClass == ArtFlags.PRIORITY_NONE) {
                paramsBuilder.setPriorityClass(ReasonMapping.getPriorityClassForReason(reason));
            }
        }
        if (compilerFilter != null) {
            paramsBuilder.setCompilerFilter(compilerFilter);
        }
        if (priorityClass != ArtFlags.PRIORITY_NONE) {
            paramsBuilder.setPriorityClass(priorityClass);
        }
        if (force) {
            paramsBuilder.setFlags(ArtFlags.FLAG_FORCE, ArtFlags.FLAG_FORCE);
        }
        if (splitArg != null) {
            if (scopeFlags != 0) {
                pw.println("Error: '--primary-dex', '--secondary-dex', "
                        + "'--include-dependencies', or '--full' must not be set when '--split' "
                        + "is set.");
                return 1;
            }
            if (forAllPackages) {
                pw.println("Error:  '-a' cannot be specified together with '--split'");
                return 1;
            }
            scopeFlags = ArtFlags.FLAG_FOR_PRIMARY_DEX;
            paramsBuilder.setFlags(ArtFlags.FLAG_FOR_SINGLE_SPLIT, ArtFlags.FLAG_FOR_SINGLE_SPLIT)
                    .setSplitName(getSplitName(pw, snapshot, packageNames.get(0), splitArg));
        }
        if (scopeFlags != 0) {
            paramsBuilder.setFlags(scopeFlags,
                    ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
                            | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
        } else {
            paramsBuilder.setFlags(
                    ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
                    ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
                            | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
        }
        if (forAllPackages) {
            // We'll iterate over all packages anyway.
            paramsBuilder.setFlags(0, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES);
        }

        if (reset) {
            return resetPackages(pw, snapshot, packageNames, verbose);
        } else {
            if (legacyClearProfile) {
                // For compat only. Combining this with dexopt usually produces in undesired
                // results.
                for (String packageName : packageNames) {
                    mArtManagerLocal.clearAppProfiles(snapshot, packageName);
                }
            }
            return dexoptPackages(pw, snapshot, packageNames, paramsBuilder.build(), verbose);
        }
    }

    private int handleForceDexopt(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        pw.println("Warning: 'pm force-dex-opt' is deprecated. Please use 'pm compile "
                + "-f PACKAGE_NAME' instead");
        return dexoptPackages(pw, snapshot, List.of(getNextArgRequired()),
                new DexoptParams.Builder(ReasonMapping.REASON_CMDLINE)
                        .setFlags(ArtFlags.FLAG_FORCE, ArtFlags.FLAG_FORCE)
                        .setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX
                                        | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
                                ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX
                                        | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
                        .build(),
                false /* verbose */);
    }

    private int handleBgDexoptJob(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        String opt = getNextOption();
        if (opt == null) {
            List<String> packageNames = new ArrayList<>();
            String arg;
            while ((arg = getNextArg()) != null) {
                packageNames.add(arg);
            }
            if (!packageNames.isEmpty()) {
                pw.println("Warning: Running 'pm bg-dexopt-job' with package names is deprecated. "
                        + "Please use 'pm compile -r bg-dexopt PACKAGE_NAME' instead");
                return dexoptPackages(pw, snapshot, packageNames,
                        new DexoptParams.Builder(ReasonMapping.REASON_BG_DEXOPT).build(),
                        false /* verbose */);
            }

            CompletableFuture<BackgroundDexoptJob.Result> runningJob =
                    mArtManagerLocal.getRunningBackgroundDexoptJob();
            if (runningJob != null) {
                pw.println("Another job already running. Waiting for it to finish... To cancel it, "
                        + "run 'pm bg-dexopt-job --cancel'. in a separate shell.");
                pw.flush();
                Utils.getFuture(runningJob);
            }
            CompletableFuture<BackgroundDexoptJob.Result> future =
                    mArtManagerLocal.startBackgroundDexoptJobAndReturnFuture();
            pw.println("Job running...  To cancel it, run 'pm bg-dexopt-job --cancel'. in a "
                    + "separate shell.");
            pw.flush();
            BackgroundDexoptJob.Result result = Utils.getFuture(future);
            if (result instanceof BackgroundDexoptJob.CompletedResult) {
                var completedResult = (BackgroundDexoptJob.CompletedResult) result;
                if (completedResult.dexoptResult().getFinalStatus()
                        == DexoptResult.DEXOPT_CANCELLED) {
                    pw.println("Job cancelled. See logs for details");
                } else {
                    pw.println("Job finished. See logs for details");
                }
            } else if (result instanceof BackgroundDexoptJob.FatalErrorResult) {
                // Never expected.
                pw.println("Job encountered a fatal error");
            }
            return 0;
        }
        switch (opt) {
            case "--cancel": {
                return handleCancelBgDexoptJob(pw);
            }
            case "--enable": {
                // This operation requires the uid to be "system" (1000).
                long identityToken = Binder.clearCallingIdentity();
                try {
                    mArtManagerLocal.scheduleBackgroundDexoptJob();
                } finally {
                    Binder.restoreCallingIdentity(identityToken);
                }
                pw.println("Background dexopt job enabled");
                return 0;
            }
            case "--disable": {
                // This operation requires the uid to be "system" (1000).
                long identityToken = Binder.clearCallingIdentity();
                try {
                    mArtManagerLocal.unscheduleBackgroundDexoptJob();
                } finally {
                    Binder.restoreCallingIdentity(identityToken);
                }
                pw.println("Background dexopt job disabled");
                return 0;
            }
            default:
                pw.println("Error: Unknown option: " + opt);
                return 1;
        }
    }

    private int handleCancelBgDexoptJob(@NonNull PrintWriter pw) {
        mArtManagerLocal.cancelBackgroundDexoptJob();
        pw.println("Background dexopt job cancelled");
        return 0;
    }

    private int handleCleanup(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        long freedBytes = mArtManagerLocal.cleanup(snapshot);
        pw.printf("Freed %d bytes\n", freedBytes);
        return 0;
    }

    private int handleDeleteDexopt(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        DeleteResult result =
                mArtManagerLocal.deleteDexoptArtifacts(snapshot, getNextArgRequired());
        pw.printf("Freed %d bytes\n", result.getFreedBytes());
        return 0;
    }

    private int handleSnapshotProfile(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
            throws SnapshotProfileException {
        String splitName = null;
        String codePath = null;

        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--split":
                    splitName = getNextArgRequired();
                    break;
                default:
                    pw.println("Error: Unknown option: " + opt);
                    return 1;
            }
        }

        String packageName = getNextArgRequired();

        if ("--code-path".equals(getNextOption())) {
            pw.println("Warning: Specifying a split using '--code-path' is deprecated. Please use "
                    + "'--split SPLIT_NAME' instead");
            pw.println("Tip: '--split SPLIT_NAME' must be passed before the package name");
            codePath = getNextArgRequired();
        }

        if (splitName != null && codePath != null) {
            pw.println("Error: '--split' and '--code-path' cannot be both specified");
            return 1;
        }

        if (packageName.equals(Utils.PLATFORM_PACKAGE_NAME)) {
            if (splitName != null) {
                pw.println("Error: '--split' must not be specified for boot image profile");
                return 1;
            }
            if (codePath != null) {
                pw.println("Error: '--code-path' must not be specified for boot image profile");
                return 1;
            }
            return handleSnapshotBootProfile(pw, snapshot);
        }

        if (splitName != null && splitName.isEmpty()) {
            splitName = null;
        }
        if (codePath != null) {
            splitName = getSplitNameByFullPath(snapshot, packageName, codePath);
        }

        return handleSnapshotAppProfile(pw, snapshot, packageName, splitName);
    }

    private int handleSnapshotBootProfile(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
            throws SnapshotProfileException {
        String outputRelativePath = "android.prof";
        ParcelFileDescriptor fd = mArtManagerLocal.snapshotBootImageProfile(snapshot);
        writeProfileFdContentsToFile(pw, fd, outputRelativePath);
        return 0;
    }

    private int handleSnapshotAppProfile(@NonNull PrintWriter pw,
            @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
            @Nullable String splitName) throws SnapshotProfileException {
        String outputRelativePath = String.format("%s%s.prof", packageName,
                splitName != null ? String.format("-split_%s.apk", splitName) : "");
        ParcelFileDescriptor fd =
                mArtManagerLocal.snapshotAppProfile(snapshot, packageName, splitName);
        writeProfileFdContentsToFile(pw, fd, outputRelativePath);
        return 0;
    }

    private int handleDumpProfile(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
            throws SnapshotProfileException {
        boolean dumpClassesAndMethods = false;

        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--dump-classes-and-methods": {
                    dumpClassesAndMethods = true;
                    break;
                }
                default:
                    pw.println("Error: Unknown option: " + opt);
                    return 1;
            }
        }

        String packageName = getNextArgRequired();

        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
        try (var tracing = new Utils.Tracing("dump profiles")) {
            for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) {
                if (!dexInfo.hasCode()) {
                    continue;
                }
                String profileName = PrimaryDexUtils.getProfileName(dexInfo.splitName());
                // The path is intentionally inconsistent with the one for "snapshot-profile". This
                // is to match the behavior of the legacy PM shell command.
                String outputRelativePath =
                        String.format("%s-%s.prof.txt", packageName, profileName);
                ParcelFileDescriptor fd = mArtManagerLocal.dumpAppProfile(
                        snapshot, packageName, dexInfo.splitName(), dumpClassesAndMethods);
                writeProfileFdContentsToFile(pw, fd, outputRelativePath);
            }
        }
        return 0;
    }

    private int handleBatchDexopt(
            @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
        String reason = null;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-r":
                    reason = getNextArgRequired();
                    break;
                default:
                    pw.println("Error: Unknown option: " + opt);
                    return 1;
            }
        }
        if (reason == null) {
            pw.println("Error: '-r REASON' is required");
            return 1;
        }
        if (!ReasonMapping.BATCH_DEXOPT_REASONS.contains(reason)) {
            pw.printf("Error: Invalid batch dexopt reason '%s'. Valid values are: %s\n", reason,
                    ReasonMapping.BATCH_DEXOPT_REASONS);
            return 1;
        }
        DexoptResult result;
        ExecutorService progressCallbackExecutor = Executors.newSingleThreadExecutor();
        try (var signal = new WithCancellationSignal(pw, true /* verbose */)) {
            result = mArtManagerLocal.dexoptPackages(
                    snapshot, reason, signal.get(), progressCallbackExecutor, progress -> {
                        pw.println(String.format("Dexopting apps: %d%%", progress.getPercentage()));
                        pw.flush();
                    });
            Utils.executeAndWait(progressCallbackExecutor, () -> {
                printDexoptResult(pw, result, true /* verbose */, true /* multiPackage */);
            });
        } finally {
            progressCallbackExecutor.shutdown();
        }
        return 0;
    }

    @Override
    public void onHelp() {
        // No one should call this. The help text should be printed by the `onHelp` handler of `cmd
        // package`.
        throw new UnsupportedOperationException("Unexpected call to 'onHelp'");
    }

    public static void printHelp(@NonNull PrintWriter pw) {
        pw.println("compile [-r COMPILATION_REASON] [-m COMPILER_FILTER] [-p PRIORITY] [-f]");
        pw.println("    [--primary-dex] [--secondary-dex] [--include-dependencies] [--full]");
        pw.println("    [--split SPLIT_NAME] [--reset] [-a | PACKAGE_NAME]");
        pw.println("  Dexopt a package or all packages.");
        pw.println("  Options:");
        pw.println("    -a Dexopt all packages");
        pw.println("    -r Set the compiler filter and the priority based on the given");
        pw.println("       compilation reason.");
        pw.println("       Available options: 'first-boot', 'boot-after-ota',");
        pw.println("       'boot-after-mainline-update', 'install', 'bg-dexopt', 'cmdline'.");
        pw.println("    -m Set the target compiler filter. The filter actually used may be");
        pw.println("       different, e.g. 'speed-profile' without profiles present may result in");
        pw.println("       'verify' being used instead. If not specified, this defaults to the");
        pw.println("       value given by -r, or the system property 'pm.dexopt.cmdline'.");
        pw.println("       Available options (in descending order): 'speed', 'speed-profile',");
        pw.println("       'verify'.");
        pw.println("    -p Set the priority of the operation, which determines the resource usage");
        pw.println("       and the process priority. If not specified, this defaults to");
        pw.println("       the value given by -r, or 'PRIORITY_INTERACTIVE'.");
        pw.println("       Available options (in descending order): 'PRIORITY_BOOT',");
        pw.println("       'PRIORITY_INTERACTIVE_FAST', 'PRIORITY_INTERACTIVE',");
        pw.println("       'PRIORITY_BACKGROUND'.");
        pw.println("    -f Force dexopt, also when the compiler filter being applied is not");
        pw.println("       better than that of the current dexopt artifacts for a package.");
        pw.println("    --reset Reset the dexopt state of the package as if the package is newly");
        pw.println("       installed.");
        pw.println("       More specifically, it clears reference profiles, current profiles, and");
        pw.println("       any code compiled from those local profiles. If there is an external");
        pw.println("       profile (e.g., a cloud profile), the code compiled from that profile");
        pw.println("       will be kept.");
        pw.println("       For secondary dex files, it also clears all dexopt artifacts.");
        pw.println("       When this flag is set, all the other flags are ignored.");
        pw.println("    -v Verbose mode. This mode prints detailed results.");
        pw.println("  Scope options:");
        pw.println("    --primary-dex Dexopt primary dex files only (all APKs that are installed");
        pw.println("      as part of the package, including the base APK and all other split");
        pw.println("      APKs).");
        pw.println("    --secondary-dex Dexopt secondary dex files only (APKs/JARs that the app");
        pw.println("      puts in its own data directory at runtime and loads with custom");
        pw.println("      classloaders).");
        pw.println("    --include-dependencies Include dependency packages (dependencies that are");
        pw.println("      declared by the app with <uses-library> tags and transitive");
        pw.println("      dependencies). This option can only be used together with");
        pw.println("      '--primary-dex' or '--secondary-dex'.");
        pw.println("    --full Dexopt all above. (Recommended)");
        pw.println("    --split SPLIT_NAME Only dexopt the given split. If SPLIT_NAME is an empty");
        pw.println("      string, only dexopt the base APK.");
        pw.println("      Tip: To pass an empty string, use a pair of quotes (\"\").");
        pw.println("      When this option is set, '--primary-dex', '--secondary-dex',");
        pw.println("      '--include-dependencies', '--full', and '-a' must not be set.");
        pw.println("    Note: If none of the scope options above are set, the scope defaults to");
        pw.println("    '--primary-dex --include-dependencies'.");
        pw.println();
        pw.println("delete-dexopt PACKAGE_NAME");
        pw.println("  Delete the dexopt artifacts of both primary dex files and secondary dex");
        pw.println("  files of a package.");
        pw.println();
        pw.println("bg-dexopt-job [--cancel | --disable | --enable]");
        pw.println("  Control the background dexopt job.");
        pw.println("  Without flags, it starts a background dexopt job immediately and waits for");
        pw.println("    it to finish. If a job is already started either automatically by the");
        pw.println("    system or through this command, it will wait for the running job to");
        pw.println("    finish and then start a new one.");
        pw.println("  Different from 'pm compile -r bg-dexopt -a', the behavior of this command");
        pw.println("  is the same as a real background dexopt job. Specifically,");
        pw.println("    - It only dexopts a subset of apps determined by either the system's");
        pw.println("      default logic based on app usage data or the custom logic specified by");
        pw.println("      the 'ArtManagerLocal.setBatchDexoptStartCallback' Java API.");
        pw.println("    - It runs dexopt in parallel, where the concurrency setting is specified");
        pw.println("      by the system property 'pm.dexopt.bg-dexopt.concurrency'.");
        pw.println("    - If the storage is low, it also downgrades unused apps.");
        pw.println("    - It also cleans up obsolete files.");
        pw.println("  Options:");
        pw.println("    --cancel Cancel any currently running background dexopt job immediately.");
        pw.println("      This cancels jobs started either automatically by the system or through");
        pw.println("      this command. This command is not blocking.");
        pw.println("    --disable: Disable the background dexopt job from being started by the");
        pw.println("      job scheduler. If a job is already started by the job scheduler and is");
        pw.println("      running, it will be cancelled immediately. Does not affect jobs started");
        pw.println("      through this command or by the system in other ways.");
        pw.println("      This state will be lost when the system_server process exits.");
        pw.println("    --enable: Enable the background dexopt job to be started by the job");
        pw.println("      scheduler again, if previously disabled by --disable.");
        pw.println("  When a list of package names is passed, this command does NOT start a real");
        pw.println("  background dexopt job. Instead, it dexopts the given packages sequentially.");
        pw.println("  This usage is deprecated. Please use 'pm compile -r bg-dexopt PACKAGE_NAME'");
        pw.println("  instead.");
        pw.println();
        pw.println("snapshot-profile [android | [--split SPLIT_NAME] PACKAGE_NAME]");
        pw.println("  Snapshot the boot image profile or the app profile and save it to");
        pw.println("  '" + PROFILE_DEBUG_LOCATION + "'.");
        pw.println("  If 'android' is passed, the command snapshots the boot image profile, and");
        pw.println("  the output filename is 'android.prof'.");
        pw.println("  If a package name is passed, the command snapshots the app profile.");
        pw.println("  Options:");
        pw.println("    --split SPLIT_NAME If specified, the command snapshots the profile of the");
        pw.println("      given split, and the output filename is");
        pw.println("      'PACKAGE_NAME-split_SPLIT_NAME.apk.prof'.");
        pw.println("      If not specified, the command snapshots the profile of the base APK,");
        pw.println("      and the output filename is 'PACKAGE_NAME.prof'");
        pw.println();
        pw.println("dump-profiles [--dump-classes-and-methods] PACKAGE_NAME");
        pw.println("  Dump the profiles of the given app in text format and save the outputs to");
        pw.println("  '" + PROFILE_DEBUG_LOCATION + "'.");
        pw.println("  The profile of the base APK is dumped to 'PACKAGE_NAME-primary.prof.txt'");
        pw.println("  The profile of a split APK is dumped to");
        pw.println("  'PACKAGE_NAME-SPLIT_NAME.split.prof.txt'");
        pw.println();
        pw.println("art SUB_COMMAND [ARGS]...");
        pw.println("  Run ART Service commands");
        pw.println();
        pw.println("  Supported sub-commands:");
        pw.println();
        pw.println("  cancel JOB_ID");
        pw.println("    Cancel a job started by a shell command. This doesn't apply to background");
        pw.println("    jobs.");
        pw.println();
        pw.println("  clear-app-profiles PACKAGE_NAME");
        pw.println("    Clear the profiles that are collected locally for the given package,");
        pw.println("    including the profiles for primary and secondary dex files. More");
        pw.println("    specifically, this command clears reference profiles and current");
        pw.println("    profiles. External profiles (e.g., cloud profiles) will be kept.");
        pw.println();
        pw.println("  cleanup");
        pw.println("    Cleanup obsolete files, such as dexopt artifacts that are outdated or");
        pw.println("    correspond to dex container files that no longer exist.");
        pw.println();
        pw.println("  dump [PACKAGE_NAME]");
        pw.println("    Dumps the dexopt state in text format to stdout.");
        pw.println("    If PACKAGE_NAME is empty, the command is for all packages. Otherwise, it");
        pw.println("    is for the given package.");
        pw.println();
        pw.println("  dexopt-packages -r REASON");
        pw.println("    Run batch dexopt for the given reason.");
        pw.println("    Valid values for REASON: 'first-boot', 'boot-after-ota',");
        pw.println("    'boot-after-mainline-update', 'bg-dexopt'");
        pw.println("    This command is different from 'pm compile -r REASON -a'. For example, it");
        pw.println("    only dexopts a subset of apps, and it runs dexopt in parallel. See the");
        pw.println("    API documentation for 'ArtManagerLocal.dexoptPackages' for details.");
    }

    private void enforceRootOrShell() {
        final int uid = Binder.getCallingUid();
        if (uid != Process.ROOT_UID && uid != Process.SHELL_UID) {
            throw new SecurityException("ART service shell commands need root or shell access");
        }
    }

    @PriorityClassApi
    int parsePriorityClass(@NonNull String priorityClass) {
        switch (priorityClass) {
            case "PRIORITY_BOOT":
                return ArtFlags.PRIORITY_BOOT;
            case "PRIORITY_INTERACTIVE_FAST":
                return ArtFlags.PRIORITY_INTERACTIVE_FAST;
            case "PRIORITY_INTERACTIVE":
                return ArtFlags.PRIORITY_INTERACTIVE;
            case "PRIORITY_BACKGROUND":
                return ArtFlags.PRIORITY_BACKGROUND;
            default:
                throw new IllegalArgumentException("Unknown priority " + priorityClass);
        }
    }

    @Nullable
    private String getSplitName(@NonNull PrintWriter pw,
            @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
            @NonNull String splitArg) {
        if (splitArg.isEmpty()) {
            return null; // Base APK.
        }

        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
        List<PrimaryDexInfo> dexInfoList = PrimaryDexUtils.getDexInfo(pkg);

        for (PrimaryDexInfo dexInfo : dexInfoList) {
            if (splitArg.equals(dexInfo.splitName())) {
                return splitArg;
            }
        }

        for (PrimaryDexInfo dexInfo : dexInfoList) {
            if (splitArg.equals(new File(dexInfo.dexPath()).getName())) {
                pw.println("Warning: Specifying a split using a filename is deprecated. Please "
                        + "use a split name (or an empty string for the base APK) instead");
                return dexInfo.splitName();
            }
        }

        throw new IllegalArgumentException(String.format("Split '%s' not found", splitArg));
    }

    @Nullable
    private String getSplitNameByFullPath(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
            @NonNull String packageName, @NonNull String fullPath) {
        PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
        AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
        List<PrimaryDexInfo> dexInfoList = PrimaryDexUtils.getDexInfo(pkg);

        for (PrimaryDexInfo dexInfo : dexInfoList) {
            if (fullPath.equals(dexInfo.dexPath())) {
                return dexInfo.splitName();
            }
        }

        throw new IllegalArgumentException(String.format("Code path '%s' not found", fullPath));
    }

    private int resetPackages(@NonNull PrintWriter pw,
            @NonNull PackageManagerLocal.FilteredSnapshot snapshot,
            @NonNull List<String> packageNames, boolean verbose) {
        try (var signal = new WithCancellationSignal(pw, verbose)) {
            for (String packageName : packageNames) {
                DexoptResult result =
                        mArtManagerLocal.resetDexoptStatus(snapshot, packageName, signal.get());
                printDexoptResult(pw, result, verbose, packageNames.size() > 1);
            }
        }
        return 0;
    }

    private int dexoptPackages(@NonNull PrintWriter pw,
            @NonNull PackageManagerLocal.FilteredSnapshot snapshot,
            @NonNull List<String> packageNames, @NonNull DexoptParams params, boolean verbose) {
        try (var signal = new WithCancellationSignal(pw, verbose)) {
            for (String packageName : packageNames) {
                DexoptResult result =
                        mArtManagerLocal.dexoptPackage(snapshot, packageName, params, signal.get());
                printDexoptResult(pw, result, verbose, packageNames.size() > 1);
            }
        }
        return 0;
    }

    @NonNull
    private String dexoptResultStatusToSimpleString(@DexoptResultStatus int status) {
        return (status == DexoptResult.DEXOPT_SKIPPED || status == DexoptResult.DEXOPT_PERFORMED)
                ? "Success"
                : "Failure";
    }

    private void printDexoptResult(@NonNull PrintWriter pw, @NonNull DexoptResult result,
            boolean verbose, boolean multiPackage) {
        for (PackageDexoptResult packageResult : result.getPackageDexoptResults()) {
            if (verbose) {
                pw.printf("[%s]\n", packageResult.getPackageName());
                for (DexContainerFileDexoptResult fileResult :
                        packageResult.getDexContainerFileDexoptResults()) {
                    pw.println(fileResult);
                }
            } else if (multiPackage) {
                pw.printf("[%s] %s\n", packageResult.getPackageName(),
                        dexoptResultStatusToSimpleString(packageResult.getStatus()));
            }
        }

        if (verbose) {
            pw.println("Final Status: "
                    + DexoptResult.dexoptResultStatusToString(result.getFinalStatus()));
        } else if (!multiPackage) {
            // Multi-package result is printed by the loop above.
            pw.println(dexoptResultStatusToSimpleString(result.getFinalStatus()));
        }

        pw.flush();
    }

    private void writeProfileFdContentsToFile(@NonNull PrintWriter pw,
            @NonNull ParcelFileDescriptor fd, @NonNull String outputRelativePath) {
        try {
            StructStat st = Os.stat(PROFILE_DEBUG_LOCATION);
            if (st.st_uid != Process.SYSTEM_UID || st.st_gid != Process.SHELL_UID
                    || (st.st_mode & 0007) != 0) {
                throw new RuntimeException(
                        String.format("%s has wrong permissions: uid=%d, gid=%d, mode=%o",
                                PROFILE_DEBUG_LOCATION, st.st_uid, st.st_gid, st.st_mode));
            }
        } catch (ErrnoException e) {
            throw new RuntimeException("Unable to stat " + PROFILE_DEBUG_LOCATION, e);
        }
        Path outputPath = Paths.get(PROFILE_DEBUG_LOCATION, outputRelativePath);
        try (InputStream inputStream = new AutoCloseInputStream(fd);
                FileOutputStream outputStream = new FileOutputStream(outputPath.toFile())) {
            // The system server doesn't have the permission to chown the file to "shell", so we
            // make it readable by everyone and put it in a directory that is only accessible by
            // "shell", which is created by system/core/rootdir/init.rc. The permissions are
            // verified by the code above.
            Os.fchmod(outputStream.getFD(), 0644);
            Streams.copy(inputStream, outputStream);
            pw.printf("Profile saved to '%s'\n", outputPath);
        } catch (IOException | ErrnoException e) {
            Utils.deleteIfExistsSafe(outputPath);
            throw new RuntimeException(e);
        }
    }

    private static class WithCancellationSignal implements AutoCloseable {
        @NonNull private final CancellationSignal mSignal = new CancellationSignal();
        @NonNull private final String mJobId;

        public WithCancellationSignal(@NonNull PrintWriter pw, boolean verbose) {
            mJobId = UUID.randomUUID().toString();
            if (verbose) {
                pw.printf(
                        "Job running. To cancel it, run 'pm art cancel %s' in a separate shell.\n",
                        mJobId);
                pw.flush();
            }

            synchronized (sCancellationSignalMap) {
                sCancellationSignalMap.put(mJobId, mSignal);
            }
        }

        @NonNull
        public CancellationSignal get() {
            return mSignal;
        }

        public void close() {
            synchronized (sCancellationSignalMap) {
                sCancellationSignalMap.remove(mJobId);
            }
        }
    }
}
