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

Commit 9edf890b authored by Felipe Leme's avatar Felipe Leme
Browse files

Added UserVisibilityListener logic to UserVisibilityMediator.

This CL only adds logic on UserVisibilityMediator so it can call
the UserVisibilityListener callbacks upon users starting or stopping,
but it doesn't change UserController to use it yet - that will be done
in a separate CL. In fact, there are still some corner-case scenarios
there the logic is not working (for example, when the system user is
switched out, it's not sending a visibility change event for its
profiles)

Bug: 244333150
Test: atest UserVisibilityMediatorMUMDTest \
            UserVisibilityMediatorSUSDTest

Change-Id: Id10462768b905c5e72b5fa9fb33af048dadd2ad0
parent dc36cd4f
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -634,7 +634,7 @@ public class UserManagerService extends IUserManager.Stub {
    @GuardedBy("mUserStates")
    private final WatchedUserStates mUserStates = new WatchedUserStates();

    private final UserVisibilityMediator mUserVisibilityMediator = new UserVisibilityMediator();
    private final UserVisibilityMediator mUserVisibilityMediator;

    private static UserManagerService sInstance;

@@ -733,6 +733,7 @@ public class UserManagerService extends IUserManager.Stub {
        mPackagesLock = packagesLock;
        mUsers = users != null ? users : new SparseArray<>();
        mHandler = new MainHandler();
        mUserVisibilityMediator = new UserVisibilityMediator(mHandler);
        mUserDataPreparer = userDataPreparer;
        mUserTypes = UserTypeFactory.getUserTypes();
        invalidateOwnerNameIfNecessary(context.getResources(), true /* forceUpdate */);
+148 −22
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import static com.android.server.pm.UserManagerInternal.userAssignmentResultToSt
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Dumpable;
@@ -40,9 +41,11 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.pm.UserManagerInternal.UserAssignmentResult;
import com.android.server.pm.UserManagerInternal.UserVisibilityListener;
import com.android.server.utils.Slogf;

import java.io.PrintWriter;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Class responsible for deciding whether a user is visible (or visible for a given display).
@@ -110,15 +113,24 @@ public final class UserVisibilityMediator implements Dumpable {
    @GuardedBy("mLock")
    private final SparseIntArray mStartedProfileGroupIds = new SparseIntArray();

    UserVisibilityMediator() {
        this(UserManager.isUsersOnSecondaryDisplaysEnabled());
    /**
     * Handler user to call listeners
     */
    private final Handler mHandler;

    // @GuardedBy("mLock") - hold lock for writes, no lock necessary for simple reads
    final CopyOnWriteArrayList<UserVisibilityListener> mListeners =
            new CopyOnWriteArrayList<>();

    UserVisibilityMediator(Handler handler) {
        this(UserManager.isUsersOnSecondaryDisplaysEnabled(), handler);
    }

    @VisibleForTesting
    UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled) {
    UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled, Handler handler) {
        mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
        mUsersOnSecondaryDisplays = mUsersOnSecondaryDisplaysEnabled ? new SparseIntArray() : null;

        mHandler = handler;
        // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices
        mStartedProfileGroupIds.put(INITIAL_CURRENT_USER_ID, INITIAL_CURRENT_USER_ID);
    }
@@ -151,6 +163,7 @@ public final class UserVisibilityMediator implements Dumpable {
        }

        int result;
        IntArray visibleUsersBefore, visibleUsersAfter;
        synchronized (mLock) {
            result = getUserVisibilityOnStartLocked(userId, profileGroupId, foreground, displayId);
            if (DBG) {
@@ -166,6 +179,8 @@ public final class UserVisibilityMediator implements Dumpable {
                return USER_ASSIGNMENT_RESULT_FAILURE;
            }

            visibleUsersBefore = getVisibleUsers();

            // Set current user / profiles state
            if (foreground) {
                mCurrentUserId = userId;
@@ -195,8 +210,12 @@ public final class UserVisibilityMediator implements Dumpable {
                    Slogf.wtf(TAG,  "invalid resut from canAssignUserToDisplayLocked: %d",
                            mappingResult);
            }

            visibleUsersAfter = getVisibleUsers();
        }

        dispatchVisibilityChanged(visibleUsersBefore, visibleUsersAfter);

        if (DBG) {
            Slogf.d(TAG, "returning %s", userAssignmentResultToString(result));
        }
@@ -320,11 +339,23 @@ public final class UserVisibilityMediator implements Dumpable {
    /**
     * See {@link UserManagerInternal#unassignUserFromDisplayOnStop(int)}.
     */
    public void unassignUserFromDisplayOnStop(int userId) {
    public void unassignUserFromDisplayOnStop(@UserIdInt int userId) {
        if (DBG) {
            Slogf.d(TAG, "unassignUserFromDisplayOnStop(%d)", userId);
        }
        IntArray visibleUsersBefore, visibleUsersAfter;
        synchronized (mLock) {
            visibleUsersBefore = getVisibleUsers();

            unassignUserFromDisplayOnStopLocked(userId);

            visibleUsersAfter = getVisibleUsers();
        }
        dispatchVisibilityChanged(visibleUsersBefore, visibleUsersAfter);
    }

    @GuardedBy("mLock")
    private void unassignUserFromDisplayOnStopLocked(@UserIdInt int userId) {
        if (DBG) {
            Slogf.d(TAG, "Removing %d from mStartedProfileGroupIds (%s)", userId,
                    mStartedProfileGroupIds);
@@ -343,7 +374,6 @@ public final class UserVisibilityMediator implements Dumpable {
        }
        mUsersOnSecondaryDisplays.delete(userId);
    }
    }

    /**
     * See {@link UserManagerInternal#isUserVisible(int)}.
@@ -351,18 +381,29 @@ public final class UserVisibilityMediator implements Dumpable {
    public boolean isUserVisible(@UserIdInt int userId) {
        // First check current foreground user and their profiles (on main display)
        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
            if (DBG) {
                Slogf.d(TAG, "isUserVisible(%d): true to current user or profile", userId);
            }
            return true;
        }

        // Device doesn't support multiple users on multiple displays, so only users checked above
        // can be visible
        if (!mUsersOnSecondaryDisplaysEnabled) {
            if (DBG) {
                Slogf.d(TAG, "isUserVisible(%d): false for non-current user on MUMD", userId);
            }
            return false;
        }

        boolean visible;
        synchronized (mLock) {
            return mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0;
            visible = mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0;
        }
        if (DBG) {
            Slogf.d(TAG, "isUserVisible(%d): %b from mapping", userId, visible);
        }
        return visible;
    }

    /**
@@ -481,6 +522,79 @@ public final class UserVisibilityMediator implements Dumpable {
        return visibleUsers;
    }

    /**
     * Adds a {@link UserVisibilityListener listener}.
     */
    public void addListener(UserVisibilityListener listener) {
        if (DBG) {
            Slogf.d(TAG, "adding listener %s", listener);
        }
        synchronized (mLock) {
            mListeners.add(listener);
        }
    }

    /**
     * Removes a {@link UserVisibilityListener listener}.
     */
    public void removeListener(UserVisibilityListener listener) {
        if (DBG) {
            Slogf.d(TAG, "removing listener %s", listener);
        }
        synchronized (mLock) {
            mListeners.remove(listener);
        }
    }

    /**
     * Nofify all listeners about the visibility changes from before / after a change of state.
     */
    private void dispatchVisibilityChanged(IntArray visibleUsersBefore,
            IntArray visibleUsersAfter) {
        if (visibleUsersBefore == null) {
            // Optimization - it's only null when listeners is empty
            if (DBG) {
                Slogf.d(TAG,  "dispatchVisibilityChanged(): ignoring, no listeners");
            }
            return;
        }
        CopyOnWriteArrayList<UserVisibilityListener> listeners = mListeners;
        if (DBG) {
            Slogf.d(TAG,
                    "dispatchVisibilityChanged(): visibleUsersBefore=%s, visibleUsersAfter=%s, "
                    + "%d listeners (%s)", visibleUsersBefore, visibleUsersAfter, listeners.size(),
                    mListeners);
        }
        for (int i = 0; i < visibleUsersBefore.size(); i++) {
            int userId = visibleUsersBefore.get(i);
            if (visibleUsersAfter.indexOf(userId) == -1) {
                dispatchVisibilityChanged(listeners, userId, /* visible= */ false);
            }
        }
        for (int i = 0; i < visibleUsersAfter.size(); i++) {
            int userId = visibleUsersAfter.get(i);
            if (visibleUsersBefore.indexOf(userId) == -1) {
                dispatchVisibilityChanged(listeners, userId, /* visible= */ true);
            }
        }
    }

    private void dispatchVisibilityChanged(CopyOnWriteArrayList<UserVisibilityListener> listeners,
            @UserIdInt int userId, boolean visible) {
        if (DBG) {
            Slogf.d(TAG, "dispatchVisibilityChanged(%d -> %b): sending to %d listeners",
                    userId, visible, listeners.size());
        }
        for (int i = 0; i < mListeners.size(); i++) {
            UserVisibilityListener listener =  mListeners.get(i);
            if (DBG) {
                Slogf.v(TAG, "dispatchVisibilityChanged(%d -> %b): sending to %s",
                        userId, visible, listener);
            }
            mHandler.post(() -> listener.onUserVisibilityChanged(userId, visible));
        }
    }

    private void dump(IndentingPrintWriter ipw) {
        ipw.println("UserVisibilityMediator");
        ipw.increaseIndent();
@@ -504,6 +618,18 @@ public final class UserVisibilityMediator implements Dumpable {
                dumpSparseIntArray(ipw, mUsersOnSecondaryDisplays,
                        "background user / secondary display", "u", "d");
            }
            int numberListeners = mListeners.size();
            ipw.print("Number of listeners: ");
            ipw.println(numberListeners);
            if (numberListeners > 0) {
                ipw.increaseIndent();
                for (int i = 0; i < numberListeners; i++) {
                    ipw.print(i);
                    ipw.print(": ");
                    ipw.println(mListeners.get(i));
                }
                ipw.decreaseIndent();
            }
        }

        ipw.decreaseIndent();
+1 −1
Original line number Diff line number Diff line
@@ -43,7 +43,7 @@ public abstract class ExtendedMockitoTestCase {

    private MockitoSession mSession;

    private final Expect mExpect = Expect.create();
    protected final Expect mExpect = Expect.create();
    protected final DumpableDumperRule mDumpableDumperRule = new DumpableDumperRule();

    @Rule
+184 −0
Original line number Diff line number Diff line
/*
 * 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 static org.junit.Assert.fail;

import android.util.Log;

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

import com.google.common.truth.Expect;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * {@link UserVisibilityListener} implementation that expects callback events to be asynchronously
 * received.
 */
public final class AsyncUserVisibilityListener implements UserVisibilityListener {

    private static final String TAG = AsyncUserVisibilityListener.class.getSimpleName();

    private static final long WAIT_TIMEOUT_MS = 2_000;
    private static final long WAIT_NO_EVENTS_TIMEOUT_MS = 1_000;

    private static int sNextId;

    private final Object mLock = new Object();
    private final Expect mExpect;
    private final int mId = ++sNextId;
    private final Thread mExpectedReceiverThread;
    private final CountDownLatch mLatch;
    private final List<UserVisibilityChangedEvent> mExpectedEvents;

    @GuardedBy("mLock")
    private final List<UserVisibilityChangedEvent> mReceivedEvents = new ArrayList<>();

    @GuardedBy("mLock")
    private final List<String> mErrors = new ArrayList<>();

    private AsyncUserVisibilityListener(Expect expect, Thread expectedReceiverThread,
            List<UserVisibilityChangedEvent> expectedEvents) {
        mExpect = expect;
        mExpectedReceiverThread = expectedReceiverThread;
        mExpectedEvents = expectedEvents;
        mLatch = new CountDownLatch(expectedEvents.size());
    }

    @Override
    public void onUserVisibilityChanged(int userId, boolean visible) {
        UserVisibilityChangedEvent event = new UserVisibilityChangedEvent(userId, visible);
        Thread callingThread = Thread.currentThread();
        Log.d(TAG, "Received event (" + event + ") on thread " + callingThread);

        if (callingThread != mExpectedReceiverThread) {
            addError("event %s received in on thread %s but was expected on thread %s",
                    event, callingThread, mExpectedReceiverThread);
        }
        synchronized (mLock) {
            mReceivedEvents.add(event);
            mLatch.countDown();
        }
    }

    /**
     * Verifies the expected events were called.
     */
    public void verify() throws InterruptedException {
        waitForEventsAndCheckErrors();

        List<UserVisibilityChangedEvent> receivedEvents = getReceivedEvents();

        if (receivedEvents.isEmpty()) {
            mExpect.withMessage("received events").that(receivedEvents).isEmpty();
            return;
        }

        // NOTE: check "inOrder" might be too harsh in some cases (for example, if the fg user
        // has 2 profiles, the order of the events on the profiles wouldn't matter), but we
        // still need some dependency (like "user A became invisible before user B became
        // visible", so this is fine for now (but eventually we might need to add more
        // sophisticated assertions)
        mExpect.withMessage("received events").that(receivedEvents)
                .containsExactlyElementsIn(mExpectedEvents).inOrder();
    }

    @Override
    public String toString() {
        List<UserVisibilityChangedEvent> receivedEvents = getReceivedEvents();
        return "[" + getClass().getSimpleName() + ": id=" + mId
                + ", creationThread=" + mExpectedReceiverThread
                + ", received=" + receivedEvents.size()
                + ", events=" + receivedEvents + "]";
    }

    private List<UserVisibilityChangedEvent> getReceivedEvents() {
        synchronized (mLock) {
            return Collections.unmodifiableList(mReceivedEvents);
        }
    }

    private void waitForEventsAndCheckErrors() throws InterruptedException {
        waitForEvents();
        synchronized (mLock) {
            if (!mErrors.isEmpty()) {
                fail(mErrors.size() + " errors on received events: " + mErrors);
            }
        }
    }

    private void waitForEvents() throws InterruptedException {
        if (mExpectedEvents.isEmpty()) {
            Log.v(TAG, "Sleeping " + WAIT_NO_EVENTS_TIMEOUT_MS + "ms to make sure no event is "
                    + "received");
            Thread.sleep(WAIT_NO_EVENTS_TIMEOUT_MS);
            return;
        }

        int expectedNumberEvents = mExpectedEvents.size();
        Log.v(TAG, "Waiting up to " + WAIT_TIMEOUT_MS + "ms until " + expectedNumberEvents
                + " events are received");
        if (!mLatch.await(WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
            List<UserVisibilityChangedEvent> receivedEvents = getReceivedEvents();
            addError("Timed out (%d ms) waiting for %d events; received %d so far (%s), "
                    + "but expecting %d (%s)", WAIT_NO_EVENTS_TIMEOUT_MS, expectedNumberEvents,
                    receivedEvents.size(), receivedEvents, expectedNumberEvents, mExpectedEvents);
        }
    }

    @SuppressWarnings("AnnotateFormatMethod")
    private void addError(String format, Object...args) {
        synchronized (mLock) {
            mErrors.add(String.format(format, args));
        }
    }

    /**
     * Factory for {@link AsyncUserVisibilityListener} objects.
     */
    public static final class Factory {
        private final Expect mExpect;
        private final Thread mExpectedReceiverThread;

        public Factory(Expect expect, Thread expectedReceiverThread) {
            mExpect = expect;
            mExpectedReceiverThread = expectedReceiverThread;
        }

        /**
         * Creates a {@link AsyncUserVisibilityListener} that is expecting the given events.
         */
        public AsyncUserVisibilityListener forEvents(UserVisibilityChangedEvent...expectedEvents) {
            return new AsyncUserVisibilityListener(mExpect, mExpectedReceiverThread,
                    Arrays.asList(expectedEvents));
        }

        /**
         * Creates a {@link AsyncUserVisibilityListener} that is expecting no events.
         */
        public AsyncUserVisibilityListener forNoEvents() {
            return new AsyncUserVisibilityListener(mExpect, mExpectedReceiverThread,
                    Collections.emptyList());
        }
    }
}
+71 −0
Original line number Diff line number Diff line
/*
 * 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.UserIdInt;

/**
 * Representation of a {@link UserManagerInternal.UserVisibilityListener} event.
 */
public final class UserVisibilityChangedEvent {

    public @UserIdInt int userId;
    public boolean visible;

    UserVisibilityChangedEvent(@UserIdInt int userId, boolean visible) {
        this.userId = userId;
        this.visible = visible;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + userId;
        result = prime * result + (visible ? 1231 : 1237);
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        UserVisibilityChangedEvent other = (UserVisibilityChangedEvent) obj;
        if (userId != other.userId) return false;
        if (visible != other.visible) return false;
        return true;
    }

    @Override
    public String toString() {
        return userId + ":" + (visible ? "visible" : "invisible");
    }

    /**
     * Factory method.
     */
    public static UserVisibilityChangedEvent onVisible(@UserIdInt int userId) {
        return new UserVisibilityChangedEvent(userId, /* visible= */ true);
    }

    /**
     * Factory method.
     */
    public static UserVisibilityChangedEvent onInvisible(@UserIdInt int userId) {
        return new UserVisibilityChangedEvent(userId, /* visible= */ false);
    }
}
Loading