/*
 * 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.pm;

import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SpecialUsers.CanBeNULL;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.IActivityManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Build;
import android.os.Process;
import android.os.RemoteException;
import android.os.ShellCommand;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.IndentingPrintWriter;
import android.util.Slog;

import com.android.internal.R;
import com.android.internal.os.RoSystemProperties;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.LocalServices;
import com.android.server.UiThread;
import com.android.server.power.ShutdownThread;
import com.android.server.utils.Slogf;

import java.io.PrintWriter;
import java.util.List;

/**
 * Shell command implementation for the user manager service
 */
public class UserManagerServiceShellCommand extends ShellCommand {

    private static final String LOG_TAG = "UserManagerServiceShellCommand";
    @NonNull
    private final UserManagerService mService;
    @NonNull
    private final UserSystemPackageInstaller mSystemPackageInstaller;
    @NonNull
    private final LockPatternUtils mLockPatternUtils;
    @NonNull
    private final Context mContext;

    UserManagerServiceShellCommand(UserManagerService service,
            UserSystemPackageInstaller userSystemPackageInstaller,
            LockPatternUtils lockPatternUtils,
            Context context) {
        mService = service;
        mSystemPackageInstaller = userSystemPackageInstaller;
        mLockPatternUtils = lockPatternUtils;
        mContext = context;
    }

    @Override
    public void onHelp() {
        final PrintWriter pw = getOutPrintWriter();
        pw.println("User manager (user) commands:");
        pw.println("  help");
        pw.println("    Prints this help text.");
        pw.println();
        pw.println("  list [-v | --verbose] [-V | --very-verbose] [--all]");
        pw.println("    Prints all users on the system.");
        pw.println();
        pw.println("  report-system-user-package-whitelist-problems [-v | --verbose] "
                + "[--critical-only] [--mode MODE]");
        pw.println("    Reports all issues on user-type package allowlist XML files. Options:");
        pw.println("    -v | --verbose: shows extra info, like number of issues");
        pw.println("    --critical-only: show only critical issues, excluding warnings");
        pw.println("    --mode MODE: shows what errors would be if device used mode MODE");
        pw.println("      (where MODE is the allowlist mode integer as defined by "
                + "config_userTypePackageWhitelistMode)");
        pw.println();
        pw.println("  set-system-user-mode-emulation [--reboot | --no-restart] "
                + "<headless | full | default>");
        pw.println("    Changes whether the system user is headless, full, or default (as "
                + "defined by OEM).");
        pw.println("    WARNING: this command is meant just for development and debugging "
                + "purposes.");
        pw.println("             It should NEVER be used on automated tests.");
        pw.println("    NOTE: by default it restarts the Android runtime, unless called with");
        pw.println("          --reboot (which does a full reboot) or");
        pw.println("          --no-restart (which requires a manual restart)");
        pw.println();
        pw.println("  is-headless-system-user-mode [-v | --verbose]");
        pw.println("    Checks whether the device uses headless system user mode.");
        pw.println("  is-visible-background-users-on-default-display-supported [-v | --verbose]");
        pw.println("    Checks whether the device allows users to be start visible on background "
                + "in the default display.");
        pw.println("    It returns the effective mode, even when using emulation");
        pw.println("    (to get the real mode as well, use -v or --verbose)");
        pw.println();
        pw.println("  is-visible-background-users-supported [-v | --verbose]");
        pw.println("    Checks whether the device allows users to be start visible on background.");
        pw.println("    It returns the effective mode, even when using emulation");
        pw.println("    (to get the real mode as well, use -v or --verbose)");
        pw.println();
        pw.println("  is-user-visible [--display DISPLAY_ID] <USER_ID>");
        pw.println("    Checks if the given user is visible in the given display.");
        pw.println("    If the display option is not set, it uses the user's context to check");
        pw.println("    (so it emulates what apps would get from UserManager.isUserVisible())");
        pw.println();
        pw.println("  get-main-user ");
        pw.println("    Displays main user id or message if there is no main user");
        pw.println();
        pw.println("  grant-admin <USER_ID>");
        pw.println("    Grants admin privileges to the given user (requires adb root)");
        pw.println();
        pw.println("  revoke-admin <USER_ID>");
        pw.println("    Revokes admin privileges from the given user (requires adb root)");
        pw.println();
    }

