/*
 * Copyright (C) 2016 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.content.pm.UserInfo.FLAG_DEMO;
import static android.content.pm.UserInfo.FLAG_DISABLED;
import static android.content.pm.UserInfo.FLAG_EPHEMERAL;
import static android.content.pm.UserInfo.FLAG_FULL;
import static android.content.pm.UserInfo.FLAG_GUEST;
import static android.content.pm.UserInfo.FLAG_INITIALIZED;
import static android.content.pm.UserInfo.FLAG_MAIN;
import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
import static android.content.pm.UserInfo.FLAG_PROFILE;
import static android.content.pm.UserInfo.FLAG_RESTRICTED;
import static android.content.pm.UserInfo.FLAG_SYSTEM;
import static android.os.UserManager.USER_TYPE_FULL_DEMO;
import static android.os.UserManager.USER_TYPE_FULL_GUEST;
import static android.os.UserManager.USER_TYPE_FULL_RESTRICTED;
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.annotation.UserIdInt;
import android.app.PropertyInvalidatedCache;
import android.content.pm.UserInfo;
import android.content.pm.UserInfo.UserInfoFlag;
import android.content.res.Resources;
import android.multiuser.Flags;
import android.os.Looper;
import android.os.Parcel;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import android.text.TextUtils;
import android.util.Xml;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.frameworks.servicestests.R;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerService.UserData;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * <p>Run with:<pre>
 * runtest -c com.android.server.pm.UserManagerServiceUserInfoTest frameworks-services
 * </pre>
 */
@Presubmit
@RunWith(AndroidJUnit4.class)
@MediumTest
public class UserManagerServiceUserInfoTest {
    private UserManagerService mUserManagerService;
    private Resources mResources;

    @Before
    public void setup() {
        // Currently UserManagerService cannot be instantiated twice inside a VM without a cleanup
        // TODO: Remove once UMS supports proper dependency injection
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
        // Disable binder caches in this process.
        PropertyInvalidatedCache.disableForTestMode();

        LocalServices.removeServiceForTest(UserManagerInternal.class);
        mUserManagerService = new UserManagerService(InstrumentationRegistry.getContext());

        // The tests assume that the device has one user and its the system user.
        List<UserInfo> users = mUserManagerService.getUsers(/* excludeDying */ false);
        assertEquals("Multiple users so this test can't run.", 1, users.size());
        assertEquals("Only user present isn't the system user.",
                UserHandle.USER_SYSTEM, users.get(0).id);

        mResources = InstrumentationRegistry.getTargetContext().getResources();
    }

    @Test
    public void testWriteReadUserInfo() throws Exception {
        UserData data = new UserData();
        data.info = createUser();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(baos);
        mUserManagerService.writeUserLP(data, out);
        byte[] bytes = baos.toByteArray();

        UserData read = mUserManagerService.readUserLP(
                data.info.id, new ByteArrayInputStream(bytes), 0);

        assertUserInfoEquals(data.info, read.info, /* parcelCopy= */ false);
    }

