Merge "Do not use Map.Entry outside of lock" into udc-dev

This commit is contained in:
Kevin Chyn
2023-06-14 03:30:46 +00:00
committed by Android (Google) Code Review
2 changed files with 44 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ import android.annotation.CallbackExecutor;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.content.Context; import android.content.Context;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.Pair;
import com.android.internal.R; import com.android.internal.R;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
@@ -165,19 +166,28 @@ final class DeviceStateController {
// Make a copy here because it's possible that the consumer tries to remove a callback // Make a copy here because it's possible that the consumer tries to remove a callback
// while we're still iterating through the list, which would end up in a // while we're still iterating through the list, which would end up in a
// ConcurrentModificationException. // ConcurrentModificationException. Note that cannot use a List<Map.Entry> because the
final List<Map.Entry<Consumer<DeviceState>, Executor>> entries = new ArrayList<>(); // entries are tied to the backing map. So, if a client removes a callback while
synchronized (mWmLock) { // we are notifying clients, we will get a NPE.
for (Map.Entry<Consumer<DeviceState>, Executor> entry final List<Pair<Consumer<DeviceState>, Executor>> entries = copyDeviceStateCallbacks();
: mDeviceStateCallbacks.entrySet()) {
entries.add(entry); for (int i = 0; i < entries.size(); i++) {
final Pair<Consumer<DeviceState>, Executor> entry = entries.get(i);
entry.second.execute(() -> entry.first.accept(deviceState));
}
} }
} }
for (int i = 0; i < entries.size(); i++) { @VisibleForTesting
Map.Entry<Consumer<DeviceState>, Executor> entry = entries.get(i); @NonNull
entry.getValue().execute(() -> entry.getKey().accept(mCurrentDeviceState)); List<Pair<Consumer<DeviceState>, Executor>> copyDeviceStateCallbacks() {
} final List<Pair<Consumer<DeviceState>, Executor>> entries = new ArrayList<>();
synchronized (mWmLock) {
mDeviceStateCallbacks.forEach((deviceStateConsumer, executor) -> {
entries.add(new Pair<>(deviceStateConsumer, executor));
});
} }
return entries;
} }
} }

View File

@@ -17,17 +17,16 @@
package com.android.server.wm; package com.android.server.wm;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
import android.util.Pair;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
@@ -38,6 +37,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -58,6 +59,7 @@ public class DeviceStateControllerTests {
private DeviceStateController.DeviceState mCurrentState = private DeviceStateController.DeviceState mCurrentState =
DeviceStateController.DeviceState.UNKNOWN; DeviceStateController.DeviceState.UNKNOWN;
private Consumer<DeviceStateController.DeviceState> mDelegate; private Consumer<DeviceStateController.DeviceState> mDelegate;
private Executor mExecutor = MoreExecutors.directExecutor();
@Before @Before
public void setUp() { public void setUp() {
@@ -124,13 +126,31 @@ public class DeviceStateControllerTests {
mTarget.onDeviceStateReceivedByDisplayManager(mFoldedStates[0]); mTarget.onDeviceStateReceivedByDisplayManager(mFoldedStates[0]);
assertEquals(DeviceStateController.DeviceState.FOLDED, mCurrentState); assertEquals(DeviceStateController.DeviceState.FOLDED, mCurrentState);
// The callback should not receive state change when the it is unregistered. // The callback should not receive state change when it is unregistered.
mTarget.unregisterDeviceStateCallback(mDelegate); mTarget.unregisterDeviceStateCallback(mDelegate);
assertTrue(mTarget.mDeviceStateCallbacks.isEmpty()); assertTrue(mTarget.mDeviceStateCallbacks.isEmpty());
mTarget.onDeviceStateReceivedByDisplayManager(mOpenDeviceStates[0]); mTarget.onDeviceStateReceivedByDisplayManager(mOpenDeviceStates[0]);
assertEquals(DeviceStateController.DeviceState.FOLDED /* unchanged */, mCurrentState); assertEquals(DeviceStateController.DeviceState.FOLDED /* unchanged */, mCurrentState);
} }
@Test
public void testCopyDeviceStateCallbacks() {
initialize(true /* supportFold */, true /* supportHalfFolded */);
assertEquals(1, mTarget.mDeviceStateCallbacks.size());
assertTrue(mTarget.mDeviceStateCallbacks.containsKey(mDelegate));
List<Pair<Consumer<DeviceStateController.DeviceState>, Executor>> entries =
mTarget.copyDeviceStateCallbacks();
mTarget.unregisterDeviceStateCallback(mDelegate);
// In contrast to List<Map.Entry> where the entries are tied to changes in the backing map,
// List<Pair> should still contain non-null callbacks and executors even though they were
// removed from the backing map via the unregister method above.
assertEquals(1, entries.size());
assertEquals(mDelegate, entries.get(0).first);
assertEquals(mExecutor, entries.get(0).second);
}
private final int[] mFoldedStates = {0}; private final int[] mFoldedStates = {0};
private final int[] mOpenDeviceStates = {1}; private final int[] mOpenDeviceStates = {1};
private final int[] mHalfFoldedStates = {2}; private final int[] mHalfFoldedStates = {2};
@@ -194,7 +214,7 @@ public class DeviceStateControllerTests {
when(mMockContext.getResources()).thenReturn((mockRes)); when(mMockContext.getResources()).thenReturn((mockRes));
mockFold(mSupportFold, mSupportHalfFold); mockFold(mSupportFold, mSupportHalfFold);
mTarget = new DeviceStateController(mMockContext, new WindowManagerGlobalLock()); mTarget = new DeviceStateController(mMockContext, new WindowManagerGlobalLock());
mTarget.registerDeviceStateCallback(mDelegate, MoreExecutors.directExecutor()); mTarget.registerDeviceStateCallback(mDelegate, mExecutor);
} }
} }
} }