diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 5e41dd9c4f..a6a967b1a1 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -4489,6 +4489,7 @@ public class ConnectivityManager { public static final int CALLBACK_BLK_CHANGED = 11; /** @hide */ public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12; + // When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits. /** @hide */ diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java new file mode 100644 index 0000000000..060a984437 --- /dev/null +++ b/service/src/com/android/server/CallbackQueue.java @@ -0,0 +1,126 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; + +import com.android.net.module.util.GrowingIntArray; + +/** + * A utility class to add/remove {@link NetworkCallback}s from a queue. + * + *
This is intended to be used as a temporary builder to create/modify callbacks stored in an int + * array for memory efficiency. + * + *
Intended usage: + *
+ * final CallbackQueue queue = new CallbackQueue(storedCallbacks);
+ * queue.forEach(netId, callbackId -> { [...] });
+ * queue.addCallback(netId, callbackId);
+ * [...]
+ * queue.shrinkToLength();
+ * storedCallbacks = queue.getBackingArray();
+ *
+ *
+ * This class is not thread-safe. + */ +public class CallbackQueue extends GrowingIntArray { + public CallbackQueue(int[] initialCallbacks) { + super(initialCallbacks); + } + + /** + * Get a callback int from netId and callbackId. + * + *
The first 16 bits of each int is the netId; the last 16 bits are the callback index. + */ + private static int getCallbackInt(int netId, int callbackId) { + return (netId << 16) | (callbackId & 0xffff); + } + + private static int getNetId(int callbackInt) { + return callbackInt >>> 16; + } + + private static int getCallbackId(int callbackInt) { + return callbackInt & 0xffff; + } + + /** + * A consumer interface for {@link #forEach(CallbackConsumer)}. + * + *
This is similar to a BiConsumer<Integer, Integer>, but avoids the boxing cost. + */ + public interface CallbackConsumer { + /** + * Method called on each callback in the queue. + */ + void accept(int netId, int callbackId); + } + + /** + * Iterate over all callbacks in the queue. + */ + public void forEach(@NonNull CallbackConsumer consumer) { + forEach(value -> { + final int netId = getNetId(value); + final int callbackId = getCallbackId(value); + consumer.accept(netId, callbackId); + }); + } + + /** + * Indicates whether the queue contains a callback for the given (netId, callbackId). + */ + public boolean hasCallback(int netId, int callbackId) { + return contains(getCallbackInt(netId, callbackId)); + } + + /** + * Remove all callbacks for the given netId. + * + * @return true if at least one callback was removed. + */ + public boolean removeCallbacksForNetId(int netId) { + return removeValues(cb -> getNetId(cb) == netId); + } + + /** + * Remove all callbacks for the given netId and callbackId. + * @return true if at least one callback was removed. + */ + public boolean removeCallbacks(int netId, int callbackId) { + final int cbInt = getCallbackInt(netId, callbackId); + return removeValues(cb -> cb == cbInt); + } + + /** + * Add a callback at the end of the queue. + */ + public void addCallback(int netId, int callbackId) { + add(getCallbackInt(netId, callbackId)); + } + + @Override + protected String valueToString(int item) { + final int callbackId = getCallbackId(item); + final int netId = getNetId(item); + return ConnectivityManager.getCallbackName(callbackId) + "(" + netId + ")"; + } +} diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index f01574237b..5167570fc0 100755 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -145,6 +145,7 @@ import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPer import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf; import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE; import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS; +import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS; import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI; import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING; @@ -510,6 +511,9 @@ public class ConnectivityService extends IConnectivityManager.Stub private final boolean mUseDeclaredMethodsForCallbacksEnabled; + // Flag to delay callbacks for frozen apps, suppressing duplicate and stale callbacks. + private final boolean mQueueCallbacksForFrozenApps; + /** * Uids ConnectivityService tracks blocked status of to send blocked status callbacks. * Key is uid based on mAsUid of registered networkRequestInfo @@ -1873,6 +1877,9 @@ public class ConnectivityService extends IConnectivityManager.Stub context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN); mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context, ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS); + // registerUidFrozenStateChangedCallback is only available on U+ + mQueueCallbacksForFrozenApps = mDeps.isAtLeastU() + && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS); mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator( mContext, mTelephonyManager, mRequestRestrictedWifiEnabled, this::handleUidCarrierPrivilegesLost, mHandler); @@ -2028,7 +2035,7 @@ public class ConnectivityService extends IConnectivityManager.Stub mDelayDestroySockets = mDeps.isFeatureNotChickenedOut(context, DELAY_DESTROY_SOCKETS); mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut( mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS); - if (mDestroyFrozenSockets) { + if (mDestroyFrozenSockets || mQueueCallbacksForFrozenApps) { final UidFrozenStateChangedCallback frozenStateChangedCallback = new UidFrozenStateChangedCallback() { @Override @@ -3464,6 +3471,14 @@ public class ConnectivityService extends IConnectivityManager.Stub private void handleFrozenUids(int[] uids, int[] frozenStates) { ensureRunningOnConnectivityServiceThread(); + handleDestroyFrozenSockets(uids, frozenStates); + // TODO: handle freezing NetworkCallbacks + } + + private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) { + if (!mDestroyFrozenSockets) { + return; + } for (int i = 0; i < uids.length; i++) { final int uid = uids[i]; final boolean addReason = frozenStates[i] == UID_FROZEN_STATE_FROZEN; diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java index 1ee1ed7432..df8731619a 100644 --- a/service/src/com/android/server/connectivity/ConnectivityFlags.java +++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java @@ -49,6 +49,9 @@ public final class ConnectivityFlags { public static final String USE_DECLARED_METHODS_FOR_CALLBACKS = "use_declared_methods_for_callbacks"; + public static final String QUEUE_CALLBACKS_FOR_FROZEN_APPS = + "queue_callbacks_for_frozen_apps"; + private boolean mNoRematchAllRequestsOnRegister; /** diff --git a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java new file mode 100644 index 0000000000..4a81c10f82 --- /dev/null +++ b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java @@ -0,0 +1,190 @@ +/* + * 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.net.module.util; + +import android.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.StringJoiner; +import java.util.function.IntConsumer; +import java.util.function.IntPredicate; + +/** + * A growing array of primitive ints. + * + *
This is similar to ArrayList<Integer>, but avoids the cost of boxing (each Integer costs + * 16 bytes) and creation / garbage collection of individual Integer objects. + * + *
This class does not use any heuristic for growing capacity, so every call to + * {@link #add(int)} may reallocate the backing array. Callers should use + * {@link #ensureHasCapacity(int)} to minimize this behavior when they plan to add several values. + */ +public class GrowingIntArray { + private int[] mValues; + private int mLength; + + /** + * Create an empty GrowingIntArray with the given capacity. + */ + public GrowingIntArray(int initialCapacity) { + mValues = new int[initialCapacity]; + mLength = 0; + } + + /** + * Create a GrowingIntArray with an initial array of values. + * + *
The array will be used as-is and may be modified, so callers must stop using it after + * calling this constructor. + */ + protected GrowingIntArray(int[] initialValues) { + mValues = initialValues; + mLength = initialValues.length; + } + + /** + * Add a value to the array. + */ + public void add(int value) { + ensureHasCapacity(1); + mValues[mLength] = value; + mLength++; + } + + /** + * Get the current number of values in the array. + */ + public int length() { + return mLength; + } + + /** + * Get the value at a given index. + * + * @throws ArrayIndexOutOfBoundsException if the index is out of bounds. + */ + public int get(int index) { + if (index < 0 || index >= mLength) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mValues[index]; + } + + /** + * Iterate over all values in the array. + */ + public void forEach(@NonNull IntConsumer consumer) { + for (int i = 0; i < mLength; i++) { + consumer.accept(mValues[i]); + } + } + + /** + * Remove all values matching a predicate. + * + * @return true if at least one value was removed. + */ + public boolean removeValues(@NonNull IntPredicate predicate) { + int newQueueLength = 0; + for (int i = 0; i < mLength; i++) { + final int cb = mValues[i]; + if (!predicate.test(cb)) { + mValues[newQueueLength] = cb; + newQueueLength++; + } + } + if (mLength != newQueueLength) { + mLength = newQueueLength; + return true; + } + return false; + } + + /** + * Indicates whether the array contains the given value. + */ + public boolean contains(int value) { + for (int i = 0; i < mLength; i++) { + if (mValues[i] == value) { + return true; + } + } + return false; + } + + /** + * Remove all values from the array. + */ + public void clear() { + mLength = 0; + } + + /** + * Ensure at least the given number of values can be added to the array without reallocating. + * + * @param capacity The minimum number of additional values the array must be able to hold. + */ + public void ensureHasCapacity(int capacity) { + if (mValues.length >= mLength + capacity) { + return; + } + mValues = Arrays.copyOf(mValues, mLength + capacity); + } + + @VisibleForTesting + int getBackingArrayLength() { + return mValues.length; + } + + /** + * Shrink the array backing this class to the minimum required length. + */ + public void shrinkToLength() { + if (mValues.length != mLength) { + mValues = Arrays.copyOf(mValues, mLength); + } + } + + /** + * Get values as array by shrinking the internal array to length and returning it. + * + *
This avoids reallocations if the array is already the correct length, but callers should
+ * stop using this instance of {@link GrowingIntArray} if they use the array returned by this
+ * method.
+ */
+ public int[] getShrinkedBackingArray() {
+ shrinkToLength();
+ return mValues;
+ }
+
+ /**
+ * Get the String representation of an item in the array, for use by {@link #toString()}.
+ */
+ protected String valueToString(int item) {
+ return String.valueOf(item);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringJoiner joiner = new StringJoiner(",", "[", "]");
+ forEach(item -> joiner.add(valueToString(item)));
+ return joiner.toString();
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
new file mode 100644
index 0000000000..bdcb8c0b59
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.net.module.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GrowingIntArrayTest {
+ @Test
+ fun testAddAndGet() {
+ val array = GrowingIntArray(1)
+ array.add(-1)
+ array.add(0)
+ array.add(2)
+
+ assertEquals(-1, array.get(0))
+ assertEquals(0, array.get(1))
+ assertEquals(2, array.get(2))
+ assertEquals(3, array.length())
+ }
+
+ @Test
+ fun testForEach() {
+ val array = GrowingIntArray(10)
+ array.add(-1)
+ array.add(0)
+ array.add(2)
+
+ val actual = mutableListOf