    /** Tests that device policy restrictions are written/read properly. */
    @Test
    public void testWriteReadDevicePolicyUserRestrictions() throws Exception {
        final String globalRestriction = UserManager.DISALLOW_FACTORY_RESET;
        final String localRestriction = UserManager.DISALLOW_CONFIG_DATE_TIME;

        UserData data = new UserData();
        data.info = createUser(100, FLAG_FULL, "A type");

        mUserManagerService.putUserInfo(data.info);

        //Local restrictions are written to the user specific files and global restrictions
        // are written to the SYSTEM user file.
        setUserRestrictions(data.info.id, globalRestriction, localRestriction, true);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(baos);
        mUserManagerService.writeUserLP(data, out);
        byte[] secondaryUserBytes = baos.toByteArray();
        baos.reset();

        byte[] systemUserBytes = new byte[0];
        if (Flags.saveGlobalAndGuestRestrictionsOnSystemUserXmlReadOnly()) {
            UserData systemUserData = new UserData();
            systemUserData.info = mUserManagerService.getUserInfo(UserHandle.USER_SYSTEM);
            mUserManagerService.writeUserLP(systemUserData, baos);
            systemUserBytes = baos.toByteArray();
        }

        // Clear the restrictions to see if they are properly read in from the user file.
        setUserRestrictions(data.info.id, globalRestriction, localRestriction, false);

        final int userVersion = 10;
        //read the secondary and SYSTEM user file to fetch local/global device policy restrictions.
        mUserManagerService.readUserLP(data.info.id, new ByteArrayInputStream(secondaryUserBytes),
                userVersion);
        if (Flags.saveGlobalAndGuestRestrictionsOnSystemUserXmlReadOnly()) {
            mUserManagerService.readUserLP(UserHandle.USER_SYSTEM,
                    new ByteArrayInputStream(systemUserBytes), userVersion);
        }

        assertTrue(mUserManagerService.hasUserRestrictionOnAnyUser(globalRestriction));
        assertTrue(mUserManagerService.hasUserRestrictionOnAnyUser(localRestriction));
    }

    /** Sets a global and local restriction and verifies they were set properly **/
    private void setUserRestrictions(int id, String global, String local, boolean enabled) {
        mUserManagerService.setUserRestrictionInner(UserHandle.USER_ALL, global, enabled);
        assertEquals(mUserManagerService.hasUserRestrictionOnAnyUser(global), enabled);

        mUserManagerService.setUserRestrictionInner(id, local, enabled);
        assertEquals(mUserManagerService.hasUserRestrictionOnAnyUser(local), enabled);
    }

    @Test
    public void testParcelUnparcelUserInfo() throws Exception {
        UserInfo info = createUser();

        Parcel out = Parcel.obtain();
        info.writeToParcel(out, 0);
        byte[] data = out.marshall();
        out.recycle();

        Parcel in = Parcel.obtain();
        in.unmarshall(data, 0, data.length);
        in.setDataPosition(0);
        UserInfo read = UserInfo.CREATOR.createFromParcel(in);
        in.recycle();

        assertUserInfoEquals(info, read, /* parcelCopy= */ true);
    }

    @Test
    public void testCopyConstructor() throws Exception {
        UserInfo info = createUser();

        UserInfo copy = new UserInfo(info);

        assertUserInfoEquals(info, copy, /* parcelCopy= */ false);
    }

    @Test
    public void testGetUserName() throws Exception {
        assertFalse("System user name shouldn't be set",
                mUserManagerService.isUserNameSet(UserHandle.USER_SYSTEM));
        UserInfo userInfo = mUserManagerService.getUserInfo(UserHandle.USER_SYSTEM);
        assertFalse("A system provided name should be returned for primary user",
                TextUtils.isEmpty(userInfo.name));

        userInfo = createUser();
        userInfo.partial = false;
        final int TEST_ID = 100;
        userInfo.id = TEST_ID;
        mUserManagerService.putUserInfo(userInfo);
        assertTrue("Test user name must be set", mUserManagerService.isUserNameSet(TEST_ID));
        assertEquals("A Name", mUserManagerService.getUserInfo(TEST_ID).name);
    }

    /** Test UMS.isUserOfType(). */
    @Test
    public void testIsUserOfType() throws Exception {
        assertTrue("System user was of invalid type",
                mUserManagerService.isUserOfType(UserHandle.USER_SYSTEM, USER_TYPE_SYSTEM_HEADLESS)
                || mUserManagerService.isUserOfType(UserHandle.USER_SYSTEM, USER_TYPE_FULL_SYSTEM));

        final int testId = 100;
        final String typeName = "A type";
        UserInfo userInfo = createUser(testId, 0, typeName);
        mUserManagerService.putUserInfo(userInfo);
        assertTrue(mUserManagerService.isUserOfType(testId, typeName));
    }