    @Override
    public int onCommand(String cmd) {
        if (cmd == null) {
            return handleDefaultCommands(null);
        }

        try {
            switch(cmd) {
                case "list":
                    return runList();
                case "report-system-user-package-whitelist-problems":
                    return runReportPackageAllowlistProblems();
                case "set-system-user-mode-emulation":
                    return runSetSystemUserModeEmulation();
                case "is-headless-system-user-mode":
                    return runIsHeadlessSystemUserMode();
                case "is-visible-background-users-supported":
                    return runIsVisibleBackgroundUserSupported();
                case "is-visible-background-users-on-default-display-supported":
                    return runIsVisibleBackgroundUserOnDefaultDisplaySupported();
                case "is-user-visible":
                    return runIsUserVisible();
                case "get-main-user":
                    return runGetMainUserId();
                case "can-switch-to-headless-system-user":
                    return runCanSwitchToHeadlessSystemUser();
                case "is-main-user-permanent-admin":
                    return runIsMainUserPermanentAdmin();
                case "grant-admin":
                    return runGrantAdmin();
                case "revoke-admin":
                    return runRevokeAdmin();
                default:
                    return handleDefaultCommands(cmd);
            }
        } catch (RemoteException e) {
            getOutPrintWriter().println("Remote exception: " + e);
        }
        return -1;
    }

