Merge changes I6cc2507d,I187ea15d into main

* changes:
  Add flag for QUEUE_CALLBACKS_FOR_FROZEN_APPS
  Add a CallbackQueue class
This commit is contained in:
Remi NGUYEN VAN 2024-07-16 08:46:11 +00:00 • committed by Gerrit Code Review
commit 16828bdeba
9 changed files with 648 additions and 1 deletions

View file

@ -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 */

View file

@ -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.
*
* <p>This is intended to be used as a temporary builder to create/modify callbacks stored in an int
* array for memory efficiency.
*
* <p>Intended usage:
* <pre>
* final CallbackQueue queue = new CallbackQueue(storedCallbacks);
* queue.forEach(netId, callbackId -> { [...] });
* queue.addCallback(netId, callbackId);
* [...]
* queue.shrinkToLength();
* storedCallbacks = queue.getBackingArray();
* </pre>
*
* <p>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.
*
* <p>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)}.
*
* <p>This is similar to a BiConsumer&lt;Integer, Integer&gt;, 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 + ")";
}
}

View file

@ -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;

View file

@ -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;
/**

View file

@ -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.
*
* <p>This is similar to ArrayList&lt;Integer&gt;, but avoids the cost of boxing (each Integer costs
* 16 bytes) and creation / garbage collection of individual Integer objects.
*
* <p>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.
*
* <p>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.
*
* <p>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();
}
}

View file

@ -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<Int>()
array.forEach { actual.add(it) }
val expected = listOf(-1, 0, 2)
assertEquals(expected, actual)
}
@Test
fun testForEach_EmptyArray() {
val array = GrowingIntArray(10)
array.forEach {
fail("This should not be called")
}
}
@Test
fun testRemoveValues() {
val array = GrowingIntArray(10)
array.add(-1)
array.add(0)
array.add(2)
array.removeValues { it <= 0 }
assertEquals(1, array.length())
assertEquals(2, array.get(0))
}
@Test
fun testContains() {
val array = GrowingIntArray(10)
array.add(-1)
array.add(2)
assertTrue(array.contains(-1))
assertTrue(array.contains(2))
assertFalse(array.contains(0))
assertFalse(array.contains(3))
}
@Test
fun testClear() {
val array = GrowingIntArray(10)
array.add(-1)
array.add(2)
array.clear()
assertEquals(0, array.length())
}
@Test
fun testEnsureHasCapacity() {
val array = GrowingIntArray(0)
array.add(42)
array.ensureHasCapacity(2)
assertEquals(3, array.backingArrayLength)
}
@Test
fun testGetShrinkedBackingArray() {
val array = GrowingIntArray(10)
array.add(-1)
array.add(2)
assertContentEquals(intArrayOf(-1, 2), array.shrinkedBackingArray)
}
@Test
fun testToString() {
assertEquals("[]", GrowingIntArray(10).toString())
assertEquals("[1,2,3]", GrowingIntArray(3).apply {
add(1)
add(2)
add(3)
}.toString())
}
}

View file

@ -0,0 +1,181 @@
/*
* 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.net.ConnectivityManager
import android.net.ConnectivityManager.CALLBACK_AVAILABLE
import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
import android.os.Build
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.lang.reflect.Modifier
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
private const val TEST_NETID_1 = 123
// Maximum 16 bits unsigned value
private const val TEST_NETID_2 = 0xffff
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class CallbackQueueTest {
@Test
fun testAddCallback() {
val cbs = listOf(
TEST_NETID_1 to CALLBACK_AVAILABLE,
TEST_NETID_2 to CALLBACK_AVAILABLE,
TEST_NETID_1 to CALLBACK_CAP_CHANGED,
TEST_NETID_1 to CALLBACK_CAP_CHANGED
)
val queue = CallbackQueue(intArrayOf()).apply {
cbs.forEach { addCallback(it.first, it.second) }
}
assertQueueEquals(cbs, queue)
}
@Test
fun testHasCallback() {
val queue = CallbackQueue(intArrayOf()).apply {
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
}
assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_AVAILABLE))
assertTrue(queue.hasCallback(TEST_NETID_2, CALLBACK_AVAILABLE))
assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED))
assertFalse(queue.hasCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED))
assertFalse(queue.hasCallback(1234, CALLBACK_AVAILABLE))
assertFalse(queue.hasCallback(TEST_NETID_1, 5678))
assertFalse(queue.hasCallback(1234, 5678))
}
@Test
fun testRemoveCallbacks() {
val queue = CallbackQueue(intArrayOf()).apply {
assertFalse(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
assertTrue(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
}
assertQueueEquals(listOf(
TEST_NETID_1 to CALLBACK_CAP_CHANGED,
TEST_NETID_2 to CALLBACK_AVAILABLE
), queue)
}
@Test
fun testRemoveCallbacksForNetId() {
val queue = CallbackQueue(intArrayOf()).apply {
assertFalse(removeCallbacksForNetId(TEST_NETID_2))
addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
assertFalse(removeCallbacksForNetId(TEST_NETID_1))
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_2, CALLBACK_IP_CHANGED)
assertTrue(removeCallbacksForNetId(TEST_NETID_2))
}
assertQueueEquals(listOf(
TEST_NETID_1 to CALLBACK_AVAILABLE,
TEST_NETID_1 to CALLBACK_CAP_CHANGED,
TEST_NETID_1 to CALLBACK_AVAILABLE,
), queue)
}
@Test
fun testConstructorFromExistingArray() {
val queue1 = CallbackQueue(intArrayOf()).apply {
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
}
val queue2 = CallbackQueue(queue1.shrinkedBackingArray)
assertQueueEquals(listOf(
TEST_NETID_1 to CALLBACK_AVAILABLE,
TEST_NETID_2 to CALLBACK_AVAILABLE
), queue2)
}
@Test
fun testToString() {
assertEquals("[]", CallbackQueue(intArrayOf()).toString())
assertEquals(
"[CALLBACK_AVAILABLE($TEST_NETID_1)]",
CallbackQueue(intArrayOf()).apply {
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
}.toString()
)
assertEquals(
"[CALLBACK_AVAILABLE($TEST_NETID_1),CALLBACK_CAP_CHANGED($TEST_NETID_2)]",
CallbackQueue(intArrayOf()).apply {
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
}.toString()
)
}
@Test
fun testMaxNetId() {
// CallbackQueue assumes netIds are at most 16 bits
assertTrue(NetIdManager.MAX_NET_ID <= 0xffff)
}
@Test
fun testMaxCallbackId() {
// CallbackQueue assumes callback IDs are at most 16 bits.
val constants = ConnectivityManager::class.java.declaredFields.filter {
Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
it.name.startsWith("CALLBACK_")
}
constants.forEach {
it.isAccessible = true
assertTrue(it.get(null) as Int <= 0xffff)
}
}
}
private fun assertQueueEquals(expected: List<Pair<Int, Int>>, actual: CallbackQueue) {
assertEquals(
expected.size,
actual.length(),
"Size mismatch between expected: $expected and actual: $actual"
)
var nextIndex = 0
actual.forEach { netId, cbId ->
val (expNetId, expCbId) = expected[nextIndex]
val msg = "$actual does not match $expected at index $nextIndex"
assertEquals(expNetId, netId, msg)
assertEquals(expCbId, cbId, msg)
nextIndex++
}
// Ensure forEach iterations and size are consistent
assertEquals(expected.size, nextIndex)
}

View file

@ -2180,6 +2180,7 @@ public class ConnectivityServiceTest {
case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
return true;
default:

View file

@ -165,6 +165,7 @@ open class CSTest {
it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
}
fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)