    /** Test UserInfo.supportsSwitchTo() for partial user. */
    @Test
    public void testSupportSwitchTo_partial() throws Exception {
        UserInfo userInfo = createUser(100, FLAG_FULL, null);
        userInfo.partial = true;
        assertFalse("Switching to a partial user should be disabled",
                userInfo.supportsSwitchTo());
    }

    /** Test UserInfo.supportsSwitchTo() for disabled user. */
    @Test
    public void testSupportSwitchTo_disabled() throws Exception {
        UserInfo userInfo = createUser(100, FLAG_DISABLED, null);
        assertFalse("Switching to a DISABLED user should be disabled",
                userInfo.supportsSwitchTo());
    }

    /** Test UserInfo.supportsSwitchTo() for precreated users. */
    @Test
    public void testSupportSwitchTo_preCreated() throws Exception {
        UserInfo userInfo = createUser(100, FLAG_FULL, null);
        userInfo.preCreated = true;
        assertFalse("Switching to a precreated user should be disabled",
                userInfo.supportsSwitchTo());

        userInfo.preCreated = false;
        assertTrue("Switching to a full, real user should be allowed", userInfo.supportsSwitchTo());
    }

    /** Test UserInfo.supportsSwitchTo() for profiles. */
    @Test
    public void testSupportSwitchTo_profile() throws Exception {
        UserInfo userInfo = createUser(100, FLAG_PROFILE, null);
        assertFalse("Switching to a profiles should be disabled", userInfo.supportsSwitchTo());
    }

    /** Test UserInfo.canHaveProfile for main user */
    @Test
    public void testCanHaveProfile() throws Exception {
        UserInfo userInfo = createUser(100, FLAG_MAIN, null);
        assertTrue("Main users can have profile", userInfo.canHaveProfile());
    }

    /** Tests upgradeIfNecessaryLP (but without locking) for upgrading from version 8 to 9+. */
    @Test
    public void testUpgradeIfNecessaryLP_9() {
        final int versionToTest = 9;
        // do not trigger a user type upgrade
        final int userTypeVersion = UserTypeFactory.getUserTypeVersion();

        mUserManagerService.putUserInfo(createUser(100, FLAG_MANAGED_PROFILE, null));
        mUserManagerService.putUserInfo(createUser(101,
                FLAG_GUEST | FLAG_EPHEMERAL | FLAG_FULL, null));
        mUserManagerService.putUserInfo(createUser(102, FLAG_RESTRICTED | FLAG_FULL, null));
        mUserManagerService.putUserInfo(createUser(103, FLAG_FULL, null));
        mUserManagerService.putUserInfo(createUser(104, FLAG_SYSTEM, null));
        mUserManagerService.putUserInfo(createUser(105, FLAG_SYSTEM | FLAG_FULL, null));
        mUserManagerService.putUserInfo(createUser(106, FLAG_DEMO | FLAG_FULL, null));

        mUserManagerService.upgradeIfNecessaryLP(versionToTest - 1, userTypeVersion);

        assertTrue(mUserManagerService.isUserOfType(100, USER_TYPE_PROFILE_MANAGED));
        assertTrue((mUserManagerService.getUserInfo(100).flags & FLAG_PROFILE) != 0);

        assertTrue(mUserManagerService.isUserOfType(101, USER_TYPE_FULL_GUEST));

        assertTrue(mUserManagerService.isUserOfType(102, USER_TYPE_FULL_RESTRICTED));
        assertTrue((mUserManagerService.getUserInfo(102).flags & FLAG_PROFILE) == 0);

        assertTrue(mUserManagerService.isUserOfType(103, USER_TYPE_FULL_SECONDARY));
        assertTrue((mUserManagerService.getUserInfo(103).flags & FLAG_PROFILE) == 0);

        assertTrue(mUserManagerService.isUserOfType(104, USER_TYPE_SYSTEM_HEADLESS));

        assertTrue(mUserManagerService.isUserOfType(105, USER_TYPE_FULL_SYSTEM));

        assertTrue(mUserManagerService.isUserOfType(106, USER_TYPE_FULL_DEMO));
    }