    private int runList() throws RemoteException {
        final PrintWriter pw = getOutPrintWriter();
        boolean all = false;
        boolean verbose = false;
        boolean veryVerbose = false;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-v":
                case "--verbose":
                    // verbose is really the normal human-readable version; avoid clutter
                    verbose = true;
                    break;
                case "-V":
                case "--very-verbose":
                    verbose = true;
                    veryVerbose = true;
                    break;
                case "--all":
                    all = true;
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }
        final IActivityManager am = ActivityManager.getService();
        final List<UserInfo> users = mService.getUsersWithUnresolvedNames(
                /* excludePartial= */ !all, /* excludeDying= */ false);
        if (users == null) {
            pw.println("Error: couldn't get users");
            return 1;
        } else {
            final int size = users.size();
            int currentUser = UserHandle.USER_NULL;
            if (verbose) {
                pw.printf("%d users:\n\n", size);
                currentUser = am.getCurrentUser().id;
            } else {
                // NOTE: the standard "list users" command is used by integration tests and
                // hence should not be changed. If you need to add more info, use the
                // verbose option.
                pw.println("Users:");
            }
            for (int i = 0; i < size; i++) {
                final UserInfo user = users.get(i);
                final boolean running = am.isUserRunning(user.id, 0);
                if (verbose) {
                    final DevicePolicyManagerInternal dpm = LocalServices
                            .getService(DevicePolicyManagerInternal.class);
                    String deviceOwner = "";
                    String profileOwner = "";
                    if (dpm != null) {
                        final long identity = Binder.clearCallingIdentity();
                        // NOTE: dpm methods below CANNOT be called while holding the mUsersLock
                        try {
                            if (dpm.getDeviceOwnerUserId() == user.id) {
                                deviceOwner = " (device-owner)";
                            }
                            if (dpm.getProfileOwnerAsUser(user.id) != null) {
                                profileOwner = " (profile-owner)";
                            }
                        } finally {
                            Binder.restoreCallingIdentity(identity);
                        }
                    }
                    final boolean current = user.id == currentUser;
                    final boolean hasParent = user.profileGroupId != user.id
                            && user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
                    final boolean visible = mService.isUserVisible(user.id);
                    // If name is null, use the default (owner / guest)
                    final String name = user.name != null ? user.name : mService.getName(user);
                    String unresolvedName = "";
                    if (veryVerbose) {
                        unresolvedName = ", unresolvedName=" + user.name;
                    }
                    // verbose is the normal standard human-readable version;
                    // therefore, avoid clutter unless veryVerbose
                    pw.printf("%d: id=%d, name=%s%s, type=%s, "
                            + "flags=%s%s%s%s%s%s%s%s%s%s%s\n",
                            i,
                            user.id,
                            name,
                            unresolvedName,
                            user.userType.replace("android.os.usertype.", ""),
                            UserInfo.flagsToString(user.flags),
                            hasParent ? " (parentId=" + user.profileGroupId + ")" : "",
                            running ? " (running)" : "",
                            user.partial ? " (partial)" : "",
                            user.guestToRemove ? " (guestToRemove)" : "",
                            user.preCreated ? " (pre-created)" : "",
                            user.convertedFromPreCreated ? " (converted)" : "",
                            deviceOwner, profileOwner,
                            current ? " (current)" : "",
                            visible ? " (visible)" : ""
                    );
                } else {
                    // NOTE: the standard "list users" command is used by integration tests and
                    // hence should not be changed. If you need to add more info, use the
                    // verbose option.
                    pw.printf("\t%s%s\n", user, running ? " running" : "");
                }
            }
            return 0;
        }
    }

    private int runReportPackageAllowlistProblems() {
        final PrintWriter pw = getOutPrintWriter();
        boolean verbose = false;
        boolean criticalOnly = false;
        int mode = UserSystemPackageInstaller.USER_TYPE_PACKAGE_ALLOWLIST_MODE_NONE;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-v":
                case "--verbose":
                    verbose = true;
                    break;
                case "--critical-only":
                    criticalOnly = true;
                    break;
                case "--mode":
                    mode = Integer.parseInt(getNextArgRequired());
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }

        Slog.d(LOG_TAG, "runReportPackageAllowlistProblems(): verbose=" + verbose
                + ", criticalOnly=" + criticalOnly
                + ", mode=" + UserSystemPackageInstaller.modeToString(mode));

        try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ")) {
            mSystemPackageInstaller.dumpPackageAllowlistProblems(ipw, mode, verbose,
                    criticalOnly);
        }
        return 0;
    }

    private int runSetSystemUserModeEmulation() {
        if (!confirmBuildIsDebuggable() || !confirmIsCalledByRoot()) {
            return -1;
        }

        final PrintWriter pw = getOutPrintWriter();

        // The headless system user cannot be locked; in theory, we could just make this check
        // when going full -> headless, but it doesn't hurt to check on both (and it makes the
        // code simpler)
        if (mLockPatternUtils.isSecure(UserHandle.USER_SYSTEM)) {
            pw.println("Cannot change system user mode when it has a credential");
            return -1;
        }

        boolean restart = true;
        boolean reboot = false;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--reboot":
                    reboot = true;
                    break;
                case "--no-restart":
                    restart = false;
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }
        if (reboot && !restart) {
            getErrPrintWriter().println("You can use --reboot or --no-restart, but not both");
            return -1;
        }

        final String mode = getNextArgRequired();
        final boolean isHeadlessSystemUserModeCurrently = UserManager
                .isHeadlessSystemUserMode();
        final boolean changed;

        switch (mode) {
            case UserManager.SYSTEM_USER_MODE_EMULATION_FULL:
                changed = isHeadlessSystemUserModeCurrently;
                break;
            case UserManager.SYSTEM_USER_MODE_EMULATION_HEADLESS:
                changed = !isHeadlessSystemUserModeCurrently;
                break;
            case UserManager.SYSTEM_USER_MODE_EMULATION_DEFAULT:
                changed = true; // Always update when resetting to default
                break;
            default:
                getErrPrintWriter().printf("Invalid arg: %s\n", mode);
                return -1;
        }

        if (!changed) {
            pw.printf("No change needed, system user is already %s\n",
                    isHeadlessSystemUserModeCurrently ? "headless" : "full");
            return 0;
        }

        Slogf.d(LOG_TAG, "Updating system property %s to %s",
                UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY, mode);

        SystemProperties.set(UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY, mode);

        if (reboot) {
            Slog.i(LOG_TAG, "Rebooting to finalize the changes");
            pw.println("Rebooting to finalize changes");
            UiThread.getHandler()
                    .post(() -> ShutdownThread.reboot(
                            ActivityThread.currentActivityThread().getSystemUiContext(),
                            "To switch headless / full system user mode",
                            /* confirm= */ false));
        } else if (restart) {
            Slog.i(LOG_TAG, "Shutting PackageManager down");
            LocalServices.getService(PackageManagerInternal.class).shutdown();

            final IActivityManager am = ActivityManager.getService();
            if (am != null) {
                try {
                    Slog.i(LOG_TAG, "Shutting ActivityManager down");
                    am.shutdown(/* timeout= */ 10_000);
                } catch (RemoteException e) {
                    Slog.e(LOG_TAG, "Failed to shut down ActivityManager" + e);
                }
            }

            final int pid = Process.myPid();
            Slogf.i(LOG_TAG, "Restarting Android runtime(PID=%d) to finalize changes", pid);
            pw.println("Restarting Android runtime to finalize changes");
            pw.println("The restart may trigger a 'Broken pipe' message; this is to be expected.");
            pw.flush();

            // Ideally there should be a cleaner / safer option to restart system_server, but
            // that doesn't seem to be the case. For example, ShutdownThread.reboot() calls
            // pm.shutdown() and am.shutdown() (which we already are calling above), but when
            // the system is restarted through 'adb shell stop && adb shell start`, these
            // methods are not called, so just killing the process seems to be fine.

            Process.killProcess(pid);
        } else {
            pw.println("System user mode changed - please reboot (or restart Android runtime) "
                    + "to continue");
            pw.println("NOTICE: after restart, some apps might be uninstalled (and their data "
                    + "will be lost)");
        }
        return 0;
    }

    @RequiresPermission(anyOf = {
            "android.permission.INTERACT_ACROSS_USERS",
            "android.permission.INTERACT_ACROSS_USERS_FULL"
    })
    private int runIsUserVisible() {
        PrintWriter pw = getOutPrintWriter();
        Integer displayId = null;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--display":
                    displayId = Integer.parseInt(getNextArgRequired());
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }
        int userId = getRequiredUserIdNextArg();

        boolean isVisible;
        if (displayId != null) {
            isVisible = mService.isUserVisibleOnDisplay(userId, displayId);
        } else {
            isVisible = getUserManagerForUser(userId).isUserVisible();
        }
        // NOTE: do not change output below (or command name / args), as it's used by ITestDevice
        pw.println(isVisible);
        return 0;
    }

    private int runIsHeadlessSystemUserMode() {
        PrintWriter pw = getOutPrintWriter();

        boolean verbose = false;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-v":
                case "--verbose":
                    verbose = true;
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }
        boolean effective = mService.isHeadlessSystemUserMode();
        if (!verbose) {
            // NOTE: do not change output below, as it's used by ITestDevice
            // (it's ok to change the verbose option though)
            pw.println(effective);
        } else {
            pw.printf("effective=%b real=%b\n", effective,
                    RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER);
        }
        return 0;
    }

    private int runIsVisibleBackgroundUserSupported() {
        PrintWriter pw = getOutPrintWriter();

        boolean verbose = false;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-v":
                case "--verbose":
                    verbose = true;
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }

        boolean effective = UserManager.isVisibleBackgroundUsersEnabled();
        if (!verbose) {
            // NOTE: do not change output below, as it's used by ITestDevice
            // (it's ok to change the verbose option though)
            pw.println(effective);
        } else {
            pw.printf("effective=%b real=%b\n", effective, Resources.getSystem()
                    .getBoolean(R.bool.config_multiuserVisibleBackgroundUsers));
        }
        return 0;
    }

    private int runIsVisibleBackgroundUserOnDefaultDisplaySupported() {
        PrintWriter pw = getOutPrintWriter();

        boolean verbose = false;
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "-v":
                case "--verbose":
                    verbose = true;
                    break;
                default:
                    pw.println("Invalid option: " + opt);
                    return -1;
            }
        }

        boolean effective = UserManager.isVisibleBackgroundUsersOnDefaultDisplayEnabled();
        if (!verbose) {
            // NOTE: do not change output below, as it's used by ITestDevice
            // (it's ok to change the verbose option though)
            pw.println(effective);
        } else {
            pw.printf("effective=%b real=%b\n", effective, Resources.getSystem()
                    .getBoolean(R.bool.config_multiuserVisibleBackgroundUsersOnDefaultDisplay));
        }
        return 0;
    }

    private int runGetMainUserId() {
        PrintWriter pw = getOutPrintWriter();
        final int mainUserId = mService.getMainUserId();
        if (mainUserId == UserHandle.USER_NULL) {
            pw.println("None");
            return 1;
        }
        pw.println(mainUserId);
        return 0;
    }

    private int runCanSwitchToHeadlessSystemUser() {
        PrintWriter pw = getOutPrintWriter();
        boolean canSwitchToHeadlessSystemUser = mService.canSwitchToHeadlessSystemUser();
        pw.println(canSwitchToHeadlessSystemUser);
        return 0;
    }

    private int runIsMainUserPermanentAdmin() {
        PrintWriter pw = getOutPrintWriter();
        boolean isMainUserPermanentAdmin = mService.isMainUserPermanentAdmin();
        pw.println(isMainUserPermanentAdmin);
        return 0;
    }

    private int runGrantAdmin() throws RemoteException {
        return grantOrRevokeAdmin(/* grant= */ true);
    }

    private int runRevokeAdmin() throws RemoteException {
        return grantOrRevokeAdmin(/* grant= */ false);
    }

    private int grantOrRevokeAdmin(boolean grant) {
        if (!confirmBuildIsDebuggable() || !confirmIsCalledByRoot()) {
            return -1;
        }
        int userId = getRequiredUserIdNextArg();
        if (userId == UserHandle.USER_NULL) {
            return -1;
        }
        boolean success;
        if (grant) {
            Slogf.i(LOG_TAG, "Calling setUserAdmin(%d)", userId);
            success = mService.setUserAdminInternal(userId);
        } else {
            Slogf.i(LOG_TAG, "Calling revokeUserAdmin(%d)", userId);
            success = mService.revokeUserAdminInternal(userId);
        }
        if (success) {
            getOutPrintWriter().println("Success");
            return 0;
        } else {
            getOutPrintWriter().println("Failed");
            return -1;
        }
    }

    /**
     * Gets the {@link UserManager} associated with the context of the given user.
     */
    private UserManager getUserManagerForUser(@UserIdInt int userId) {
        UserHandle user = UserHandle.of(userId);
        Context context = mContext.createContextAsUser(user, /* flags= */ 0);
        return context.getSystemService(UserManager.class);
    }

    /**
     * Confirms if the build is debuggable
     *
     * <p>It logs an error when it isn't.
     */
    private boolean confirmBuildIsDebuggable() {
        if (Build.isDebuggable()) {
            return true;
        }
        getErrPrintWriter().println("Command not available on user builds");
        return false;
    }

    /**
     * Confirms if the command is called when {@code adb} is rooted.
     *
     * <p>It logs an error when it isn't.
     */
    private boolean confirmIsCalledByRoot() {
        if (Binder.getCallingUid() == Process.ROOT_UID) {
            return true;
        }
        getErrPrintWriter().println("Command only available on root user");
        return false;
    }

    /**
     * Gets a user id from the next argument, properly handling special cases like `cur`.
     *
     * @return the user id or {@code USER_NULL} if the user id could not be parsed (in which case it
     * will also log an error).
     */
    @UserIdInt
    @CanBeNULL
    private int getRequiredUserIdNextArg() {
        int userId;
        try {
            userId = UserHandle.parseUserArg(getNextArgRequired());
        } catch (Exception e) {
            getErrPrintWriter().printf("Exception (%s) parsing userId argument\n", e);
            return UserHandle.USER_NULL;
        }
        switch (userId) {
            case UserHandle.USER_ALL:
            case UserHandle.USER_CURRENT_OR_SELF:
            case UserHandle.USER_NULL:
                getErrPrintWriter().printf("Invalid value (%d) for userId argument\n", userId);
                return UserHandle.USER_NULL;
            case UserHandle.USER_CURRENT:
                return ActivityManager.getCurrentUser();
            default:
                return userId;
        }
    }
}
