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

Commit bcbd43ff authored by Antonio Kantek's avatar Antonio Kantek Committed by Android (Google) Code Review
Browse files

Merge "Introduce UserData and UserDataRepository" into main

parents c2741c38 63b3426b
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -289,6 +289,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
    @GuardedBy("ImfLock.class")
    private int mCurrentUserId;

    /** Holds all user related data */
    @GuardedBy("ImfLock.class")
    private UserDataRepository mUserDataRepository;

    @MultiUserUnawareField
    final SettingsObserver mSettingsObserver;
    final WindowManagerInternal mWindowManagerInternal;
@@ -1284,9 +1288,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
        public void onUserStarting(TargetUser user) {
            // Called on ActivityManager thread.
            SecureSettingsWrapper.onUserStarting(user.getUserIdentifier());
            synchronized (ImfLock.class) {
                mService.mUserDataRepository.getOrCreate(user.getUserIdentifier());
            }
        }

    }

    void onUnlockUser(@UserIdInt int userId) {
        synchronized (ImfLock.class) {
            if (DEBUG) {
@@ -1373,6 +1381,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
            AdditionalSubtypeMapRepository.initialize(mHandler, mContext);

            mCurrentUserId = mActivityManagerInternal.getCurrentUserId();
            mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal);
            for (int id : mUserManagerInternal.getUserIds()) {
                mUserDataRepository.getOrCreate(id);
            }

            final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);

+89 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.inputmethod;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.pm.UserInfo;
import android.os.Handler;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.server.pm.UserManagerInternal;

import java.util.function.Consumer;

final class UserDataRepository {

    @GuardedBy("ImfLock.class")
    private final SparseArray<UserData> mUserData = new SparseArray<>();

    @GuardedBy("ImfLock.class")
    @NonNull
    UserData getOrCreate(@UserIdInt int userId) {
        UserData userData = mUserData.get(userId);
        if (userData == null) {
            userData = new UserData(userId);
            mUserData.put(userId, userData);
        }
        return userData;
    }

    @GuardedBy("ImfLock.class")
    void forAllUserData(Consumer<UserData> consumer) {
        for (int i = 0; i < mUserData.size(); i++) {
            consumer.accept(mUserData.valueAt(i));
        }
    }

    UserDataRepository(@NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal) {
        userManagerInternal.addUserLifecycleListener(
                new UserManagerInternal.UserLifecycleListener() {
                    @Override
                    public void onUserRemoved(UserInfo user) {
                        final int userId = user.id;
                        handler.post(() -> {
                            synchronized (ImfLock.class) {
                                mUserData.remove(userId);
                            }
                        });
                    }

                    @Override
                    public void onUserCreated(UserInfo user, Object unusedToken) {
                        final int userId = user.id;
                        handler.post(() -> {
                            synchronized (ImfLock.class) {
                                getOrCreate(userId);
                            }
                        });
                    }
                });
    }

    /** Placeholder for all IMMS user specific fields */
    static final class UserData {
        @UserIdInt
        final int mUserId;

       /**
         * Intended to be instantiated only from this file.
         */
        private UserData(@UserIdInt int userId) {
            mUserId = userId;
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ android_ravenwood_test {
    ],
    srcs: [
        "src/com/android/server/inputmethod/**/ClientControllerTest.java",
        "src/com/android/server/inputmethod/**/UserDataRepositoryTest.java",
    ],
    auto_gen_config: true,
}
+132 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.inputmethod;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.content.pm.UserInfo;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
import android.platform.test.ravenwood.RavenwoodRule;

import com.android.server.pm.UserManagerInternal;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.List;

// This test is designed to run on both device and host (Ravenwood) side.
public final class UserDataRepositoryTest {

    private static final int ANY_USER_ID = 1;

    @Rule
    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
            .setProvideMainThread(true).build();

    @Mock
    private UserManagerInternal mMockUserManagerInternal;

    private Handler mHandler;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mHandler = new Handler(Looper.getMainLooper());
    }

    @Test
    public void testUserDataRepository_addsNewUserInfoOnUserCreatedEvent() {
        // Create UserDataRepository and capture the user lifecycle listener
        final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
        verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
        final var listener = captor.getValue();

        // Assert that UserDataRepository is empty and then call onUserCreated
        assertThat(collectUserData(repository)).isEmpty();
        final var userInfo = new UserInfo();
        userInfo.id = ANY_USER_ID;
        listener.onUserCreated(userInfo, /* unused token */ new Object());
        waitForIdle();

        // Assert UserDataRepository contains the expected UserData
        final var allUserData = collectUserData(repository);
        assertThat(allUserData).hasSize(1);
        assertThat(allUserData.get(0).mUserId).isEqualTo(userInfo.id);
    }

    @Test
    public void testUserDataRepository_removesUserInfoOnUserRemovedEvent() {
        // Create UserDataRepository and capture the user lifecycle listener
        final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
        verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
        final var listener = captor.getValue();

        // Add one UserData ...
        final var userInfo = new UserInfo();
        userInfo.id = ANY_USER_ID;
        listener.onUserCreated(userInfo, /* unused token */ new Object());
        waitForIdle();
        // ... and then call onUserRemoved
        assertThat(collectUserData(repository)).hasSize(1);
        listener.onUserRemoved(userInfo);
        waitForIdle();

        // Assert UserDataRepository is now empty
        assertThat(collectUserData(repository)).isEmpty();
    }

    @Test
    public void testGetOrCreate() {
        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);

        synchronized (ImfLock.class) {
            final var userData = repository.getOrCreate(ANY_USER_ID);
            assertThat(userData.mUserId).isEqualTo(ANY_USER_ID);
        }

        final var allUserData = collectUserData(repository);
        assertThat(allUserData).hasSize(1);
        assertThat(allUserData.get(0).mUserId).isEqualTo(ANY_USER_ID);
    }

    private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) {
        final var collected = new ArrayList<UserDataRepository.UserData>();
        synchronized (ImfLock.class) {
            repository.forAllUserData(userData -> collected.add(userData));
        }
        return collected;
    }

    private void waitForIdle() {
        final var done = new ConditionVariable();
        mHandler.post(done::open);
        done.block();
    }
}