[connectivity] add NetworkCapabilities.RedactionHelper

Add a new hidden inner class NetworkCapabilities.RedactionHelper for
accessing conditionally available NetworkSpecifier methods via
reflection.

Reflection is necessary because NetworkSpecifier is platform code. So
it's possible for a mainline update to define a
getApplicableRedactions() or redact(long) method in a subclass of
NetworkSpecifier (e.g., EthernetNetworkSpecifier), and then to run
that mainline module on an Android release where the
NetworkSpecifier base class does not have those methods. On such a
device, ConnectivityService must still call those methods if they
exist, otherwise it would fail to redact information that needs to
be redacted. Another way to do this would be for ConnectivityService
to just call the method and catch NoSuchMethodError, but this code
runs fairly often and constructing exceptions is expensive.

This class is not used in this CL but will be used for NetworkSpecifier
redaction in ag/34242202 and ag/34242203

Bug 374037595
Flag: NONE new code nerver used

Change-Id: I8f88c6bb105ebf8d485e18cd214bebf2abe67e63
This commit is contained in:
Kangping Dong 2025-06-30 14:31:14 +08:00
commit 89c94195a0
2 changed files with 229 additions and 0 deletions

View file

@ -19,6 +19,8 @@ package android.net;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder;
import static com.android.net.module.util.BitUtils.describeDifferences;
import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_SPECIFIER_REDACTION_REFLECTION_ERROR;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@ -37,6 +39,7 @@ import android.os.Process;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.LruCache;
import android.util.Range;
import com.android.internal.annotations.VisibleForTesting;
@ -44,15 +47,19 @@ import com.android.modules.utils.build.SdkLevel;
import com.android.net.flags.Flags;
import com.android.net.module.util.BitUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.FrameworkConnectivityStatsLog;
import com.android.net.module.util.NetworkCapabilitiesUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
@ -3519,4 +3526,148 @@ public final class NetworkCapabilities implements Parcelable {
return new NetworkCapabilities(mCaps);
}
}
/**
* Helper class for providing access to {@link NetworkSpecifier} redaction methods via
* reflection.
*
* Reflection is necessary because NetworkSpecifier is platform code. So it's possible for a
* mainline update to define a getApplicableRedactions() or redact(long) method in a subclass
* of NetworkSpecifier (e.g., EthernetNetworkSpecifier), and then to run that mainline module
* on an Android release where the NetworkSpecifier base class does not have those methods.
* On such a device, ConnectivityService must still call those methods if they exist, otherwise
* it would fail to redact information that needs to be redacted. Another way to do this would
* be for ConnectivityService to just call the method and catch NoSuchMethodError, but this
* code runs fairly often and constructing exceptions is expensive.
*
* @hide
*/
public static final class RedactionHelper {
// LruCache is thread-safe so that no locks are used to project it this class
private static final LruCache<Class<? extends NetworkSpecifier>, Optional<Method>>
sGetApplicableRedactionsMethodCache = new LruCache<>(100);
private static final LruCache<Class<? extends NetworkSpecifier>, Optional<Method>>
sRedactLongMethodCache = new LruCache<>(100);
private RedactionHelper() {}
/**
* Returns the {@link Method} of the {@link NetworkSpecifier} class for the given method
* name, or {@code null} if the method doesn't exist.
* <p>
* This is typically used for {@link NetworkSpecifier#getApplicableRedactions()} and {@link
* NetworkSpecifier#redact(long)} to work with new NetworkSpecifier subclasses added in new
* mainline version but deployed to older platforms which don't have those two
* NetworkSpecifier methods.
* <p>
* See {@link #createWithLocationInfoSanitizedIfNecessaryWhenParceled} and {@link
* #ensureSufficientPermissionsForRequest} for how this method should be used.
*/
@Nullable
private static Method getNetworkSpecifierMethod(
Class<? extends NetworkSpecifier> clazz, String name, Class<?> returnType,
Class<?>... parameterTypes) {
// Use {@code getMethods()} rather than {@code getDeclaredMethods()} so that it can
// return the inherited NetworkSpecifier methods for those existing subclasses which
// don't override {@link NetworkSpecifier#getApplicableRedactions()} and {@link
// NetworkSpecifier#redact(long)}.
// Note also {@code getMethods()} returns only public methods, which is necessary for
// code to call the method later.
for (Method method : clazz.getMethods()) {
if (name.equals(method.getName())
&& returnType.equals(method.getReturnType())
&& Arrays.equals(parameterTypes, method.getParameterTypes())) {
return method;
}
}
return null;
}
/**
* Returns the {@link NetworkSpecifier#getApplicableRedactions()} method or {@code null} if
* it doesn't exist for the given {@link NetworkSpecifier} subclass.
*/
@Nullable
private static Method getApplicableRedactionsMethod(
Class<? extends NetworkSpecifier> clazz) {
// If this method is called concurrently from two threads, both may see cache missed
// and the reflection branch in method will be executed for twice. But this is okay
// because getApplicableRedactions() itself has no side-effect (and this is why we can
// create caches for it).
Optional<Method> cache = sGetApplicableRedactionsMethodCache.get(clazz);
if (cache != null) {
return cache.orElse(null);
}
Method method = getNetworkSpecifierMethod(
clazz, "getApplicableRedactions", long.class);
sGetApplicableRedactionsMethodCache.put(clazz, Optional.ofNullable(method));
return method;
}
/**
* Returns the value of {@link NetworkSpecifier#getApplicableRedactions()} or
* {@link REDACT_NONE} if the platform doesn't support
* {@link NetworkSpecifier#getApplicableRedactions()}.
*/
public static long getSpecifierApplicableRedactions(NetworkSpecifier specifier) {
final Method getApplicableRedactions =
getApplicableRedactionsMethod(specifier.getClass());
if (getApplicableRedactions == null) {
return REDACT_NONE;
}
try {
return (long) getApplicableRedactions.invoke(specifier);
} catch (IllegalAccessException | InvocationTargetException
| IllegalArgumentException e) {
FrameworkConnectivityStatsLog.write(CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_SPECIFIER_REDACTION_REFLECTION_ERROR);
Log.wtf(TAG, "Un expected reflection error on specifier " + specifier, e);
return REDACT_ALL;
}
}
/**
* Returns the {@link NetworkSpecifier#redact(long)} method or {@code null} if it doesn't
* exist for the given {@link NetworkSpecifier} subclass.
*/
@Nullable
private static Method getRedactLongMethod(Class<? extends NetworkSpecifier> clazz) {
// If this method is called concurrently from two threads, both may see cache missed
// and the reflection branch in method will be executed for twice. But this is okay
// because redact(long) itself has no side-effect (and this is why we can create caches
// for it).
Optional<Method> cache = sRedactLongMethodCache.get(clazz);
if (cache != null) {
return cache.orElse(null);
}
Method method = getNetworkSpecifierMethod(
clazz, "redact", NetworkSpecifier.class, long.class);
sRedactLongMethodCache.put(clazz, Optional.ofNullable(method));
return method;
}
/**
* Returns the result of {@link NetworkSpecifier#redact(long)} or {@code specifier} itself
* if the platform doesn't support {@link NetworkSpecifier#redact(long)}.
*/
@Nullable
public static NetworkSpecifier getSpecifierRedactResult(
NetworkSpecifier specifier, long redactions) {
final Method redactLong = getRedactLongMethod(specifier.getClass());
if (redactLong == null) {
return specifier;
}
try {
return (NetworkSpecifier) redactLong.invoke(specifier, redactions);
} catch (IllegalAccessException | InvocationTargetException
| IllegalArgumentException e) {
FrameworkConnectivityStatsLog.write(CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_SPECIFIER_REDACTION_REFLECTION_ERROR);
Log.wtf(TAG, "Un expected reflection error on specifier " + specifier, e);
return null;
}
}
}
}

