Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fbbfc73a authored by Felipe Leme's avatar Felipe Leme Committed by Android (Google) Code Review
Browse files

Merge "Added UserFilter and new methods on UMS that use it." into main

parents 6949bdd6 de4f9a34
Loading
Loading
Loading
Loading
+156 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.flagsToString;

import android.annotation.Nullable;
import android.content.pm.UserInfo;
import android.content.pm.UserInfo.UserInfoFlag;

import java.util.Objects;

/**
 * Simple POJO use to filter user in methods like {@code getUsers()} or {@code hasUser()}.
 */
public final class UserFilter {

    private final boolean mIncludePartial;
    private final boolean mIncludeDying;
    private final @UserInfoFlag int mRequiredFlags;

    private UserFilter(Builder builder) {
        mIncludePartial = builder.mIncludePartial;
        mIncludeDying = builder.mIncludeDying;
        mRequiredFlags = builder.mRequiredFlags;
    }

    /**
     * Returns {@code true} if the given user matches the filter (and always {@code false} if it's
     * {@code null}).
     */
    boolean matches(DeathPredictor deathPredictor, @Nullable UserInfo user) {
        Objects.requireNonNull(deathPredictor, "deathPredictor cannot be null");
        if (user == null) {
            return false;
        }

        // Check below is the "legacy" checks from getUsersInternal(), but with inverted logic
        // (!include instead of exclude)
        if ((!mIncludePartial && user.partial)
                || user.preCreated // Not supported anymore, so ignored by filter
                || (!mIncludeDying && deathPredictor.isDying(user))) {
            return false;
        }

        // Check flags
        return (user.flags & mRequiredFlags) == mRequiredFlags;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mIncludeDying, mIncludePartial, mRequiredFlags);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        UserFilter other = (UserFilter) obj;
        return mIncludeDying == other.mIncludeDying && mIncludePartial == other.mIncludePartial
                && mRequiredFlags == other.mRequiredFlags;
    }

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder("UserFilter[");
        if (mIncludePartial) {
            string.append("includePartial, ");
        }
        if (mIncludeDying) {
            string.append("includeDying, ");
        }
        if (mRequiredFlags != 0) {
            string.append("requiredFlags=").append(flagsToString(mRequiredFlags));
        } else {
            // using else to avoid ending with ,
            string.append("noRequiredFlags");
        }
        return string.append(']').toString();
    }

    /**
     * Gets a {@link Builder} instance.
     *
     * <p>For now it's always returning a new one, but eventually it could be optimized (for
     * example, reusing the same builder that's backed by a {@link ThreadLocal} instance.
     */
    public static Builder builder() {
        return new Builder();
    }

    /*
     * Bob, the Builder!
     *
     * <p>By default, it includes all users, without any filtering.
     */
    public static final class Builder {

        private boolean mIncludePartial;
        private boolean mIncludeDying;
        private @UserInfoFlag int mRequiredFlags;

        private Builder() {
        }

        /** Returns a new {@code UserFilter}, */
        public UserFilter build() {
            return new UserFilter(this);
        }

        /** Filter will Include partial users. */
        public Builder withPartialUsers() {
            mIncludePartial = true;
            return this;
        }

        /** Filter will Include dying users. */
        public Builder withDyingUsers() {
            mIncludeDying = true;
            return this;
        }

        /** When set, filter will only include users whose flags contain the given flags */
        public Builder setRequiredFlags(@UserInfoFlag int flags) {
            mRequiredFlags = flags;
            return this;
        }
    }

    /** Used to decide if a user is dying, as that information is not present in the user itself. */
    interface DeathPredictor {

        /** Returns {@code true} if the poor user is indeed dying... */
        boolean isDying(UserInfo userInfo);
    }
}
+63 −1
Original line number Diff line number Diff line
@@ -184,6 +184,7 @@ import com.android.server.StorageManagerInternal;
import com.android.server.SystemService;
import com.android.server.am.UserState;
import com.android.server.locksettings.LockSettingsInternal;
import com.android.server.pm.UserFilter.DeathPredictor;
import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
import com.android.server.storage.DeviceStorageMonitorInternal;
@@ -228,6 +229,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

/**
 * Service for {@link UserManager}.
@@ -583,6 +585,10 @@ public class UserManagerService extends IUserManager.Stub {
    @GuardedBy("mUsersLock")
    private final SparseBooleanArray mRemovingUserIds = new SparseBooleanArray();

    /** Used on methods that take a UserFilter (like {@link #getUsers(UserFilter)}) */
    @GuardedBy("mUsersLock")
    private final DeathPredictor mDeathPredictor = user -> mRemovingUserIds.get(user.id);

    /**
     * Queue of recently removed userIds. Used for recycling of userIds
     */