    /** Tests readUserLP upgrading from version 9 to 10+. */
    @Test
    public void testUserRestrictionsUpgradeFromV9() throws Exception {
        final String[] localRestrictions = new String[] {
            UserManager.DISALLOW_CAMERA,
            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
        };

        final int userId = 100;
        UserData data = new UserData();
        data.info = createUser(userId, FLAG_FULL, "A type");

        mUserManagerService.putUserInfo(data.info);

        for (String restriction : localRestrictions) {
            assertFalse(mUserManagerService.hasBaseUserRestriction(restriction, userId));
            assertFalse(mUserManagerService.hasUserRestriction(restriction, userId));
        }

        // Convert the xml resource to the system storage xml format.
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream os = new DataOutputStream(baos);
        XmlPullParser in = mResources.getXml(R.xml.user_100_v9);
        XmlSerializer out = Xml.newBinarySerializer();
        out.setOutput(os, StandardCharsets.UTF_8.name());
        Xml.copy(in, out);
        byte[] userBytes = baos.toByteArray();
        baos.reset();

        final int userVersion = 9;
        mUserManagerService.readUserLP(data.info.id, new ByteArrayInputStream(userBytes),
                userVersion);

        for (String restriction : localRestrictions) {
            assertFalse(mUserManagerService.hasBaseUserRestriction(restriction, userId));
            assertTrue(mUserManagerService.hasUserRestriction(restriction, userId));
        }
    }

    /** Creates a UserInfo with the given flags and userType. */
    private UserInfo createUser(@UserIdInt int userId, @UserInfoFlag int flags, String userType) {
        return new UserInfo(userId, "A Name", "A path", flags, userType);
    }

    private UserInfo createUser() {
        UserInfo user = new UserInfo(/*id*/ 21, "A Name", "A path", /*flags*/ 0x0ff0ff, "A type");
        user.serialNumber = 5;
        user.creationTime = 4L << 32;
        user.lastLoggedInTime = 5L << 32;
        user.lastLoggedInFingerprint = "afingerprint";
        user.profileGroupId = 45;
        user.restrictedProfileParentId = 4;
        user.profileBadge = 2;
        user.partial = true;
        user.guestToRemove = true;
        user.preCreated = true;
        user.convertedFromPreCreated = true;
        return user;
    }

    private void assertUserInfoEquals(UserInfo one, UserInfo two, boolean parcelCopy) {
        assertEquals("Id not preserved", one.id, two.id);
        assertEquals("Name not preserved", one.name, two.name);
        assertEquals("Icon path not preserved", one.iconPath, two.iconPath);
        assertEquals("Flags not preserved", one.flags, two.flags);
        assertEquals("UserType not preserved", one.userType, two.userType);
        assertEquals("profile group not preserved", one.profileGroupId,
                two.profileGroupId);
        assertEquals("restricted profile parent not preserved", one.restrictedProfileParentId,
                two.restrictedProfileParentId);
        assertEquals("profile badge not preserved", one.profileBadge, two.profileBadge);
        assertEquals("partial not preserved", one.partial, two.partial);
        assertEquals("guestToRemove not preserved", one.guestToRemove, two.guestToRemove);
        assertEquals("preCreated not preserved", one.preCreated, two.preCreated);
        if (parcelCopy) {
            assertFalse("convertedFromPreCreated should not be set", two.convertedFromPreCreated);
        } else {
            assertEquals("convertedFromPreCreated not preserved", one.convertedFromPreCreated,
                    two.convertedFromPreCreated);
        }
    }

