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

Commit 1144e9de authored by Chris Li's avatar Chris Li
Browse files

Synchronize window config updates (2/n)

Introduce ClientTransactionListenerController to keep track of the
registered listeners.

Bug: 260873529
Test: ClientTransactionListenerControllerTest
Change-Id: I98a7fddda24ef74a5f894e9d769456e0ba1993d0
parent 579d9cd8
Loading
Loading
Loading
Loading
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.app.servertransaction;

import static com.android.window.flags.Flags.syncWindowConfigUpdateFlag;

import static java.util.Objects.requireNonNull;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.os.Process;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;

/**
 * Singleton controller to manage listeners to individual {@link ClientTransaction}.
 *
 * TODO(b/260873529) make as TestApi to allow CTS.
 * @hide
 */
public class ClientTransactionListenerController {

    private static ClientTransactionListenerController sController;

    private final Object mLock = new Object();

    /**
     * Mapping from client registered listener for display change to the corresponding
     * {@link Executor} to invoke the listener on.
     * @see #registerDisplayChangeListener(IntConsumer, Executor)
     */
    @GuardedBy("mLock")
    private final ArrayMap<IntConsumer, Executor> mDisplayChangeListeners = new ArrayMap<>();

    private final ArrayList<IntConsumer> mTmpDisplayChangeListeners = new ArrayList<>();

    /** Gets the singleton controller. */
    @NonNull
    public static ClientTransactionListenerController getInstance() {
        synchronized (ClientTransactionListenerController.class) {
            if (sController == null) {
                sController = new ClientTransactionListenerController();
            }
            return sController;
        }
    }

    /** Creates a new instance for test only. */
    @VisibleForTesting
    @NonNull
    public static ClientTransactionListenerController createInstanceForTesting() {
        return new ClientTransactionListenerController();
    }

    private ClientTransactionListenerController() {}

    /**
     * Registers a new listener for display change. It will be invoked when receives a
     * {@link ClientTransaction} that is updating display-related window configuration, such as
     * bounds and rotation.
     *
     * WHen triggered, the listener will be invoked with the logical display id that was changed.
     *
     * @param listener the listener to invoke when receives a transaction with Display change.
     * @param executor the executor on which callback method will be invoked.
     */
    public void registerDisplayChangeListener(@NonNull IntConsumer listener,
            @NonNull @CallbackExecutor Executor executor) {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
            return;
        }
        requireNonNull(listener);
        requireNonNull(executor);
        synchronized (mLock) {
            mDisplayChangeListeners.put(listener, executor);
        }
    }

    /**
     * Unregisters the listener for display change that was previously registered through
     * {@link #registerDisplayChangeListener}.
     */
    public void unregisterDisplayChangeListener(@NonNull IntConsumer listener) {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
            return;
        }
        synchronized (mLock) {
            mDisplayChangeListeners.remove(listener);
        }
    }

    /**
     * Called when receives a {@link ClientTransaction} that is updating display-related
     * window configuration.
     */
    public void onDisplayChanged(int displayId) {
        if (!isSyncWindowConfigUpdateFlagEnabled()) {
            return;
        }
        synchronized (mLock) {
            // Make a copy of the list to avoid listener removal during callback.
            mTmpDisplayChangeListeners.addAll(mDisplayChangeListeners.keySet());
            final int num = mTmpDisplayChangeListeners.size();
            try {
                for (int i = 0; i < num; i++) {
                    final IntConsumer listener = mTmpDisplayChangeListeners.get(i);
                    final Executor executor = mDisplayChangeListeners.get(listener);
                    executor.execute(() -> listener.accept(displayId));
                }
            } finally {
                mTmpDisplayChangeListeners.clear();
            }
        }
    }

    /** Whether {@link #syncWindowConfigUpdateFlag} feature flag is enabled. */
    @VisibleForTesting
    public boolean isSyncWindowConfigUpdateFlagEnabled() {
        // Can't read flag from isolated process.
        return !Process.isIsolated() && syncWindowConfigUpdateFlag();
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -184,9 +184,12 @@ public class TransactionExecutor {
        }

        if (configUpdatedDisplays != null) {
            final ClientTransactionListenerController controller =
                    ClientTransactionListenerController.getInstance();
            final int displayCount = configUpdatedDisplays.size();
            for (int i = 0; i < displayCount; i++) {
                // TODO(b/260873529): trigger onDisplayChanged.
                final int displayId = configUpdatedDisplays.valueAt(i);
                controller.onDisplayChanged(displayId);
            }
        }
    }
+76 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.app.servertransaction;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.platform.test.annotations.Presubmit;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.function.IntConsumer;

/**
 * Tests for {@link ClientTransactionListenerController}.
 *
 * Build/Install/Run:
 *  atest FrameworksCoreTests:ClientTransactionListenerControllerTest
 */
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public class ClientTransactionListenerControllerTest {
    @Mock
    private IntConsumer mDisplayChangeListener;

    private ClientTransactionListenerController mController;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mController = spy(ClientTransactionListenerController.createInstanceForTesting());
        doReturn(true).when(mController).isSyncWindowConfigUpdateFlagEnabled();
    }

    @Test
    public void testRegisterDisplayChangeListener() {
        mController.registerDisplayChangeListener(mDisplayChangeListener, Runnable::run);

        mController.onDisplayChanged(123);

        verify(mDisplayChangeListener).accept(123);

        clearInvocations(mDisplayChangeListener);
        mController.unregisterDisplayChangeListener(mDisplayChangeListener);

        mController.onDisplayChanged(321);

        verify(mDisplayChangeListener, never()).accept(anyInt());
    }
}