@@ -1649,7 +1655,11 @@ public class UserManagerService extends IUserManager.Stub {
    }

    // Used by cmd users
    @NonNull List<UserInfo> getUsersWithUnresolvedNames(boolean excludePartial,
    /**
     * @deprecated should use {@link #getUsers(UserFilter)} instead.
     */
    @Deprecated
    List<UserInfo> getUsersWithUnresolvedNames(boolean excludePartial,
            boolean excludeDying) {
        checkCreateUsersPermission("get users with unresolved names");
        return getUsersInternal(excludePartial, excludeDying, /* resolveNullNames= */ false);
@@ -1680,6 +1690,58 @@ public class UserManagerService extends IUserManager.Stub {
        }
    }

    /** Gets the users that match the given {@code filter}. */
    List<UserInfo> getUsers(UserFilter filter) {
        return getUsersInternal(filter, /* converter= */ null);
    }

    /**
     * Gets the converted users that match the given {@code filter}.
     *
     * <p>Typically used with {@link #userWithName(UserInfo)} resolve {@code null} names.
     */
    @VisibleForTesting
    List<UserInfo> getUsers(UserFilter filter, Function<UserInfo, UserInfo> converter) {
        Objects.requireNonNull(converter, "converter cannot be null");
        return getUsersInternal(filter, converter);
    }

    private List<UserInfo> getUsersInternal(UserFilter filter,
            @Nullable Function<UserInfo, UserInfo> converter) {
        Objects.requireNonNull(filter, "filter cannot be null");
        synchronized (mUsersLock) {
            ArrayList<UserInfo> users = new ArrayList<>(mUsers.size());
            int userSize = mUsers.size();
            for (int i = 0; i < userSize; i++) {
                UserInfo user = mUsers.valueAt(i).info;
                if (filter.matches(mDeathPredictor, user)) {
                    if (converter == null) {
                        users.add(user);
                    } else {
                        users.add(converter.apply(user));
                    }
                }
            }
            return users;
        }
    }

    /** Gets the number of users that matches the given {@code filter}. */
    int getNumberOfUsers(UserFilter filter) {
        Objects.requireNonNull(filter, "filter cannot be null");
        int number = 0;
        synchronized (mUsersLock) {
            int userSize = mUsers.size();
            for (int i = 0; i < userSize; i++) {
                UserInfo user = mUsers.valueAt(i).info;
                if (filter.matches(mDeathPredictor, user)) {
                    number++;
                }
            }
        }
        return number;
    }

    @Override
    public List<UserInfo> getProfiles(@UserIdInt int userId, boolean enabledOnly) {
        boolean returnFullInfo;
+1 −0
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ android_test {
        "androidx.test.ext.truth",
        "flag-junit",
        "frameworks-base-testutils",
        "guava-android-testlib", // for EqualsTester
        "hamcrest-library",
        "kotlin-test",
        "mockingservicestests-utils-mockito",
+268 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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_ADMIN;
import static android.content.pm.UserInfo.FLAG_FULL;
import static android.content.pm.UserInfo.FLAG_MAIN;
import static android.content.pm.UserInfo.FLAG_SYSTEM;
import static android.os.UserHandle.USER_SYSTEM;
import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;

import static org.junit.Assert.assertThrows;

import android.annotation.UserIdInt;
import android.content.pm.UserInfo;
import android.content.pm.UserInfo.UserInfoFlag;

import com.android.server.pm.UserFilter.Builder;
import com.android.server.pm.UserFilter.DeathPredictor;

import com.google.common.testing.EqualsTester;
import com.google.common.truth.Expect;

import org.junit.Rule;
import org.junit.Test;

public final class UserFilterTest {

    private static final @UserIdInt int ADMIN_USER_ID = 4;
    private static final @UserIdInt int NON_ADMIN_USER_ID = 8;
    private static final @UserIdInt int FLAGLESS_USER_ID = 15;
    private static final @UserIdInt int PARTIAL_USER_ID = 16;
    private static final @UserIdInt int DYING_USER_ID = 23;
    private static final @UserIdInt int PRE_CREATED_USER_ID = 42;

    private static final DeathPredictor NOSTRADAMUS = user -> user.id == DYING_USER_ID;

    private final UserInfo mNullUser = null;

    // Note: not setting FLAG_PRIMARY on system user as it's deprecated (and not really used
    private final UserInfo mFullSystemUser =
            createUser(USER_SYSTEM, "full system user",
            FLAG_ADMIN | FLAG_FULL | FLAG_SYSTEM, USER_TYPE_FULL_SYSTEM);
    private final UserInfo mHeadlessSystemUser =
            createUser(USER_SYSTEM, "headless system user",
            FLAG_ADMIN | FLAG_SYSTEM, USER_TYPE_SYSTEM_HEADLESS);

    private final UserInfo mAdminUser =
            createUser(ADMIN_USER_ID, "admin user",
            FLAG_ADMIN | FLAG_FULL , USER_TYPE_FULL_SECONDARY);
    private final UserInfo mNonAdminUser =
            createSecondaryUser(NON_ADMIN_USER_ID, "non-admin user");
    private final UserInfo mFlagLessUser =
            createUser(FLAGLESS_USER_ID, "flagless user", /* flags= */ 0, USER_TYPE_FULL_SECONDARY);
    private final UserInfo mDyingUser = createSecondaryUser(DYING_USER_ID, "dying user");

    // Users below are defined in the constructor because we need to set some properties after
    // they're instantiated
    private final UserInfo mPartialUser;
    // NOTE: pre-created users are not supported anymore, so they shouldn't be included on any
    // result
    private final UserInfo mPreCreatedUser;

    @Rule
    public final Expect expect = Expect.create();

    public UserFilterTest() {
        mPartialUser = createSecondaryUser(PARTIAL_USER_ID, "partial user");
        mPartialUser.partial = true;

        mPreCreatedUser = createSecondaryUser(PRE_CREATED_USER_ID, "pre-created user");
        mPreCreatedUser.preCreated = true;
    }

    @Test
    public void testEquals() {
        var defaultBuilder = createBuilder();
        UserFilter default1 = defaultBuilder.build();
        UserFilter default2 = defaultBuilder.build();

        var builderWithEverything = createBuilder()
                .withPartialUsers()
                .withDyingUsers()
                .setRequiredFlags(FLAG_MAIN);
        UserFilter withEverything1 = builderWithEverything.build();
        UserFilter withEverything2 = builderWithEverything.build();

        new EqualsTester()
                .addEqualityGroup(default1, default2)
                .addEqualityGroup(withEverything1, withEverything2)
                .testEquals();
    }

    @Test
    public void testNullDeathPredictor() {
        UserFilter filter = createBuilder().build();

        assertThrows(NullPointerException.class, ()-> filter.matches(null, mFullSystemUser));
        assertThrows(NullPointerException.class, ()-> filter.matches(null, null));
    }

    @Test
    public void testDefaultFilter() {
        UserFilter filter = createBuilder().build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, true);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, true);
        expectMatches(filter, mFlagLessUser, true);
        expectMatches(filter, mPartialUser, false);
        expectMatches(filter, mDyingUser, false);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testIncludePartial() {
        UserFilter filter = createBuilder()
                .withPartialUsers()
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, true);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, true);
        expectMatches(filter, mFlagLessUser, true);
        expectMatches(filter, mPartialUser, true);
        expectMatches(filter, mDyingUser, false);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testIncludeDying() {
        UserFilter filter = createBuilder()
                .withDyingUsers()
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, true);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, true);
        expectMatches(filter, mFlagLessUser, true);
        expectMatches(filter, mPartialUser, false);
        expectMatches(filter, mDyingUser, true);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testIncludePreCreated() {
        UserFilter filter = createBuilder()
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, true);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, true);
        expectMatches(filter, mFlagLessUser, true);
        expectMatches(filter, mPartialUser, false);
        expectMatches(filter, mDyingUser, false);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testIncludeAllDefectiveUsers() {
        UserFilter filter = createBuilder()
                .withPartialUsers()
                .withDyingUsers()
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, true);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, true);
        expectMatches(filter, mFlagLessUser, true);
        expectMatches(filter, mPartialUser, true);
        expectMatches(filter, mDyingUser, true);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testAdminsOnly() {
        UserFilter filter = createBuilder()
                .setRequiredFlags(FLAG_ADMIN)
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, true);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, false);
        expectMatches(filter, mFlagLessUser, false);
        expectMatches(filter, mPartialUser, false);
        expectMatches(filter, mDyingUser, false);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testFullUsersOnly() {
        UserFilter filter = createBuilder()
                .setRequiredFlags(FLAG_FULL)
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, false);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, true);
        expectMatches(filter, mFlagLessUser, false);
        expectMatches(filter, mPartialUser, false);
        expectMatches(filter, mDyingUser, false);
        expectMatches(filter, mPreCreatedUser, false);
    }

    @Test
    public void testFullAdminsOnly() {
        UserFilter filter = createBuilder()
                .setRequiredFlags(FLAG_FULL | FLAG_ADMIN)
                .build();

        expectMatches(filter, mNullUser, false);
        expectMatches(filter, mFullSystemUser, true);
        expectMatches(filter, mHeadlessSystemUser, false);
        expectMatches(filter, mAdminUser, true);
        expectMatches(filter, mNonAdminUser, false);
        expectMatches(filter, mFlagLessUser, false);
        expectMatches(filter, mPartialUser, false);
        expectMatches(filter, mDyingUser, false);
        expectMatches(filter, mPreCreatedUser, false);
    }

    private void expectMatches(UserFilter filter, UserInfo user, boolean value) {
        expect.withMessage("matches %s", user).that(filter.matches(NOSTRADAMUS, user))
                .isEqualTo(value);
    }

    private static UserInfo createUser(@UserIdInt int userId, String name, @UserInfoFlag int flags,
            String userType) {
        return new UserInfo(userId, name, /* iconPath= */ null, flags, userType);
    }

    private static UserInfo createSecondaryUser(@UserIdInt int userId, String name) {
        return createUser(userId, name, FLAG_FULL, USER_TYPE_FULL_SECONDARY);
    }

    private Builder createBuilder() {
        return UserFilter.builder();
    }
}
+117 −0

File changed.

Preview size limit exceeded, changes collapsed.