    /** Tests upgrading profile types */
    @Test
    public void testUpgradeProfileType_updateTypeAndFlags() {
        final int userId = 42;
        final String newUserTypeName = "new.user.type";
        final String oldUserTypeName = USER_TYPE_PROFILE_MANAGED;

        UserTypeDetails.Builder oldUserTypeBuilder = new UserTypeDetails.Builder()
                .setName(oldUserTypeName)
                .setBaseType(FLAG_PROFILE)
                .setDefaultUserInfoPropertyFlags(FLAG_MANAGED_PROFILE)
                .setMaxAllowedPerParent(32)
                .setIconBadge(401)
                .setBadgeColors(402, 403, 404)
                .setBadgeLabels(23, 24, 25);
        UserTypeDetails oldUserType = oldUserTypeBuilder.createUserTypeDetails();

        UserInfo userInfo = createUser(userId,
                oldUserType.getDefaultUserInfoFlags() | FLAG_INITIALIZED, oldUserTypeName);
        mUserManagerService.putUserInfo(userInfo);

        UserTypeDetails.Builder newUserTypeBuilder = new UserTypeDetails.Builder()
                .setName(newUserTypeName)
                .setBaseType(FLAG_PROFILE)
                .setMaxAllowedPerParent(32)
                .setIconBadge(401)
                .setBadgeColors(402, 403, 404)
                .setBadgeLabels(23, 24, 25);
        UserTypeDetails newUserType = newUserTypeBuilder.createUserTypeDetails();

        mUserManagerService.upgradeProfileToTypeLU(userInfo, newUserType);

        assertTrue(mUserManagerService.isUserOfType(userId, newUserTypeName));
        assertTrue((mUserManagerService.getUserInfo(userId).flags & FLAG_PROFILE) != 0);
        assertTrue((mUserManagerService.getUserInfo(userId).flags & FLAG_MANAGED_PROFILE) == 0);
        assertTrue((mUserManagerService.getUserInfo(userId).flags & FLAG_INITIALIZED) != 0);
    }

    @Test
    public void testUpgradeProfileType_updateRestrictions() {
        final int userId = 42;
        final String newUserTypeName = "new.user.type";
        final String oldUserTypeName = USER_TYPE_PROFILE_MANAGED;

        UserTypeDetails.Builder oldUserTypeBuilder = new UserTypeDetails.Builder()
                .setName(oldUserTypeName)
                .setBaseType(FLAG_PROFILE)
                .setDefaultUserInfoPropertyFlags(FLAG_MANAGED_PROFILE)
                .setMaxAllowedPerParent(32)
                .setIconBadge(401)
                .setBadgeColors(402, 403, 404)
                .setBadgeLabels(23, 24, 25);
        UserTypeDetails oldUserType = oldUserTypeBuilder.createUserTypeDetails();

        UserInfo userInfo = createUser(userId, oldUserType.getDefaultUserInfoFlags(),
                oldUserTypeName);
        mUserManagerService.putUserInfo(userInfo);
        mUserManagerService.setUserRestriction(UserManager.DISALLOW_CAMERA, true, userId);
        mUserManagerService.setUserRestriction(UserManager.DISALLOW_PRINTING, true, userId);

        UserTypeDetails.Builder newUserTypeBuilder = new UserTypeDetails.Builder()
                .setName(newUserTypeName)
                .setBaseType(FLAG_PROFILE)
                .setMaxAllowedPerParent(32)
                .setIconBadge(401)
                .setBadgeColors(402, 403, 404)
                .setBadgeLabels(23, 24, 25)
                .setDefaultRestrictions(
                        UserManagerServiceUserTypeTest.makeRestrictionsBundle(
                                UserManager.DISALLOW_WALLPAPER));
        UserTypeDetails newUserType = newUserTypeBuilder.createUserTypeDetails();

        mUserManagerService.upgradeProfileToTypeLU(userInfo, newUserType);

        assertTrue(mUserManagerService.getUserRestrictions(userId).getBoolean(
                UserManager.DISALLOW_PRINTING));
        assertTrue(mUserManagerService.getUserRestrictions(userId).getBoolean(
                UserManager.DISALLOW_CAMERA));
        assertTrue(mUserManagerService.getUserRestrictions(userId).getBoolean(
                UserManager.DISALLOW_WALLPAPER));
    }
}