View file

@ -51,6 +51,8 @@ import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
import static android.net.NetworkCapabilities.REDACT_FOR_THREAD_NETWORK_PRIVILEGED;
import static android.net.NetworkCapabilities.REDACT_NONE;
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@ -63,9 +65,11 @@ import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
import static android.os.Process.INVALID_UID;
import static com.android.modules.utils.build.SdkLevel.isAtLeastB;
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
import static com.android.net.thread.platform.flags.Flags.FLAG_THREAD_MOBILE_ENABLED;
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
@ -74,11 +78,15 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.annotation.Nullable;
import android.net.NetworkCapabilities.RedactionHelper;
import android.net.wifi.aware.DiscoverySession;
import android.net.wifi.aware.PeerHandle;
import android.net.wifi.aware.WifiAwareNetworkSpecifier;
@ -99,6 +107,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -1235,6 +1244,44 @@ public class NetworkCapabilitiesTest {
}
}
/**
* Redactable NetworkSpecifier subclass for verifying redaction mechanism.
*/
private static final class RedactableNetworkSpecifier extends NetworkSpecifier {
final boolean locationRedacted;
final boolean threadNetworkRedacted;
RedactableNetworkSpecifier() {
locationRedacted = false;
threadNetworkRedacted = false;
}
RedactableNetworkSpecifier(boolean locationRedacted, boolean threadNetworkRedacted) {
this.locationRedacted = locationRedacted;
this.threadNetworkRedacted = threadNetworkRedacted;
}
@Override
public long getApplicableRedactions() {
return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED;
}
@Override
@Nullable
public NetworkSpecifier redact(long redactions) {
return new RedactableNetworkSpecifier(
(redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0,
(redactions & REDACT_FOR_THREAD_NETWORK_PRIVILEGED) != 0);
}
}
/**
* Unredactable NetworkSpecifier subclass for verifying redaction mechanism.
*/
private static final class UnredactableNetworkSpecifier extends NetworkSpecifier {
// not overriding the getApplicableRedactions() and redact(long) methods
}
@Test
public void testBuilder() {
final int ownerUid = 1001;
@ -1621,4 +1668,35 @@ public class NetworkCapabilitiesTest {
assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
}
}
@Test
public void testRedactionHelper() throws Exception {
final UnredactableNetworkSpecifier unredactableSpecifier =
new UnredactableNetworkSpecifier();
final RedactableNetworkSpecifier redactableSpecifier = new RedactableNetworkSpecifier();
assertEquals(REDACT_NONE,
RedactionHelper.getSpecifierApplicableRedactions(unredactableSpecifier));
assertEquals(unredactableSpecifier,
RedactionHelper.getSpecifierRedactResult(
unredactableSpecifier, REDACT_FOR_ACCESS_FINE_LOCATION));
assertEquals(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED,
RedactionHelper.getSpecifierApplicableRedactions(redactableSpecifier));
// Call getSpecifierApplicableRedactions() twice to make sure the cache works
assertEquals(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED,
RedactionHelper.getSpecifierApplicableRedactions(redactableSpecifier));
final RedactableNetworkSpecifier redactedSpecifier =
(RedactableNetworkSpecifier) RedactionHelper.getSpecifierRedactResult(
redactableSpecifier,
REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED);
assertTrue(redactedSpecifier.locationRedacted);
assertTrue(redactedSpecifier.threadNetworkRedacted);
// Call getSpecifierRedactResult twice to make sure the cache works
final RedactableNetworkSpecifier redactedSpecifier2 =
(RedactableNetworkSpecifier) RedactionHelper.getSpecifierRedactResult(
redactableSpecifier,
REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED);
assertTrue(redactedSpecifier2.locationRedacted);
assertTrue(redactedSpecifier2.threadNetworkRedacted);
}
}