Merge "[Nearby]Fix sometimes can't stop the previous advertising" into main
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 47e2848..6d857b1 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
# Keep class's integer static field for MessageUtils to parsing their name.
-keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
static final % POLICY_*;
@@ -7,18 +10,6 @@
static final % EVENT_*;
}
--keep class com.android.networkstack.tethering.util.BpfMap {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TcUtils {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TetheringUtils {
- native <methods>;
-}
-
# Ensure runtime-visible field annotations are kept when using R8 full mode.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index fa6ce95..6229f6d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -174,10 +174,10 @@
/**
* Request Tethering change.
*
- * @param request the TetheringRequest this IpServer was enabled with.
+ * @param tetheringType the downstream type of this IpServer.
* @param enabled enable or disable tethering.
*/
- public void requestEnableTethering(TetheringRequest request, boolean enabled) { }
+ public void requestEnableTethering(int tetheringType, boolean enabled) { }
}
/** Capture IpServer dependencies, for injection. */
@@ -1189,8 +1189,8 @@
handleNewPrefixRequest((IpPrefix) message.obj);
break;
case CMD_NOTIFY_PREFIX_CONFLICT:
- mLog.i("restart tethering: " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, false /* enabled */);
+ mLog.i("restart tethering: " + mInterfaceType);
+ mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
case CMD_SERVICE_FAILED_TO_START:
@@ -1474,12 +1474,12 @@
case CMD_TETHER_UNREQUESTED:
transitionTo(mInitialState);
mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
mLog.i("Untethered (interface down) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
break;
default:
return false;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 21b420a..4f07f58 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -2232,9 +2232,9 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final boolean enabled = message.arg1 == 1;
- final TetheringRequest request = (TetheringRequest) message.obj;
- enableTetheringInternal(request.getTetheringType(), enabled, null, null);
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null, null);
break;
}
default:
@@ -2812,9 +2812,9 @@
}
@Override
- public void requestEnableTethering(TetheringRequest request, boolean enabled) {
+ public void requestEnableTethering(int tetheringType, boolean enabled) {
mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
- enabled ? 1 : 0, 0, request);
+ tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index c329142..f9e3a6a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -186,9 +186,11 @@
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mBluetoothAddress.getAddress().getAddress()));
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
+ final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
+ releaseDownstream(mHotspotIpServer);
// - Test previous enabled hotspot prefix(cached prefix) is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -207,7 +209,6 @@
assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
assertNotEquals(hotspotPrefix, etherPrefix);
- releaseDownstream(mHotspotIpServer);
releaseDownstream(mEthernetIpServer);
}
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 7c9b3ec..449588a 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -111,6 +111,12 @@
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
+ final int appId = UserHandle.getAppId(callingUid);
+ if (appId == Process.SYSTEM_UID) {
+ // the system can access data usage for all apps on the device.
+ // check system uid first, to avoid possible dead lock from other APIs
+ return NetworkStatsAccess.Level.DEVICE;
+ }
final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -126,16 +132,13 @@
Binder.restoreCallingIdentity(token);
}
- final int appId = UserHandle.getAppId(callingUid);
-
final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
- if (hasCarrierPrivileges || isDeviceOwner
- || appId == Process.SYSTEM_UID || isNetworkStack) {
- // Carrier-privileged apps and device owners, and the system (including the
- // network stack) can access data usage for all apps on the device.
+ if (hasCarrierPrivileges || isDeviceOwner || isNetworkStack) {
+ // Carrier-privileged apps and device owners, and the network stack
+ // can access data usage for all apps on the device.
return NetworkStatsAccess.Level.DEVICE;
}
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index e4b2f43..fd824f3 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -282,7 +282,7 @@
}
/**
- * Get the service type. (e.g. "_http._tcp.local" )
+ * Get the service type. (e.g. "_http._tcp" )
*/
@NonNull
public String getServiceType() {
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 797c107..a9d1569 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -233,6 +233,32 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
}
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class L2capNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeaderCompression();
+ method public int getPsm();
+ method @Nullable public android.net.MacAddress getRemoteAddress();
+ method public int getRole();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.L2capNetworkSpecifier> CREATOR;
+ field public static final int HEADER_COMPRESSION_6LOWPAN = 2; // 0x2
+ field public static final int HEADER_COMPRESSION_ANY = 0; // 0x0
+ field public static final int HEADER_COMPRESSION_NONE = 1; // 0x1
+ field public static final int PSM_ANY = -1; // 0xffffffff
+ field public static final int ROLE_ANY = 0; // 0x0
+ field public static final int ROLE_CLIENT = 1; // 0x1
+ field public static final int ROLE_SERVER = 2; // 0x2
+ }
+
+ public static final class L2capNetworkSpecifier.Builder {
+ ctor public L2capNetworkSpecifier.Builder();
+ method @NonNull public android.net.L2capNetworkSpecifier build();
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@NonNull android.net.MacAddress);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int);
+ }
+
public class LinkAddress implements android.os.Parcelable {
method public int describeContents();
method public java.net.InetAddress getAddress();
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
new file mode 100644
index 0000000..c7067f6
--- /dev/null
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2025 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.net;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify an L2CAP network.
+ *
+ * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral)
+ * and a client (Bluetooth central) node. This specifier contains information required to request or
+ * reserve an L2CAP network.
+ *
+ * An L2CAP server network allocates a PSM to be advertised to the client. Therefore, the server
+ * network must always be reserved using {@link ConnectivityManager#reserveNetwork}. The subsequent
+ * {@link ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} includes information
+ * (i.e. the PSM) for the server to advertise to the client.
+ * Under the hood, an L2CAP server network is represented by a {@link
+ * android.bluetooth.BluetoothServerSocket} which can, in theory, accept many connections. However,
+ * before Android 15 Bluetooth APIs do not expose the channel ID, so these connections are
+ * indistinguishable. In practice, this means that network matching semantics in {@link
+ * ConnectivityService} will tear down all but the first connection.
+ *
+ * The L2cap client network can be connected using {@link ConnectivityManager#requestNetwork}
+ * including passing in the relevant information (i.e. PSM and destination MAC address) using the
+ * {@link L2capNetworkSpecifier}.
+ *
+ */
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+public final class L2capNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /** Accept any role. */
+ public static final int ROLE_ANY = 0;
+ /** Specifier describes a client network. */
+ public static final int ROLE_CLIENT = 1;
+ /** Specifier describes a server network. */
+ public static final int ROLE_SERVER = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ROLE_", value = {
+ ROLE_ANY,
+ ROLE_CLIENT,
+ ROLE_SERVER
+ })
+ public @interface Role {}
+ /** Role used to distinguish client from server networks. */
+ @Role
+ private final int mRole;
+
+ /** Accept any form of header compression. */
+ public static final int HEADER_COMPRESSION_ANY = 0;
+ /** Do not compress packets on this network. */
+ public static final int HEADER_COMPRESSION_NONE = 1;
+ /** Use 6lowpan header compression as specified in rfc6282. */
+ public static final int HEADER_COMPRESSION_6LOWPAN = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "HEADER_COMPRESSION_", value = {
+ HEADER_COMPRESSION_ANY,
+ HEADER_COMPRESSION_NONE,
+ HEADER_COMPRESSION_6LOWPAN
+ })
+ public @interface HeaderCompression {}
+ /** Header compression mechanism used on this network. */
+ @HeaderCompression
+ private final int mHeaderCompression;
+
+ /**
+ * The MAC address of the remote.
+ */
+ @Nullable
+ private final MacAddress mRemoteAddress;
+
+ /** Match any PSM. */
+ public static final int PSM_ANY = -1;
+
+ /** The Bluetooth L2CAP Protocol Service Multiplexer (PSM). */
+ private final int mPsm;
+
+ private L2capNetworkSpecifier(Parcel in) {
+ mRole = in.readInt();
+ mHeaderCompression = in.readInt();
+ mRemoteAddress = in.readParcelable(getClass().getClassLoader());
+ mPsm = in.readInt();
+ }
+
+ /** @hide */
+ public L2capNetworkSpecifier(@Role int role, @HeaderCompression int headerCompression,
+ MacAddress remoteAddress, int psm) {
+ mRole = role;
+ mHeaderCompression = headerCompression;
+ mRemoteAddress = remoteAddress;
+ mPsm = psm;
+ }
+
+ /** Returns the role to be used for this network. */
+ @Role
+ public int getRole() {
+ return mRole;
+ }
+
+ /** Returns the compression mechanism for this network. */
+ @HeaderCompression
+ public int getHeaderCompression() {
+ return mHeaderCompression;
+ }
+
+ /** Returns the remote MAC address for this network to connect to. */
+ public @Nullable MacAddress getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ /** Returns the PSM for this network to connect to. */
+ public int getPsm() {
+ return mPsm;
+ }
+
+ /** A builder class for L2capNetworkSpecifier. */
+ public static final class Builder {
+ @Role
+ private int mRole;
+ @HeaderCompression
+ private int mHeaderCompression;
+ @Nullable
+ private MacAddress mRemoteAddress;
+ private int mPsm = PSM_ANY;
+
+ /**
+ * Set the role to use for this network.
+ *
+ * @param role the role to use.
+ */
+ @NonNull
+ public Builder setRole(@Role int role) {
+ mRole = role;
+ return this;
+ }
+
+ /**
+ * Set the header compression mechanism to use for this network.
+ *
+ * @param headerCompression the header compression mechanism to use.
+ */
+ @NonNull
+ public Builder setHeaderCompression(@HeaderCompression int headerCompression) {
+ mHeaderCompression = headerCompression;
+ return this;
+ }
+
+ /**
+ * Set the remote address for the client to connect to.
+ *
+ * Only valid for client networks. A null MacAddress matches *any* MacAddress.
+ *
+ * @param remoteAddress the MAC address to connect to.
+ */
+ @NonNull
+ public Builder setRemoteAddress(@NonNull MacAddress remoteAddress) {
+ Objects.requireNonNull(remoteAddress);
+ mRemoteAddress = remoteAddress;
+ return this;
+ }
+
+ /**
+ * Set the PSM for the client to connect to.
+ *
+ * Can only be configured on client networks.
+ *
+ * @param psm the Protocol Service Multiplexer (PSM) to connect to.
+ */
+ @NonNull
+ public Builder setPsm(int psm) {
+ mPsm = psm;
+ return this;
+ }
+
+ /** Create the L2capNetworkSpecifier object. */
+ @NonNull
+ public L2capNetworkSpecifier build() {
+ // TODO: throw an exception for combinations that cannot be supported.
+ return new L2capNetworkSpecifier(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ // TODO: implement matching semantics.
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable
+ public NetworkSpecifier redact() {
+ // Redact the remote MAC address and the PSM (for non-server roles).
+ final NetworkSpecifier redactedSpecifier = new Builder()
+ .setRole(mRole)
+ .setHeaderCompression(mHeaderCompression)
+ // TODO: consider not redacting the specifier in onReserved, so the redaction can be
+ // more strict (i.e. the PSM could always be redacted).
+ .setPsm(mRole == ROLE_SERVER ? mPsm : PSM_ANY)
+ .build();
+ return redactedSpecifier;
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+
+ /** @hide */
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof L2capNetworkSpecifier)) return false;
+
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) obj;
+ return mRole == rhs.mRole
+ && mHeaderCompression == rhs.mHeaderCompression
+ && Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mPsm == rhs.mPsm;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mRole);
+ dest.writeInt(mHeaderCompression);
+ dest.writeParcelable(mRemoteAddress, flags);
+ dest.writeInt(mPsm);
+ }
+
+ public static final @NonNull Creator<L2capNetworkSpecifier> CREATOR = new Creator<>() {
+ @Override
+ public L2capNetworkSpecifier createFromParcel(Parcel in) {
+ return new L2capNetworkSpecifier(in);
+ }
+
+ @Override
+ public L2capNetworkSpecifier[] newArray(int size) {
+ return new L2capNetworkSpecifier[size];
+ }
+ };
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index b95363a..5a08d44 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -282,6 +282,13 @@
this.type = that.type;
}
+ private NetworkRequest(Parcel in) {
+ networkCapabilities = NetworkCapabilities.CREATOR.createFromParcel(in);
+ legacyType = in.readInt();
+ requestId = in.readInt();
+ type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
+ }
+
/**
* Builder used to create {@link NetworkRequest} objects. Specify the Network features
* needed in terms of {@link NetworkCapabilities} features
@@ -678,12 +685,7 @@
public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
new Creator<NetworkRequest>() {
public NetworkRequest createFromParcel(Parcel in) {
- NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
- int legacyType = in.readInt();
- int requestId = in.readInt();
- Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
- NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
- return result;
+ return new NetworkRequest(in);
}
public NetworkRequest[] newArray(int size) {
return new NetworkRequest[size];
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 79123ee..764300c 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -16,16 +16,10 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
import android.annotation.RequiresApi;
import android.app.DownloadManager;
@@ -41,12 +35,12 @@
import com.android.server.net.ct.DownloadHelper.DownloadStatus;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.util.ArrayList;
+import java.util.List;
/** Helper class to download certificate transparency log files. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -58,38 +52,60 @@
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
private final SignatureVerifier mSignatureVerifier;
- private final CertificateTransparencyInstaller mInstaller;
private final CertificateTransparencyLogger mLogger;
+ private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
+
+ private boolean started = false;
+
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
SignatureVerifier signatureVerifier,
- CertificateTransparencyInstaller installer,
CertificateTransparencyLogger logger) {
mContext = context;
mSignatureVerifier = signatureVerifier;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
- mInstaller = installer;
mLogger = logger;
}
- void initialize() {
- mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+ void addCompatibilityVersion(CompatibilityVersion compatVersion) {
+ mCompatVersions.add(compatVersion);
+ }
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
- mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
+ void start() {
+ if (started) {
+ return;
+ }
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ Context.RECEIVER_EXPORTED);
+ mDataStore.load();
+ started = true;
if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ Log.d(TAG, "CertificateTransparencyDownloader started.");
+ }
+ }
+
+ void stop() {
+ if (!started) {
+ return;
+ }
+ mContext.unregisterReceiver(this);
+ mDataStore.delete();
+ started = false;
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader stopped.");
}
}
long startPublicKeyDownload() {
- long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+ long downloadId = download(Config.URL_PUBLIC_KEY);
if (downloadId != -1) {
mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
mDataStore.store();
@@ -97,19 +113,31 @@
return downloadId;
}
- long startMetadataDownload() {
- long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+ private long startMetadataDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getMetadataUrl());
if (downloadId != -1) {
- mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+ mDataStore.setPropertyLong(compatVersion.getMetadataPropertyName(), downloadId);
mDataStore.store();
}
return downloadId;
}
- long startContentDownload() {
- long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+ @VisibleForTesting
+ void startMetadataDownload() {
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (startMetadataDownload(compatVersion) == -1) {
+ Log.e(TAG, "Metadata download not started for " + compatVersion.getCompatVersion());
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started for " + compatVersion.getCompatVersion());
+ }
+ }
+ }
+
+ @VisibleForTesting
+ long startContentDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getContentUrl());
if (downloadId != -1) {
- mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+ mDataStore.setPropertyLong(compatVersion.getContentPropertyName(), downloadId);
mDataStore.store();
}
return downloadId;
@@ -123,25 +151,28 @@
return;
}
- long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ long completedId =
+ intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, /* defaultValue= */ -1);
if (completedId == -1) {
Log.e(TAG, "Invalid completed download Id");
return;
}
- if (isPublicKeyDownloadId(completedId)) {
+ if (getPublicKeyDownloadId() == completedId) {
handlePublicKeyDownloadCompleted(completedId);
return;
}
- if (isMetadataDownloadId(completedId)) {
- handleMetadataDownloadCompleted(completedId);
- return;
- }
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (getMetadataDownloadId(compatVersion) == completedId) {
+ handleMetadataDownloadCompleted(compatVersion, completedId);
+ return;
+ }
- if (isContentDownloadId(completedId)) {
- handleContentDownloadCompleted(completedId);
- return;
+ if (getContentDownloadId(compatVersion) == completedId) {
+ handleContentDownloadCompleted(compatVersion, completedId);
+ return;
+ }
}
Log.i(TAG, "Download id " + completedId + " is not recognized.");
@@ -167,73 +198,84 @@
return;
}
- if (startMetadataDownload() == -1) {
- Log.e(TAG, "Metadata download not started.");
- } else if (Config.DEBUG) {
- Log.d(TAG, "Metadata download started successfully.");
- }
+ startMetadataDownload();
}
- private void handleMetadataDownloadCompleted(long downloadId) {
+ private void handleMetadataDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
if (!status.isSuccessful()) {
handleDownloadFailed(status);
return;
}
- if (startContentDownload() == -1) {
- Log.e(TAG, "Content download not started.");
+ if (startContentDownload(compatVersion) == -1) {
+ Log.e(TAG, "Content download failed for" + compatVersion.getCompatVersion());
} else if (Config.DEBUG) {
- Log.d(TAG, "Content download started successfully.");
+ Log.d(TAG, "Content download started for" + compatVersion.getCompatVersion());
}
}
- private void handleContentDownloadCompleted(long downloadId) {
+ private void handleContentDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
if (!status.isSuccessful()) {
handleDownloadFailed(status);
return;
}
- Uri contentUri = getContentDownloadUri();
- Uri metadataUri = getMetadataDownloadUri();
+ Uri contentUri = getContentDownloadUri(compatVersion);
+ Uri metadataUri = getMetadataDownloadUri(compatVersion);
if (contentUri == null || metadataUri == null) {
Log.e(TAG, "Invalid URIs");
return;
}
boolean success = false;
+ boolean failureLogged = false;
+
try {
success = mSignatureVerifier.verify(contentUri, metadataUri);
+ } catch (MissingPublicKeyException e) {
+ if (updateFailureCount()) {
+ failureLogged = true;
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ }
+ } catch (InvalidKeyException e) {
+ if (updateFailureCount()) {
+ failureLogged = true;
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ }
} catch (IOException | GeneralSecurityException e) {
Log.e(TAG, "Could not verify new log list", e);
}
+
if (!success) {
Log.w(TAG, "Log list did not pass verification");
- // TODO(b/384931263): add logging for failed signature verification
- return;
- }
-
- String version = null;
- try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- version = new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
- .getString("version");
- } catch (JSONException | IOException e) {
- Log.e(TAG, "Could not extract version from log list", e);
+ // Avoid logging failure twice
+ if (!failureLogged && updateFailureCount()) {
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ }
return;
}
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
+ success = compatVersion.install(inputStream);
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
}
if (success) {
- // Update information about the stored version on successful install.
- mDataStore.setProperty(Config.VERSION, version);
-
// Reset the number of consecutive log list failure updates back to zero.
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
mDataStore.store();
@@ -242,7 +284,7 @@
mLogger.logCTLogListUpdateFailedEvent(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
}
}
}
@@ -251,57 +293,32 @@
Log.e(TAG, "Download failed with " + status);
if (updateFailureCount()) {
- int failureCount = mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+ int failureCount =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
- // HTTP Error
- if (400 <= status.reason() && status.reason() <= 600) {
+ if (status.isHttpError()) {
mLogger.logCTLogListUpdateFailedEvent(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR,
failureCount,
- status.reason()
- );
+ status.reason());
} else {
// TODO(b/384935059): handle blocked domain logging
- // TODO(b/384936292): add additionalchecks for pending wifi status
- mLogger.logCTLogListUpdateFailedEvent(
- downloadStatusToFailureReason(status.reason()),
- failureCount
- );
+ mLogger.logCTLogListUpdateFailedEventWithDownloadStatus(
+ status.reason(), failureCount);
}
}
}
- /** Converts DownloadStatus reason into failure reason to log. */
- private int downloadStatusToFailureReason(int downloadStatusReason) {
- switch(downloadStatusReason) {
- case DownloadManager.PAUSED_WAITING_TO_RETRY:
- case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
- case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
- case DownloadManager.ERROR_HTTP_DATA_ERROR:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
- case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
- case DownloadManager.ERROR_CANNOT_RESUME:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
- case DownloadManager.ERROR_INSUFFICIENT_SPACE:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
- case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
- default:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
- }
- }
-
/**
* Updates the data store with the current number of consecutive log list update failures.
*
* @return whether the failure count exceeds the threshold and should be logged.
*/
private boolean updateFailureCount() {
- int failure_count = mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+ int failure_count =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
int new_failure_count = failure_count + 1;
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
@@ -309,8 +326,7 @@
boolean shouldReport = new_failure_count >= Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD;
if (shouldReport) {
- Log.d(TAG,
- "Log list update failure count exceeds threshold: " + new_failure_count);
+ Log.d(TAG, "Log list update failure count exceeds threshold: " + new_failure_count);
}
return shouldReport;
}
@@ -326,17 +342,19 @@
@VisibleForTesting
long getPublicKeyDownloadId() {
- return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+ return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, /* defaultValue= */ -1);
}
@VisibleForTesting
- long getMetadataDownloadId() {
- return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+ long getMetadataDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getMetadataPropertyName(), /* defaultValue */ -1);
}
@VisibleForTesting
- long getContentDownloadId() {
- return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+ long getContentDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getContentPropertyName(), /* defaultValue= */ -1);
}
@VisibleForTesting
@@ -346,38 +364,27 @@
@VisibleForTesting
boolean hasMetadataDownloadId() {
- return getMetadataDownloadId() != -1;
+ return mCompatVersions.stream()
+ .map(this::getMetadataDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
}
@VisibleForTesting
boolean hasContentDownloadId() {
- return getContentDownloadId() != -1;
- }
-
- @VisibleForTesting
- boolean isPublicKeyDownloadId(long downloadId) {
- return getPublicKeyDownloadId() == downloadId;
- }
-
- @VisibleForTesting
- boolean isMetadataDownloadId(long downloadId) {
- return getMetadataDownloadId() == downloadId;
- }
-
- @VisibleForTesting
- boolean isContentDownloadId(long downloadId) {
- return getContentDownloadId() == downloadId;
+ return mCompatVersions.stream()
+ .map(this::getContentDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
}
private Uri getPublicKeyDownloadUri() {
return mDownloadHelper.getUri(getPublicKeyDownloadId());
}
- private Uri getMetadataDownloadUri() {
- return mDownloadHelper.getUri(getMetadataDownloadId());
+ private Uri getMetadataDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getMetadataDownloadId(compatVersion));
}
- private Uri getContentDownloadUri() {
- return mDownloadHelper.getUri(getContentDownloadId());
+ private Uri getContentDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getContentDownloadId(compatVersion));
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
deleted file mode 100644
index 3138ea7..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.net.ct;
-
-import android.annotation.RequiresApi;
-import android.os.Build;
-import android.provider.DeviceConfig;
-import android.provider.DeviceConfig.Properties;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.security.GeneralSecurityException;
-import java.util.concurrent.Executors;
-
-/** Listener class for the Certificate Transparency Phenotype flags. */
-@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
-
- private static final String TAG = "CertificateTransparencyFlagsListener";
-
- private final DataStore mDataStore;
- private final SignatureVerifier mSignatureVerifier;
- private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
- CertificateTransparencyFlagsListener(
- DataStore dataStore,
- SignatureVerifier signatureVerifier,
- CertificateTransparencyDownloader certificateTransparencyDownloader) {
- mDataStore = dataStore;
- mSignatureVerifier = signatureVerifier;
- mCertificateTransparencyDownloader = certificateTransparencyDownloader;
- }
-
- void initialize() {
- mDataStore.load();
- mCertificateTransparencyDownloader.initialize();
- DeviceConfig.addOnPropertiesChangedListener(
- Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
- if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
- }
- // TODO: handle property changes triggering on boot before registering this listener.
- }
-
- @Override
- public void onPropertiesChanged(Properties properties) {
- if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
- return;
- }
-
- String newPublicKey =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_PUBLIC_KEY,
- /* defaultValue= */ "");
- String newVersion =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_VERSION,
- /* defaultValue= */ "");
- String newContentUrl =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_CONTENT_URL,
- /* defaultValue= */ "");
- String newMetadataUrl =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_METADATA_URL,
- /* defaultValue= */ "");
- if (TextUtils.isEmpty(newPublicKey)
- || TextUtils.isEmpty(newVersion)
- || TextUtils.isEmpty(newContentUrl)
- || TextUtils.isEmpty(newMetadataUrl)) {
- return;
- }
-
- if (Config.DEBUG) {
- Log.d(TAG, "newPublicKey=" + newPublicKey);
- Log.d(TAG, "newVersion=" + newVersion);
- Log.d(TAG, "newContentUrl=" + newContentUrl);
- Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
- }
-
- String oldVersion = mDataStore.getProperty(Config.VERSION);
- String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
- String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
-
- if (TextUtils.equals(newVersion, oldVersion)
- && TextUtils.equals(newContentUrl, oldContentUrl)
- && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
- Log.i(TAG, "No flag changed, ignoring update");
- return;
- }
-
- try {
- mSignatureVerifier.setPublicKey(newPublicKey);
- } catch (GeneralSecurityException | IllegalArgumentException e) {
- Log.e(TAG, "Error setting the public Key", e);
- return;
- }
-
- // TODO: handle the case where there is already a pending download.
-
- mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
- mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
- mDataStore.store();
-
- if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
- Log.e(TAG, "Metadata download not started.");
- } else if (Config.DEBUG) {
- Log.d(TAG, "Metadata download started successfully.");
- }
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
deleted file mode 100644
index 9970667..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.net.ct;
-
-import android.util.Log;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Installer of CT log lists. */
-public class CertificateTransparencyInstaller {
-
- private static final String TAG = "CertificateTransparencyInstaller";
-
- private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
-
- // The CT root directory.
- private final File mRootDirectory;
-
- public CertificateTransparencyInstaller(File rootDirectory) {
- mRootDirectory = rootDirectory;
- }
-
- public CertificateTransparencyInstaller(String rootDirectoryPath) {
- this(new File(rootDirectoryPath));
- }
-
- public CertificateTransparencyInstaller() {
- this(Config.CT_ROOT_DIRECTORY_PATH);
- }
-
- void addCompatibilityVersion(String versionName) {
- removeCompatibilityVersion(versionName);
- CompatibilityVersion newCompatVersion =
- new CompatibilityVersion(new File(mRootDirectory, versionName));
- mCompatVersions.put(versionName, newCompatVersion);
- }
-
- void removeCompatibilityVersion(String versionName) {
- CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
- if (compatVersion != null && !compatVersion.delete()) {
- Log.w(TAG, "Could not delete compatibility version directory.");
- }
- }
-
- CompatibilityVersion getCompatibilityVersion(String versionName) {
- return mCompatVersions.get(versionName);
- }
-
- /**
- * Install a new log list to use during SCT verification.
- *
- * @param compatibilityVersion the compatibility version of the new log list
- * @param newContent an input stream providing the log list
- * @param version the minor version of the new log list
- * @return true if the log list was installed successfully, false otherwise.
- * @throws IOException if the list cannot be saved in the CT directory.
- */
- public boolean install(String compatibilityVersion, InputStream newContent, String version)
- throws IOException {
- CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
- if (compatVersion == null) {
- Log.e(TAG, "No compatibility version for " + compatibilityVersion);
- return false;
- }
- // Ensure root directory exists and is readable.
- DirectoryUtils.makeDir(mRootDirectory);
-
- if (!compatVersion.install(newContent, version)) {
- Log.e(TAG, "Failed to install logs version " + version);
- return false;
- }
- Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
- return true;
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index abede87..a8acc60 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.ConfigUpdate;
import android.os.SystemClock;
@@ -32,26 +33,38 @@
public class CertificateTransparencyJob extends BroadcastReceiver {
private static final String TAG = "CertificateTransparencyJob";
+ private static final String UPDATE_CONFIG_PERMISSION = "android.permission.UPDATE_CONFIG";
private final Context mContext;
- private final DataStore mDataStore;
+ private final CompatibilityVersion mCompatVersion;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private final AlarmManager mAlarmManager;
+ private final PendingIntent mPendingIntent;
private boolean mDependenciesReady = false;
/** Creates a new {@link CertificateTransparencyJob} object. */
public CertificateTransparencyJob(
- Context context,
- DataStore dataStore,
- CertificateTransparencyDownloader certificateTransparencyDownloader) {
+ Context context, CertificateTransparencyDownloader certificateTransparencyDownloader) {
mContext = context;
- mDataStore = dataStore;
+ mCompatVersion =
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION,
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ Config.CT_ROOT_DIRECTORY_PATH);
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
mAlarmManager = context.getSystemService(AlarmManager.class);
+ mPendingIntent =
+ PendingIntent.getBroadcast(
+ mContext,
+ /* requestCode= */ 0,
+ new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ PendingIntent.FLAG_IMMUTABLE);
}
- void initialize() {
+ void schedule() {
mContext.registerReceiver(
this,
new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
@@ -60,14 +73,22 @@
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
AlarmManager.INTERVAL_DAY,
- PendingIntent.getBroadcast(
- mContext,
- 0,
- new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
- PendingIntent.FLAG_IMMUTABLE));
+ mPendingIntent);
if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+ Log.d(TAG, "CertificateTransparencyJob scheduled.");
+ }
+ }
+
+ void cancel() {
+ mContext.unregisterReceiver(this);
+ mAlarmManager.cancel(mPendingIntent);
+ mCertificateTransparencyDownloader.stop();
+ mCompatVersion.delete();
+ mDependenciesReady = false;
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob canceled.");
}
}
@@ -77,20 +98,19 @@
Log.w(TAG, "Received unexpected broadcast with action " + intent);
return;
}
+ if (context.checkCallingOrSelfPermission(UPDATE_CONFIG_PERMISSION)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Caller does not have UPDATE_CONFIG permission.");
+ return;
+ }
if (Config.DEBUG) {
Log.d(TAG, "Starting CT daily job.");
}
if (!mDependenciesReady) {
- mDataStore.load();
- mCertificateTransparencyDownloader.initialize();
+ mCertificateTransparencyDownloader.start();
mDependenciesReady = true;
}
- mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
- mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
- mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
- mDataStore.store();
-
if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
Log.e(TAG, "Public key download not started.");
} else if (Config.DEBUG) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
index 93493c2..913c472 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -16,22 +16,24 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED;
+/** Interface with logging to statsd for Certificate Transparency. */
+public interface CertificateTransparencyLogger {
-/** Helper class to interface with logging to statsd. */
-public class CertificateTransparencyLogger {
-
- public CertificateTransparencyLogger() {}
+ /**
+ * Logs a CTLogListUpdateFailed event to statsd, when failure is provided by DownloadManager.
+ *
+ * @param downloadStatus DownloadManager failure status why the log list wasn't updated
+ * @param failureCount number of consecutive log list update failures
+ */
+ void logCTLogListUpdateFailedEventWithDownloadStatus(int downloadStatus, int failureCount);
/**
* Logs a CTLogListUpdateFailed event to statsd, when no HTTP error status code is present.
*
- * @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
+ * @param failureReason reason why the log list wasn't updated
* @param failureCount number of consecutive log list update failures
*/
- public void logCTLogListUpdateFailedEvent(int failureReason, int failureCount) {
- logCTLogListUpdateFailedEvent(failureReason, failureCount, /* httpErrorStatusCode= */ 0);
- }
+ void logCTLogListUpdateFailedEvent(int failureReason, int failureCount);
/**
* Logs a CTLogListUpdateFailed event to statsd, when an HTTP error status code is provided.
@@ -40,13 +42,7 @@
* @param failureCount number of consecutive log list update failures
* @param httpErrorStatusCode if relevant, the HTTP error status code from DownloadManager
*/
- public void logCTLogListUpdateFailedEvent(
- int failureReason, int failureCount, int httpErrorStatusCode) {
- CertificateTransparencyStatsLog.write(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED,
- failureReason,
- failureCount,
- httpErrorStatusCode
- );
- }
-}
+ void logCTLogListUpdateFailedEvent(
+ int failureReason, int failureCount, int httpErrorStatusCode);
+
+}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
new file mode 100644
index 0000000..b97a885
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -0,0 +1,77 @@
+/*
+ * 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.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
+
+import android.app.DownloadManager;
+
+/** Implementation for logging to statsd for Certificate Transparency. */
+class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
+
+ @Override
+ public void logCTLogListUpdateFailedEventWithDownloadStatus(
+ int downloadStatus, int failureCount) {
+ logCTLogListUpdateFailedEvent(downloadStatusToFailureReason(downloadStatus), failureCount);
+ }
+
+ @Override
+ public void logCTLogListUpdateFailedEvent(int failureReason, int failureCount) {
+ logCTLogListUpdateFailedEvent(failureReason, failureCount, /* httpErrorStatusCode= */ 0);
+ }
+
+ @Override
+ public void logCTLogListUpdateFailedEvent(
+ int failureReason, int failureCount, int httpErrorStatusCode) {
+ CertificateTransparencyStatsLog.write(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED,
+ failureReason,
+ failureCount,
+ httpErrorStatusCode
+ );
+ }
+
+ /** Converts DownloadStatus reason into failure reason to log. */
+ private int downloadStatusToFailureReason(int downloadStatusReason) {
+ switch (downloadStatusReason) {
+ case DownloadManager.PAUSED_WAITING_TO_RETRY:
+ case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
+ case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
+ case DownloadManager.ERROR_HTTP_DATA_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
+ case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+ case DownloadManager.ERROR_CANNOT_RESUME:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
+ case DownloadManager.ERROR_INSUFFICIENT_SPACE:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+ case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
+ }
+ }
+
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 2a27204..ed98056 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -18,7 +18,6 @@
import static android.security.Flags.certificateTransparencyConfiguration;
-import static com.android.net.ct.flags.Flags.certificateTransparencyJob;
import static com.android.net.ct.flags.Flags.certificateTransparencyService;
import android.annotation.RequiresApi;
@@ -26,26 +25,29 @@
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.util.Log;
import com.android.server.SystemService;
+import java.util.concurrent.Executors;
+
/** Implementation of the Certificate Transparency service. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
+ implements DeviceConfig.OnPropertiesChangedListener {
- private final CertificateTransparencyFlagsListener mFlagsListener;
+ private static final String TAG = "CertificateTransparencyService";
+
private final CertificateTransparencyJob mCertificateTransparencyJob;
+ private boolean started = false;
+
/**
* @return true if the CertificateTransparency service is enabled.
*/
public static boolean enabled(Context context) {
- return DeviceConfig.getBoolean(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_SERVICE_ENABLED,
- /* defaultValue= */ true)
- && certificateTransparencyService()
- && certificateTransparencyConfiguration();
+ return certificateTransparencyService() && certificateTransparencyConfiguration();
}
/** Creates a new {@link CertificateTransparencyService} object. */
@@ -59,12 +61,8 @@
dataStore,
downloadHelper,
signatureVerifier,
- new CertificateTransparencyInstaller(),
- new CertificateTransparencyLogger());
- mFlagsListener =
- new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
- mCertificateTransparencyJob =
- new CertificateTransparencyJob(context, dataStore, downloader);
+ new CertificateTransparencyLoggerImpl());
+ mCertificateTransparencyJob = new CertificateTransparencyJob(context, downloader);
}
/**
@@ -75,13 +73,50 @@
public void onBootPhase(int phase) {
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- if (certificateTransparencyJob()) {
- mCertificateTransparencyJob.initialize();
- } else {
- mFlagsListener.initialize();
- }
+ DeviceConfig.addOnPropertiesChangedListener(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Executors.newSingleThreadExecutor(),
+ this);
+ onPropertiesChanged(
+ new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
break;
default:
}
}
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+ return;
+ }
+
+ if (DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_SERVICE_ENABLED,
+ /* defaultValue= */ true)) {
+ startService();
+ } else {
+ stopService();
+ }
+ }
+
+ private void startService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService start");
+ }
+ if (!started) {
+ mCertificateTransparencyJob.schedule();
+ started = true;
+ }
+ }
+
+ private void stopService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService stop");
+ }
+ if (started) {
+ mCertificateTransparencyJob.cancel();
+ started = false;
+ }
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 27488b5..fdeb746 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -15,58 +15,92 @@
*/
package com.android.server.net.ct;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/** Represents a compatibility version directory. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CompatibilityVersion {
+ private static final String TAG = "CompatibilityVersion";
+
static final String LOGS_DIR_PREFIX = "logs-";
static final String LOGS_LIST_FILE_NAME = "log_list.json";
+ static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
- private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+ private final String mCompatVersion;
- private final File mRootDirectory;
+ private final String mMetadataUrl;
+ private final String mContentUrl;
+ private final File mVersionDirectory;
private final File mCurrentLogsDirSymlink;
- private File mCurrentLogsDir = null;
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+ mCompatVersion = compatVersion;
+ mMetadataUrl = metadataUrl;
+ mContentUrl = contentUrl;
+ mVersionDirectory = new File(rootDirectory, compatVersion);
+ mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ }
- CompatibilityVersion(File rootDirectory) {
- mRootDirectory = rootDirectory;
- mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
+ this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
}
/**
* Installs a log list within this compatibility version directory.
*
* @param newContent an input stream providing the log list
- * @param version the version number of the log list
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- boolean install(InputStream newContent, String version) throws IOException {
- // To support atomically replacing the old configuration directory with the new there's a
- // bunch of steps. We create a new directory with the logs and then do an atomic update of
- // the current symlink to point to the new directory.
- // 1. Ensure that the root directory exists and is readable.
- DirectoryUtils.makeDir(mRootDirectory);
+ boolean install(InputStream newContent) throws IOException {
+ String content = new String(newContent.readAllBytes(), UTF_8);
+ try {
+ return install(
+ new ByteArrayInputStream(content.getBytes()),
+ new JSONObject(content).getString("version"));
+ } catch (JSONException e) {
+ Log.e(TAG, "invalid log list format", e);
+ return false;
+ }
+ }
- File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+ private boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new
+ // there's a bunch of steps. We create a new directory with the logs and then do
+ // an atomic update of the current symlink to point to the new directory.
+ // 1. Ensure the path to the root directory exists and is readable.
+ DirectoryUtils.makeDir(mVersionDirectory);
+
+ File newLogsDir = new File(mVersionDirectory, LOGS_DIR_PREFIX + version);
// 2. Handle the corner case where the new directory already exists.
if (newLogsDir.exists()) {
- // If the symlink has already been updated then the update died between steps 6 and 7
- // and so we cannot delete the directory since it is in use.
+ // If the symlink has already been updated then the update died between steps 6
+ // and 7 and so we cannot delete the directory since it is in use.
if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+ Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
return false;
}
- // If the symlink has not been updated then the previous installation failed and this is
- // a re-attempt. Clean-up leftover files and try again.
+ // If the symlink has not been updated then the previous installation failed and
+ // this is a re-attempt. Clean-up leftover files and try again.
DirectoryUtils.removeDir(newLogsDir);
}
try {
@@ -80,8 +114,8 @@
}
DirectoryUtils.setWorldReadable(logListFile);
- // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
- File tempSymlink = new File(mRootDirectory, "new_symlink");
+ // 5. Create temp symlink. We rename to the target symlink for an atomic update.
+ File tempSymlink = new File(mVersionDirectory, "new_symlink");
try {
Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
} catch (ErrnoException e) {
@@ -95,17 +129,33 @@
throw e;
}
// 7. Cleanup
- mCurrentLogsDir = newLogsDir;
+ Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
return true;
}
- File getRootDir() {
- return mRootDirectory;
+ String getCompatVersion() {
+ return mCompatVersion;
}
- File getLogsDir() {
- return mCurrentLogsDir;
+ String getMetadataUrl() {
+ return mMetadataUrl;
+ }
+
+ String getMetadataPropertyName() {
+ return mCompatVersion + "_" + Config.METADATA_DOWNLOAD_ID;
+ }
+
+ String getContentUrl() {
+ return mContentUrl;
+ }
+
+ String getContentPropertyName() {
+ return mCompatVersion + "_" + Config.CONTENT_DOWNLOAD_ID;
+ }
+
+ File getVersionDir() {
+ return mVersionDirectory;
}
File getLogsDirSymlink() {
@@ -113,19 +163,21 @@
}
File getLogsFile() {
- return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+ return new File(mCurrentLogsDirSymlink, LOGS_LIST_FILE_NAME);
}
- boolean delete() {
- return DirectoryUtils.removeDir(mRootDirectory);
+ void delete() {
+ if (!DirectoryUtils.removeDir(mVersionDirectory)) {
+ Log.w(TAG, "Could not delete compatibility version directory " + mVersionDirectory);
+ }
}
private void deleteOldLogDirectories() throws IOException {
- if (!mRootDirectory.exists()) {
+ if (!mVersionDirectory.exists()) {
return;
}
File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
- for (File file : mRootDirectory.listFiles()) {
+ for (File file : mVersionDirectory.listFiles()) {
if (!currentTarget.equals(file.getCanonicalFile())
&& file.getName().startsWith(LOGS_DIR_PREFIX)) {
DirectoryUtils.removeDir(file);
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index aafee60..592bc4e 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -48,11 +48,8 @@
// properties
static final String VERSION = "version";
- static final String CONTENT_URL = "content_url";
static final String CONTENT_DOWNLOAD_ID = "content_download_id";
- static final String METADATA_URL = "metadata_url";
static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
- static final String PUBLIC_KEY_URL = "public_key_url";
static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
index 3779269..8180316 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -57,6 +57,11 @@
}
}
+ boolean delete() {
+ clear();
+ return mPropertyFile.delete();
+ }
+
long getPropertyLong(String key, long defaultValue) {
return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
index ba42a82..54e277a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
@@ -25,7 +25,7 @@
class DirectoryUtils {
static void makeDir(File dir) throws IOException {
- dir.mkdir();
+ dir.mkdirs();
if (!dir.isDirectory()) {
throw new IOException("Unable to make directory " + dir.getCanonicalPath());
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
new file mode 100644
index 0000000..80607f6
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
@@ -0,0 +1,26 @@
+/*
+ * 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.net.ct;
+
+/**
+ * An exception thrown when the public key is missing for CT signature verification.
+ */
+public class MissingPublicKeyException extends Exception {
+
+ public MissingPublicKeyException(String message) {
+ super(message);
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 0b775ca..96488fc 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -27,7 +27,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
@@ -74,9 +73,10 @@
mPublicKey = Optional.of(publicKey);
}
- boolean verify(Uri file, Uri signature) throws GeneralSecurityException, IOException {
+ boolean verify(Uri file, Uri signature)
+ throws GeneralSecurityException, IOException, MissingPublicKeyException {
if (!mPublicKey.isPresent()) {
- throw new InvalidKeyException("Missing public key for signature verification");
+ throw new MissingPublicKeyException("Missing public key for signature verification");
}
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(mPublicKey.get());
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 3a359f4..34f8dd1 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -71,15 +72,14 @@
public class CertificateTransparencyDownloaderTest {
@Mock private DownloadManager mDownloadManager;
- @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
@Mock private CertificateTransparencyLogger mLogger;
private PrivateKey mPrivateKey;
private PublicKey mPublicKey;
private Context mContext;
- private File mTempFile;
private DataStore mDataStore;
private SignatureVerifier mSignatureVerifier;
+ private CompatibilityVersion mCompatVersion;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private long mNextDownloadId = 666;
@@ -93,8 +93,7 @@
mPublicKey = keyPair.getPublic();
mContext = InstrumentationRegistry.getInstrumentation().getContext();
- mTempFile = File.createTempFile("datastore-test", ".properties");
- mDataStore = new DataStore(mTempFile);
+ mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
mSignatureVerifier = new SignatureVerifier(mContext);
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
@@ -102,56 +101,64 @@
mDataStore,
new DownloadHelper(mDownloadManager),
mSignatureVerifier,
- mCertificateTransparencyInstaller,
mLogger);
+ mCompatVersion =
+ new CompatibilityVersion(
+ /* compatVersion= */ "v666",
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ mContext.getFilesDir());
- prepareDataStore();
prepareDownloadManager();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mCertificateTransparencyDownloader.start();
}
@After
public void tearDown() {
- mTempFile.delete();
mSignatureVerifier.resetPublicKey();
+ mCertificateTransparencyDownloader.stop();
+ mCompatVersion.delete();
}
@Test
public void testDownloader_startPublicKeyDownload() {
assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+
long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
+ assertThat(mCertificateTransparencyDownloader.getPublicKeyDownloadId())
+ .isEqualTo(downloadId);
}
@Test
public void testDownloader_startMetadataDownload() {
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_startContentDownload() {
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- long downloadId = mCertificateTransparencyDownloader.startContentDownload();
+
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(mPublicKey)));
assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
@@ -161,14 +168,14 @@
public void
testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setSuccessfulDownload(
- publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext,
+ makePublicKeyDownloadCompleteIntent(
+ writeToFile("i_am_not_a_base64_encoded_public_key".getBytes())));
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
@@ -176,17 +183,15 @@
@Test
public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setFailedDownload(
- publicKeyId, // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
@@ -194,283 +199,408 @@
@Test
public void testDownloader_publicKeyDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
// Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- setFailedDownload(
- publicKeyId, // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEventWithDownloadStatus(
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
}
@Test
public void testDownloader_publicKeyDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- // Set the failure count to just below the threshold
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+ // Set the failure count to well below the threshold
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- setFailedDownload(
- publicKeyId, // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
}
@Test
public void testDownloader_metadataDownloadSuccess_startContentDownload() {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, new File("log_list.sig"));
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
+ mContext,
+ makeMetadataDownloadCompleteIntent(mCompatVersion, new File("log_list.sig")));
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
}
@Test
public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setFailedDownload(
- metadataId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
}
@Test
public void testDownloader_metadataDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ throws Exception {
+ mCertificateTransparencyDownloader.startMetadataDownload();
// Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- setFailedDownload(
- metadataId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEventWithDownloadStatus(
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
}
@Test
public void testDownloader_metadataDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- // Set the failure count to just below the threshold
+ throws Exception {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to well below the threshold
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- setFailedDownload(
- metadataId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
}
@Test
- public void testDownloader_contentDownloadSuccess_installSuccess_updateDataStore()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_installSuccess() throws Exception {
String newVersion = "456";
File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(true);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertInstallSuccessful(newVersion);
}
@Test
public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setFailedDownload(
- contentId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
- verify(mCertificateTransparencyInstaller, never()).install(any(), any(), any());
assertNoVersionIsInstalled();
}
@Test
public void testDownloader_contentDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ throws Exception {
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
// Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- setFailedDownload(
- contentId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEventWithDownloadStatus(
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
}
@Test
public void testDownloader_contentDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- // Set the failure count to just below the threshold
+ throws Exception {
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+ // Set the failure count to well below the threshold
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- setFailedDownload(
- contentId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
}
@Test
- public void testDownloader_contentDownloadSuccess_installFail_doNotUpdateDataStore()
+ public void testDownloader_contentDownloadSuccess_invalidLogList_installFails()
throws Exception {
- File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(false);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
assertNoVersionIsInstalled();
}
@Test
public void
+ testDownloader_contentDownloadSuccess_noSignatureFound_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Set the public key to be missing
+ mSignatureVerifier.resetPublicKey();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, never())
+ .logCTLogListUpdateFailedEvent(
+ eq(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
+ anyInt());
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_wrongSignatureAlgo_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff algorithm
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, never())
+ .logCTLogListUpdateFailedEvent(
+ eq(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+ anyInt());
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_signatureNotVerified_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff key pair
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, never())
+ .logCTLogListUpdateFailedEvent(
+ eq(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+ anyInt());
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_wrongSignature_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ // Set the key to be deliberately wrong by using diff key pair
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never())
+ .logCTLogListUpdateFailedEvent(
+ eq(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+ anyInt());
+ verify(mLogger, never())
+ .logCTLogListUpdateFailedEvent(
+ eq(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
+ anyInt());
+ }
+
+ @Test
+ public void
testDownloader_contentDownloadSuccess_installFail_failureThresholdExceeded_logsFailure()
throws Exception {
- File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
// Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(false);
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
}
@Test
public void
testDownloader_contentDownloadSuccess_installFail_failureThresholdNotMet_doesNotLog()
throws Exception {
- File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- // Set the failure count to just below the threshold
+ // Set the failure count to well below the threshold
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(false);
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
}
@Test
@@ -479,17 +609,14 @@
File logListFile = makeLogListFile("456");
File metadataFile = File.createTempFile("log_list-wrong_metadata", "sig");
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
assertNoVersionIsInstalled();
}
@@ -499,17 +626,14 @@
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.resetPublicKey();
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
assertNoVersionIsInstalled();
}
@@ -523,52 +647,37 @@
assertNoVersionIsInstalled();
// 1. Start download of public key.
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
- // 2. On successful public key download, set the key and start the metatadata download.
- setSuccessfulDownload(publicKeyId, publicKeyFile);
-
+ // 2. On successful public key download, set the key and start the metatadata
+ // download.
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext, makePublicKeyDownloadCompleteIntent(publicKeyFile));
// 3. On successful metadata download, start the content download.
- long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
- setSuccessfulDownload(metadataId, metadataFile);
-
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
- // 4. On successful content download, verify the signature and install the new version.
- long contentId = mCertificateTransparencyDownloader.getContentDownloadId();
- setSuccessfulDownload(contentId, logListFile);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(true);
-
+ // 4. On successful content download, verify the signature and install the new
+ // version.
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertInstallSuccessful(newVersion);
}
private void assertNoVersionIsInstalled() {
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mCompatVersion.getVersionDir().exists()).isFalse();
}
private void assertInstallSuccessful(String version) {
- assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
- }
-
- private Intent makeDownloadCompleteIntent(long downloadId) {
- return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
- .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
- }
-
- private void prepareDataStore() {
- mDataStore.load();
- mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
- mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
- mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
+ File logsDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version);
+ assertThat(logsDir.exists()).isTrue();
+ File logsFile = new File(logsDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsFile.exists()).isTrue();
}
private void prepareDownloadManager() {
@@ -576,6 +685,32 @@
.thenAnswer(invocation -> mNextDownloadId++);
}
+ private Intent makePublicKeyDownloadCompleteIntent(File publicKeyfile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), publicKeyfile);
+ }
+
+ private Intent makeMetadataDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File signatureFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion),
+ signatureFile);
+ }
+
+ private Intent makeContentDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File logListFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion),
+ logListFile);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId, File file) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+
private Cursor makeSuccessfulDownloadCursor() {
MatrixCursor cursor =
new MatrixCursor(
@@ -586,9 +721,26 @@
return cursor;
}
- private void setSuccessfulDownload(long downloadId, File file) {
- when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
- when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ private Intent makePublicKeyDownloadFailedIntent(int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), error);
+ }
+
+ private Intent makeMetadataDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion), error);
+ }
+
+ private Intent makeContentDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion), error);
+ }
+
+ private Intent makeDownloadFailedIntent(long downloadId, int error) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeFailedDownloadCursor(error));
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
private Cursor makeFailedDownloadCursor(int error) {
@@ -601,16 +753,6 @@
return cursor;
}
- private void setFailedDownload(long downloadId, int... downloadManagerErrors) {
- Cursor first = makeFailedDownloadCursor(downloadManagerErrors[0]);
- Cursor[] others = new Cursor[downloadManagerErrors.length - 1];
- for (int i = 1; i < downloadManagerErrors.length; i++) {
- others[i - 1] = makeFailedDownloadCursor(downloadManagerErrors[i]);
- }
- when(mDownloadManager.query(any())).thenReturn(first, others);
- when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
- }
-
private File writePublicKeyToFile(PublicKey publicKey)
throws IOException, GeneralSecurityException {
return writeToFile(Base64.getEncoder().encode(publicKey.getEncoded()));
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
deleted file mode 100644
index 50d3f23..0000000
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.net.ct;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/** Tests for the {@link CertificateTransparencyInstaller}. */
-@RunWith(JUnit4.class)
-public class CertificateTransparencyInstallerTest {
-
- private static final String TEST_VERSION = "test-v1";
-
- private File mTestDir =
- new File(
- InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
- "test-dir");
- private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
- new CertificateTransparencyInstaller(mTestDir);
-
- @Before
- public void setUp() {
- mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
- }
-
- @After
- public void tearDown() {
- mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
- DirectoryUtils.removeDir(mTestDir);
- }
-
- @Test
- public void testCompatibilityVersion_installSuccessful() throws IOException {
- assertThat(mTestDir.mkdir()).isTrue();
- String content = "i_am_compatible";
- String version = "i_am_version";
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
- try (InputStream inputStream = asStream(content)) {
- assertThat(compatVersion.install(inputStream, version)).isTrue();
- }
- File logsDir = compatVersion.getLogsDir();
- assertThat(logsDir.exists()).isTrue();
- assertThat(logsDir.isDirectory()).isTrue();
- assertThat(logsDir.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
- File logsListFile = compatVersion.getLogsFile();
- assertThat(logsListFile.exists()).isTrue();
- assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
- assertThat(readAsString(logsListFile)).isEqualTo(content);
- File logsSymlink = compatVersion.getLogsDirSymlink();
- assertThat(logsSymlink.exists()).isTrue();
- assertThat(logsSymlink.isDirectory()).isTrue();
- assertThat(logsSymlink.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
- assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
-
- assertThat(compatVersion.delete()).isTrue();
- assertThat(logsDir.exists()).isFalse();
- assertThat(logsSymlink.exists()).isFalse();
- assertThat(logsListFile.exists()).isFalse();
- }
-
- @Test
- public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
- assertThat(mTestDir.mkdir()).isTrue();
-
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
- File rootDir = compatVersion.getRootDir();
- assertThat(rootDir.mkdir()).isTrue();
-
- String existingVersion = "666";
- File existingLogDir =
- new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
- assertThat(existingLogDir.mkdir()).isTrue();
-
- String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
- File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
- assertThat(logsListFile.createNewFile()).isTrue();
- writeToFile(logsListFile, existingContent);
-
- String newContent = "i_am_the_real_content";
- try (InputStream inputStream = asStream(newContent)) {
- assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
- }
-
- assertThat(readAsString(logsListFile)).isEqualTo(newContent);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
- String content = "i_am_a_certificate_and_i_am_transparent";
- String version = "666";
-
- try (InputStream inputStream = asStream(content)) {
- assertThat(
- mCertificateTransparencyInstaller.install(
- TEST_VERSION, inputStream, version))
- .isTrue();
- }
-
- assertThat(mTestDir.exists()).isTrue();
- assertThat(mTestDir.isDirectory()).isTrue();
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
- File logsDir = compatVersion.getLogsDir();
- assertThat(logsDir.exists()).isTrue();
- assertThat(logsDir.isDirectory()).isTrue();
- assertThat(logsDir.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
- File logsListFile = compatVersion.getLogsFile();
- assertThat(logsListFile.exists()).isTrue();
- assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
- assertThat(readAsString(logsListFile)).isEqualTo(content);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
- throws IOException {
- String existingVersion = "666";
- String existingContent = "i_was_already_installed_successfully";
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
- DirectoryUtils.makeDir(mTestDir);
- try (InputStream inputStream = asStream(existingContent)) {
- assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
- }
-
- try (InputStream inputStream = asStream("i_will_be_ignored")) {
- assertThat(
- mCertificateTransparencyInstaller.install(
- TEST_VERSION, inputStream, existingVersion))
- .isFalse();
- }
-
- assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
- }
-
- private static InputStream asStream(String string) throws IOException {
- return new ByteArrayInputStream(string.getBytes());
- }
-
- private static String readAsString(File file) throws IOException {
- return new String(new FileInputStream(file).readAllBytes());
- }
-
- private static void writeToFile(File file, String string) throws IOException {
- try (OutputStream out = new FileOutputStream(file)) {
- out.write(string.getBytes());
- }
- }
-}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
new file mode 100644
index 0000000..38fff48
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Tests for the {@link CompatibilityVersion}. */
+@RunWith(JUnit4.class)
+public class CompatibilityVersionTest {
+
+ private static final String TEST_VERSION = "v123";
+
+ private final File mTestDir =
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+ private final CompatibilityVersion mCompatVersion =
+ new CompatibilityVersion(
+ TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+ @After
+ public void tearDown() {
+ mCompatVersion.delete();
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
+ File versionDir = mCompatVersion.getVersionDir();
+ assertThat(versionDir.exists()).isFalse();
+ assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
+ assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
+ }
+
+ @Test
+ public void testCompatibilityVersion_symlink_setupSuccessful() {
+ File dirSymlink = mCompatVersion.getLogsDirSymlink();
+ assertThat(dirSymlink.exists()).isFalse();
+ assertThat(dirSymlink.getAbsolutePath())
+ .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_logsFile_setupSuccessful() {
+ File logsFile = mCompatVersion.getLogsFile();
+ assertThat(logsFile.exists()).isFalse();
+ assertThat(logsFile.getAbsolutePath())
+ .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ File logListFile = mCompatVersion.getLogsFile();
+ assertThat(logListFile.exists()).isTrue();
+ assertThat(logListFile.getCanonicalPath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/logs-i_am_version/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getCanonicalPath());
+ assertThat(logListFile.getAbsolutePath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/current/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.CURRENT_LOGS_DIR_SYMLINK_NAME),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
+ try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ mCompatVersion.delete();
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_invalidLogList() throws Exception {
+ try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
+ assertThat(mCompatVersion.install(inputStream)).isFalse();
+ }
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_incompleteVersionExists_replacesOldVersion()
+ throws Exception {
+ String existingVersion = "666";
+ File existingLogDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(existingLogDir.mkdirs()).isTrue();
+ File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.createNewFile()).isTrue();
+
+ JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
+ try (InputStream inputStream = asStream(newLogList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionAlreadyExists_installFails() throws Exception {
+ String existingVersion = "666";
+ JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
+ try (InputStream inputStream = asStream(existingLogList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
+ assertThat(mCompatVersion.install(inputStream)).isFalse();
+ }
+
+ assertThat(readAsString(mCompatVersion.getLogsFile()))
+ .isEqualTo(existingLogList.toString());
+ }
+
+ private static InputStream asStream(JSONObject logList) throws IOException {
+ return new ByteArrayInputStream(logList.toString().getBytes());
+ }
+
+ private static JSONObject makeLogList(String version) throws JSONException {
+ return new JSONObject().put("version", version);
+ }
+
+ private static JSONObject makeLogList(String version, String content) throws JSONException {
+ return makeLogList(version).put("content", content);
+ }
+
+ private static String readAsString(File file) throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ return new String(in.readAllBytes());
+ }
+ }
+}
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 4027038..3627157 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -71,4 +71,12 @@
-->
<string-array name="config_thread_mdns_vendor_specific_txts">
</string-array>
+
+ <!-- Whether to enable / start SRP server only when border routing is ready. SRP server and
+ border routing are mandatory features required by a Thread Border Router, and it takes 10 to
+ 20 seconds to establish border routing. Starting SRP server earlier is useful for use cases
+ where the user needs to know what are the devices in the network before actually needs to reach
+ to the devices, or reaching to Thread end devices doesn't require border routing to work.
+ -->
+ <bool name="config_thread_srp_server_wait_for_border_routing_enabled">true</bool>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index fbaae05..121cbc4 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -52,6 +52,7 @@
<item type="string" name="config_thread_vendor_oui" />
<item type="string" name="config_thread_model_name" />
<item type="array" name="config_thread_mdns_vendor_specific_txts" />
+ <item type="bool" name="config_thread_srp_server_wait_for_border_routing_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index fe26858..18801f0 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -110,6 +110,7 @@
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
import static android.net.NetworkCapabilities.RES_ID_UNSET;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -6765,7 +6766,7 @@
final NetworkOfferInfo offer =
findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
if (null != offer) {
- handleUnregisterNetworkOffer(offer);
+ handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
}
break;
}
@@ -7682,17 +7683,23 @@
}
}
- private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+ private void ensureAllNetworkRequestsHaveSupportedType(List<NetworkRequest> requests) {
+ final boolean isMultilayerRequest = requests.size() > 1;
for (int i = 0; i < requests.size(); i++) {
- ensureNetworkRequestHasType(requests.get(i));
+ ensureNetworkRequestHasSupportedType(requests.get(i), isMultilayerRequest);
}
}
- private void ensureNetworkRequestHasType(NetworkRequest request) {
+ private void ensureNetworkRequestHasSupportedType(NetworkRequest request,
+ boolean isMultilayerRequest) {
if (request.type == NetworkRequest.Type.NONE) {
throw new IllegalArgumentException(
"All NetworkRequests in ConnectivityService must have a type");
}
+ if (isMultilayerRequest && request.type == NetworkRequest.Type.RESERVATION) {
+ throw new IllegalArgumentException(
+ "Reservation requests are not supported in multilayer request");
+ }
}
/**
@@ -7844,7 +7851,7 @@
NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
@NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
@Nullable String callingAttributionTag, final int preferenceOrder) {
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mPendingIntent = pi;
@@ -7878,7 +7885,7 @@
@NetworkCallback.Flag int callbackFlags,
@Nullable String callingAttributionTag, int declaredMethodsFlags) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mMessenger = m;
@@ -7898,7 +7905,7 @@
NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
@NonNull final List<NetworkRequest> r) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
final NetworkAgentInfo satisfier = nri.getSatisfier();
@@ -8887,7 +8894,7 @@
@Override
public void releaseNetworkRequest(NetworkRequest networkRequest) {
- ensureNetworkRequestHasType(networkRequest);
+ ensureNetworkRequestHasSupportedType(networkRequest, false /* isMultilayerRequest */);
mHandler.sendMessage(mHandler.obtainMessage(
EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
}
@@ -8930,6 +8937,11 @@
Objects.requireNonNull(score);
Objects.requireNonNull(caps);
Objects.requireNonNull(callback);
+ if (caps.hasTransport(TRANSPORT_TEST)) {
+ enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
+ } else {
+ enforceNetworkFactoryPermission();
+ }
final boolean yieldToBadWiFi = caps.hasTransport(TRANSPORT_CELLULAR) && !avoidBadWifi();
final NetworkOffer offer = new NetworkOffer(
FullScore.makeProspectiveScore(score, caps, yieldToBadWiFi),
@@ -8968,7 +8980,7 @@
}
}
for (final NetworkOfferInfo noi : toRemove) {
- handleUnregisterNetworkOffer(noi);
+ handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
}
if (DBG) log("unregisterNetworkProvider for " + npi.name);
}
@@ -9401,7 +9413,7 @@
@Override
public void binderDied() {
- mHandler.post(() -> handleUnregisterNetworkOffer(this));
+ mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
}
}
@@ -9440,41 +9452,61 @@
return;
}
final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+
+ // If a reserved offer is updated, ensure the capabilities are not changed. This ensures
+ // that the reserved offer's capabilities match the ones passed by the onReserved callback,
+ // which is sent only once.
+ //
+ // TODO: consider letting the provider change the capabilities of an offer as long as they
+ // continue to satisfy the capabilities that were passed to onReserved. This is not needed
+ // today, but it shouldn't violate the API contract:
+ // - NetworkOffer capabilities are not promises
+ // - The app making a reservation must never assume that the capabilities of the reserved
+ // network are equal to the ones that were passed to onReserved. There will almost always be
+ // other capabilities, for example, those that change at runtime such as VALIDATED or
+ // NOT_SUSPENDED.
+ if (null != existingOffer
+ && existingOffer.offer.caps.getReservationId() != RES_ID_UNSET
+ && existingOffer.offer.caps.getReservationId() != RES_ID_MATCH_ALL_RESERVATIONS
+ && !newOffer.caps.equals(existingOffer.offer.caps)) {
+ // Reserved offers are not allowed to update their NetworkCapabilities.
+ // Doing so will immediately remove the offer from CS and send onUnavailable to the app.
+ handleUnregisterNetworkOffer(existingOffer, true /* releaseReservations */);
+ existingOffer.offer.notifyUnneeded();
+ logwtf("Reserved offers must never update their reserved NetworkCapabilities");
+ return;
+ }
+
+ final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
if (null != existingOffer) {
- // TODO: to support updating the score for reserved offers by calling
- // ConnectivityManager#offerNetwork with the same callback object or via
- // updateOfferScore, prevent handleUnregisterNetworkOffer() from sending an
- // onUnavailable() callback here.
- handleUnregisterNetworkOffer(existingOffer);
+ // Do not send onUnavailable for a reserved offer when updating it.
+ handleUnregisterNetworkOffer(existingOffer, false /* releaseReservations */);
newOffer.migrateFrom(existingOffer.offer);
if (DBG) {
// handleUnregisterNetworkOffer has already logged the old offer
log("update offer from providerId " + newOffer.providerId + " new : " + newOffer);
}
} else {
+ final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
+ if (reservationNri != null) {
+ // A NetworkRequest is only allowed to trigger a single reserved offer (and
+ // onReserved() callback). All subsequent offers are ignored. This either indicates
+ // a bug in the provider (e.g., responding twice to the same reservation, or
+ // updating the capabilities of a reserved offer), or multiple providers responding
+ // to the same offer (which could happen, but is not useful to the requesting app).
+ if (reservationNri.getReservedCapabilities() != null) {
+ loge("A reservation can only trigger a single offer; new offer is ignored.");
+ return;
+ }
+ // Always update the reserved offer before calling callCallbackForRequest.
+ reservationNri.setReservedCapabilities(noi.offer.caps);
+ callCallbackForRequest(
+ reservationNri, null /*networkAgent*/, CALLBACK_RESERVED, 0 /*arg1*/);
+ }
if (DBG) {
log("register offer from providerId " + newOffer.providerId + " : " + newOffer);
}
}
- final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
- final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
- if (reservationNri != null) {
- // A NetworkRequest is only allowed to trigger a single reserved offer (and onReserved()
- // callback). All subsequent offers are ignored. This either indicates a bug in the
- // provider (e.g., responding twice to the same reservation, or updating the
- // capabilities of a reserved offer), or multiple providers responding to the same offer
- // (which could happen, but is not useful to the requesting app).
- // TODO: add proper support for offer migration; i.e. allow the score of a reservation
- // offer to be updated.
- if (reservationNri.getReservedCapabilities() != null) {
- loge("A reservation can only trigger a single offer; new offer is ignored.");
- return;
- }
- // Always update the reserved offer before calling callCallbackForRequest.
- reservationNri.setReservedCapabilities(noi.offer.caps);
- callCallbackForRequest(
- reservationNri, null /* networkAgent */, CALLBACK_RESERVED, 0 /* arg1 */);
- }
try {
noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */);
@@ -9486,7 +9518,8 @@
issueNetworkNeeds(noi);
}
- private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+ private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi,
+ boolean releaseReservations) {
ensureRunningOnConnectivityServiceThread();
if (DBG) {
log("unregister offer from providerId " + noi.offer.providerId + " : " + noi.offer);
@@ -9504,11 +9537,10 @@
// handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it
// follows an onUnavailable).
final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi);
- if (nri != null) {
+ if (releaseReservations && nri != null) {
handleRemoveNetworkRequest(nri);
callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */);
}
-
noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
}
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index eea382e..d294046 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -42,6 +42,7 @@
* @hide
*/
public class NetworkOffer implements NetworkRanker.Scoreable {
+ private static final String TAG = NetworkOffer.class.getSimpleName();
@NonNull public final FullScore score;
@NonNull public final NetworkCapabilities caps;
@NonNull public final INetworkOfferCallback callback;
@@ -126,6 +127,23 @@
}
/**
+ * Sends onNetworkUnneeded for any remaining NetworkRequests.
+ *
+ * Used after a NetworkOffer migration failed to let the provider know that its networks should
+ * be torn down (as the offer is no longer registered).
+ */
+ public void notifyUnneeded() {
+ try {
+ for (NetworkRequest request : mCurrentlyNeeded) {
+ callback.onNetworkUnneeded(request);
+ }
+ } catch (RemoteException e) {
+ // The remote is dead; nothing to do.
+ }
+ mCurrentlyNeeded.clear();
+ }
+
+ /**
* Migrate from, and take over, a previous offer.
*
* When an updated offer is sent from a provider, call this method on the new offer, passing
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index b4a3b8a..0eab6e7 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -350,7 +350,7 @@
// TODO: remove "apex_available:platform".
apex_available: [
"//apex_available:platform",
- "com.android.btservices",
+ "com.android.bt",
"com.android.tethering",
"com.android.wifi",
],
diff --git a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
new file mode 100644
index 0000000..bc3b3a5
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -0,0 +1,324 @@
+/*
+ * 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 static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.util.PriorityQueue;
+
+/**
+ * Represents a realtime scheduler object used for scheduling tasks with precise delays.
+ * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
+ * callbacks by accounting for periods when the device is in deep sleep.
+ *
+ * <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same RealtimeScheduler **
+ *
+ * ```java
+ * // Create a RealtimeScheduler
+ * final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
+ *
+ * // Schedule a new task with a delay.
+ * scheduler.postDelayed(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the RealtimeScheduler after all tasks have finished running.
+ * scheduler.close();
+ * ```
+ */
+public class RealtimeScheduler {
+ private static final String TAG = RealtimeScheduler.class.getSimpleName();
+ // EVENT_ERROR may be generated even if not specified, as per its javadoc.
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+ private final CloseGuard mGuard = new CloseGuard();
+ @NonNull
+ private final Handler mHandler;
+ @NonNull
+ private final MessageQueue mQueue;
+ @NonNull
+ private final ParcelFileDescriptor mParcelFileDescriptor;
+ private final int mFdInt;
+
+ private final PriorityQueue<Task> mTaskQueue;
+
+ /**
+ * An abstract class for defining tasks that can be executed using a {@link Handler}.
+ */
+ private abstract static class Task implements Comparable<Task> {
+ private final long mRunTimeMs;
+ private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
+
+ /**
+ * create a task with a run time
+ */
+ Task(long runTimeMs) {
+ mRunTimeMs = runTimeMs;
+ }
+
+ /**
+ * Executes the task using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for executing the task.
+ */
+ abstract void post(Handler handler);
+
+ @Override
+ public int compareTo(@NonNull Task o) {
+ if (mRunTimeMs != o.mRunTimeMs) {
+ return Long.compare(mRunTimeMs, o.mRunTimeMs);
+ }
+ return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
+ }
+
+ /**
+ * Returns the run time of the task.
+ */
+ public long getRunTimeMs() {
+ return mRunTimeMs;
+ }
+ }
+
+ /**
+ * A task that sends a {@link Message} using a {@link Handler}.
+ */
+ private static class MessageTask extends Task {
+ private final Message mMessage;
+
+ MessageTask(Message message, long runTimeMs) {
+ super(runTimeMs);
+ mMessage = message;
+ }
+
+ /**
+ * Sends the {@link Message} using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for sending the message.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.sendMessage(mMessage);
+ }
+ }
+
+ /**
+ * A task that posts a {@link Runnable} to a {@link Handler}.
+ */
+ private static class RunnableTask extends Task {
+ private final Runnable mRunnable;
+
+ RunnableTask(Runnable runnable, long runTimeMs) {
+ super(runTimeMs);
+ mRunnable = runnable;
+ }
+
+ /**
+ * Posts the {@link Runnable} to the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for posting the runnable.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.post(mRunnable);
+ }
+ }
+
+ /**
+ * The RealtimeScheduler constructor
+ *
+ * Note: The constructor is currently safe to call on another thread because it only sets final
+ * members and registers the event to be called on the handler.
+ */
+ public RealtimeScheduler(@NonNull Handler handler) {
+ mFdInt = TimerFdUtils.createTimerFileDescriptor();
+ mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+ mHandler = handler;
+ mQueue = handler.getLooper().getQueue();
+ mTaskQueue = new PriorityQueue<>();
+ registerFdEventListener();
+
+ mGuard.open("close");
+ }
+
+ private boolean enqueueTask(@NonNull Task task, long delayMs) {
+ ensureRunningOnCorrectThread();
+ if (delayMs <= 0L) {
+ task.post(mHandler);
+ return true;
+ }
+ if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
+ if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+ return false;
+ }
+ }
+ mTaskQueue.add(task);
+ return true;
+ }
+
+ /**
+ * Set a runnable to be executed after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the runnable will be executed immediately.
+ *
+ * @param runnable the runnable to be executed
+ * @param delayMs the delay time in milliseconds
+ * @return true if the task is scheduled successfully, false otherwise.
+ */
+ public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
+ return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
+ delayMs);
+ }
+
+ /**
+ * Remove a scheduled runnable.
+ *
+ * @param runnable the runnable to be removed
+ */
+ public void removeDelayedRunnable(@NonNull Runnable runnable) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof RunnableTask
+ && ((RunnableTask) task).mRunnable == runnable);
+ }
+
+ /**
+ * Set a message to be sent after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the message will be sent immediately.
+ *
+ * @param msg the message to be sent
+ * @param delayMs the delay time in milliseconds
+ * @return true if the message is scheduled successfully, false otherwise.
+ */
+ public boolean sendDelayedMessage(Message msg, long delayMs) {
+
+ return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
+ }
+
+ /**
+ * Remove a scheduled message.
+ *
+ * @param what the message to be removed
+ */
+ public void removeDelayedMessage(int what) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof MessageTask
+ && ((MessageTask) task).mMessage.what == what);
+ }
+
+ /**
+ * Close the RealtimeScheduler. This implementation closes the underlying
+ * OS resources allocated to represent this stream.
+ */
+ public void close() {
+ ensureRunningOnCorrectThread();
+ unregisterAndDestroyFd();
+ }
+
+ private void registerFdEventListener() {
+ mQueue.addOnFileDescriptorEventListener(
+ mParcelFileDescriptor.getFileDescriptor(),
+ FD_EVENTS,
+ (fd, events) -> {
+ if (!isRunning()) {
+ return 0;
+ }
+ if ((events & EVENT_INPUT) != 0) {
+ handleExpiration();
+ }
+ return FD_EVENTS;
+ });
+ }
+
+ private boolean isRunning() {
+ return mParcelFileDescriptor.getFileDescriptor().valid();
+ }
+
+ private void handleExpiration() {
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ while (!mTaskQueue.isEmpty()) {
+ final Task task = mTaskQueue.peek();
+ currentTimeMs = SystemClock.elapsedRealtime();
+ if (currentTimeMs < task.getRunTimeMs()) {
+ break;
+ }
+ task.post(mHandler);
+ mTaskQueue.poll();
+ }
+
+
+ if (!mTaskQueue.isEmpty()) {
+ // Using currentTimeMs ensures that the calculated expiration time
+ // is always positive.
+ if (!TimerFdUtils.setExpirationTime(mFdInt,
+ mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
+ // If setting the expiration time fails, clear the task queue.
+ Log.wtf(TAG, "Failed to set expiration time");
+ mTaskQueue.clear();
+ }
+ } else {
+ // We have to clean up the timer if no tasks are left. Otherwise, the timer will keep
+ // being triggered.
+ TimerFdUtils.setExpirationTime(mFdInt, 0);
+ }
+ }
+
+ private void unregisterAndDestroyFd() {
+ if (mGuard != null) {
+ mGuard.close();
+ }
+
+ mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
+ try {
+ mParcelFileDescriptor.close();
+ } catch (IOException exception) {
+ Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
+ }
+ }
+
+ private void ensureRunningOnCorrectThread() {
+ if (mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ @SuppressWarnings("Finalize")
+ @Override
+ protected void finalize() throws Throwable {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+ super.finalize();
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
deleted file mode 100644
index dbbccc5..0000000
--- a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * 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 static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.MessageQueue;
-import android.os.ParcelFileDescriptor;
-import android.util.CloseGuard;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-
-/**
- * Represents a Timer file descriptor object used for scheduling tasks with precise delays.
- * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
- * callbacks by accounting for periods when the device is in deep sleep.
- *
- * <p> This class is designed for use exclusively from the handler thread.
- *
- * **Usage Examples:**
- *
- * ** Scheduling recurring tasks with the same TimerFileDescriptor **
- *
- * ```java
- * // Create a TimerFileDescriptor
- * final TimerFileDescriptor timerFd = new TimerFileDescriptor(handler);
- *
- * // Schedule a new task with a delay.
- * timerFd.setDelayedTask(() -> taskToExecute(), delayTime);
- *
- * // Once the delay has elapsed, and the task is running, schedule another task.
- * timerFd.setDelayedTask(() -> anotherTaskToExecute(), anotherDelayTime);
- *
- * // Remember to close the TimerFileDescriptor after all tasks have finished running.
- * timerFd.close();
- * ```
- */
-public class TimerFileDescriptor {
- private static final String TAG = TimerFileDescriptor.class.getSimpleName();
- // EVENT_ERROR may be generated even if not specified, as per its javadoc.
- private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
- private final CloseGuard mGuard = new CloseGuard();
- @NonNull
- private final Handler mHandler;
- @NonNull
- private final MessageQueue mQueue;
- @NonNull
- private final ParcelFileDescriptor mParcelFileDescriptor;
- private final int mFdInt;
- @Nullable
- private ITask mTask;
-
- /**
- * An interface for defining tasks that can be executed using a {@link Handler}.
- */
- public interface ITask {
- /**
- * Executes the task using the provided {@link Handler}.
- *
- * @param handler The {@link Handler} to use for executing the task.
- */
- void post(Handler handler);
- }
-
- /**
- * A task that sends a {@link Message} using a {@link Handler}.
- */
- public static class MessageTask implements ITask {
- private final Message mMessage;
-
- public MessageTask(Message message) {
- mMessage = message;
- }
-
- /**
- * Sends the {@link Message} using the provided {@link Handler}.
- *
- * @param handler The {@link Handler} to use for sending the message.
- */
- @Override
- public void post(Handler handler) {
- handler.sendMessage(mMessage);
- }
- }
-
- /**
- * A task that posts a {@link Runnable} to a {@link Handler}.
- */
- public static class RunnableTask implements ITask {
- private final Runnable mRunnable;
-
- public RunnableTask(Runnable runnable) {
- mRunnable = runnable;
- }
-
- /**
- * Posts the {@link Runnable} to the provided {@link Handler}.
- *
- * @param handler The {@link Handler} to use for posting the runnable.
- */
- @Override
- public void post(Handler handler) {
- handler.post(mRunnable);
- }
- }
-
- /**
- * TimerFileDescriptor constructor
- *
- * Note: The constructor is currently safe to call on another thread because it only sets final
- * members and registers the event to be called on the handler.
- */
- public TimerFileDescriptor(@NonNull Handler handler) {
- mFdInt = TimerFdUtils.createTimerFileDescriptor();
- mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
- mHandler = handler;
- mQueue = handler.getLooper().getQueue();
- registerFdEventListener();
-
- mGuard.open("close");
- }
-
- /**
- * Set a task to be executed after a specified delay.
- *
- * <p> A task can only be scheduled once at a time. Cancel previous scheduled task before the
- * new task is scheduled.
- *
- * @param task the task to be executed
- * @param delayMs the delay time in milliseconds
- * @throws IllegalArgumentException if try to replace the current scheduled task
- * @throws IllegalArgumentException if the delay time is less than 0
- */
- public void setDelayedTask(@NonNull ITask task, long delayMs) {
- ensureRunningOnCorrectThread();
- if (mTask != null) {
- throw new IllegalArgumentException("task is already scheduled");
- }
- if (delayMs <= 0L) {
- task.post(mHandler);
- return;
- }
-
- if (TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
- mTask = task;
- }
- }
-
- /**
- * Cancel the scheduled task.
- */
- public void cancelTask() {
- ensureRunningOnCorrectThread();
- if (mTask == null) return;
-
- TimerFdUtils.setExpirationTime(mFdInt, 0 /* delayMs */);
- mTask = null;
- }
-
- /**
- * Check if there is a scheduled task.
- */
- public boolean hasDelayedTask() {
- ensureRunningOnCorrectThread();
- return mTask != null;
- }
-
- /**
- * Close the TimerFileDescriptor. This implementation closes the underlying
- * OS resources allocated to represent this stream.
- */
- public void close() {
- ensureRunningOnCorrectThread();
- unregisterAndDestroyFd();
- }
-
- private void registerFdEventListener() {
- mQueue.addOnFileDescriptorEventListener(
- mParcelFileDescriptor.getFileDescriptor(),
- FD_EVENTS,
- (fd, events) -> {
- if (!isRunning()) {
- return 0;
- }
- if ((events & EVENT_INPUT) != 0) {
- handleExpiration();
- }
- return FD_EVENTS;
- });
- }
-
- private boolean isRunning() {
- return mParcelFileDescriptor.getFileDescriptor().valid();
- }
-
- private void handleExpiration() {
- // Execute the task
- if (mTask != null) {
- mTask.post(mHandler);
- mTask = null;
- }
- }
-
- private void unregisterAndDestroyFd() {
- if (mGuard != null) {
- mGuard.close();
- }
-
- mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
- try {
- mParcelFileDescriptor.close();
- } catch (IOException exception) {
- Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
- }
- }
-
- private void ensureRunningOnCorrectThread() {
- if (mHandler.getLooper() != Looper.myLooper()) {
- throw new IllegalStateException(
- "Not running on Handler thread: " + Thread.currentThread().getName());
- }
- }
-
- @SuppressWarnings("Finalize")
- @Override
- protected void finalize() throws Throwable {
- if (mGuard != null) {
- mGuard.warnIfOpen();
- }
- super.finalize();
- }
-}
diff --git a/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
new file mode 100644
index 0000000..b4f7642
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.util.Log;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class for logging terrible errors and reporting them for tracking.
+ *
+ * @hide
+ */
+public class TerribleErrorLog {
+
+ private static final String TAG = TerribleErrorLog.class.getSimpleName();
+
+ /**
+ * Logs a terrible error and reports metrics through a provided statsLog.
+ */
+ public static void logTerribleError(@NonNull BiConsumer<Integer, Integer> statsLog,
+ @NonNull String message, int protoType, int errorType) {
+ statsLog.accept(protoType, errorType);
+ Log.wtf(TAG, message);
+ }
+}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 9d1d291..f4f1ea9 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -28,6 +28,7 @@
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
"net-utils-service-connectivity",
+ "truth",
],
libs: [
"android.test.runner.stubs",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
new file mode 100644
index 0000000..30b530f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.os.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.os.SystemClock
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class RealtimeSchedulerTest {
+
+ private val TIMEOUT_MS = 1000L
+ private val TOLERANCE_MS = 50L
+ private class TestHandler(looper: Looper) : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ val pair = msg.obj as Pair<ConditionVariable, MutableList<Long>>
+ val cv = pair.first
+ cv.open()
+ val executionTimes = pair.second
+ executionTimes.add(SystemClock.elapsedRealtime())
+ }
+ }
+ private val thread = HandlerThread(RealtimeSchedulerTest::class.simpleName).apply { start() }
+ private val handler by lazy { TestHandler(thread.looper) }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testMultiplePostDelayedTasks() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 0)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 200)
+ val toBeRemoved = Runnable {
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ }
+ scheduler.postDelayed(toBeRemoved, 250)
+ scheduler.removeDelayedRunnable(toBeRemoved)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 100)
+ scheduler.postDelayed({
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ cv.open() }, 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0]).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1]).isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2]).isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3]).isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+
+ @Test
+ fun testMultipleSendDelayedMessages() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val MSG_ID_0 = 0
+ val MSG_ID_1 = 1
+ val MSG_ID_2 = 2
+ val MSG_ID_3 = 3
+ val MSG_ID_4 = 4
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_0, Pair(ConditionVariable(), executionTimes)), 0)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_1, Pair(ConditionVariable(), executionTimes)),
+ 200)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_4, Pair(ConditionVariable(), executionTimes)),
+ 250)
+ scheduler.removeDelayedMessage(MSG_ID_4)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_2, Pair(ConditionVariable(), executionTimes)),
+ 100)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_3, Pair(cv, executionTimes)),
+ 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0] - initialTimeMs).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1] - initialTimeMs)
+ .isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2] - initialTimeMs)
+ .isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3] - initialTimeMs)
+ .isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
new file mode 100644
index 0000000..5fd634e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.tryTest
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TerribleErrorLogTest {
+ @Test
+ fun testLogTerribleError() {
+ val wtfCaptures = mutableListOf<String>()
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ wtfCaptures.add("$tag,${what.message}")
+ }
+ val statsLogCapture = mutableListOf<Pair<Int, Int>>()
+ val testStatsLog = object {
+ fun write(protoType: Int, errorType: Int) {
+ statsLogCapture.add(protoType to errorType)
+ }
+ }
+ tryTest {
+ TerribleErrorLog.logTerribleError(testStatsLog::write, "error", 1, 2)
+ assertContentEquals(listOf(1 to 2), statsLogCapture)
+ assertContentEquals(listOf("TerribleErrorLog,error"), wtfCaptures)
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
deleted file mode 100644
index f5e47c9..0000000
--- a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.os.Build
-import android.os.ConditionVariable
-import android.os.Handler
-import android.os.HandlerThread
-import android.os.Looper
-import android.os.Message
-import androidx.test.filters.SmallTest
-import com.android.net.module.util.TimerFileDescriptor.ITask
-import com.android.net.module.util.TimerFileDescriptor.MessageTask
-import com.android.net.module.util.TimerFileDescriptor.RunnableTask
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.tryTest
-import com.android.testutils.visibleOnHandlerThread
-import org.junit.After
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.time.Duration
-import java.time.Instant
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-
-private const val MSG_TEST = 1
-
-@DevSdkIgnoreRunner.MonitorThreadLeak
-@RunWith(DevSdkIgnoreRunner::class)
-@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class TimerFileDescriptorTest {
- private class TestHandler(looper: Looper) : Handler(looper) {
- override fun handleMessage(msg: Message) {
- val cv = msg.obj as ConditionVariable
- cv.open()
- }
- }
- private val thread = HandlerThread(TimerFileDescriptorTest::class.simpleName).apply { start() }
- private val handler by lazy { TestHandler(thread.looper) }
-
- @After
- fun tearDown() {
- thread.quitSafely()
- thread.join()
- }
-
- private fun assertDelayedTaskPost(
- timerFd: TimerFileDescriptor,
- task: ITask,
- cv: ConditionVariable
- ) {
- val delayTime = 10L
- val startTime1 = Instant.now()
- handler.post { timerFd.setDelayedTask(task, delayTime) }
- assertTrue(cv.block(100L /* timeoutMs*/))
- assertTrue(Duration.between(startTime1, Instant.now()).toMillis() >= delayTime)
- }
-
- @Test
- fun testSetDelayedTask() {
- val timerFd = TimerFileDescriptor(handler)
- tryTest {
- // Verify the delayed task is executed with the self-implemented ITask
- val cv1 = ConditionVariable()
- assertDelayedTaskPost(timerFd, { cv1.open() }, cv1)
-
- // Verify the delayed task is executed with the RunnableTask
- val cv2 = ConditionVariable()
- assertDelayedTaskPost(timerFd, RunnableTask{ cv2.open() }, cv2)
-
- // Verify the delayed task is executed with the MessageTask
- val cv3 = ConditionVariable()
- assertDelayedTaskPost(timerFd, MessageTask(handler.obtainMessage(MSG_TEST, cv3)), cv3)
- } cleanup {
- visibleOnHandlerThread(handler) { timerFd.close() }
- }
- }
-
- @Test
- fun testCancelTask() {
- // The task is posted and canceled within the same handler loop, so the short delay used
- // here won't cause flakes.
- val delayTime = 10L
- val timerFd = TimerFileDescriptor(handler)
- val cv = ConditionVariable()
- tryTest {
- handler.post {
- timerFd.setDelayedTask({ cv.open() }, delayTime)
- assertTrue(timerFd.hasDelayedTask())
- timerFd.cancelTask()
- assertFalse(timerFd.hasDelayedTask())
- }
- assertFalse(cv.block(20L /* timeoutMs*/))
- } cleanup {
- visibleOnHandlerThread(handler) { timerFd.close() }
- }
- }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 8dc1bc4..bfbbc34 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -14,19 +14,34 @@
* limitations under the License.
*/
-package com.android.testutils;
+package com.android.testutils
import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
import android.net.KeepalivePacketData
+import android.net.LinkAddress
import android.net.LinkProperties
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkRequest
import android.net.QosFilter
import android.net.Uri
import android.os.Looper
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.EADDRNOTAVAIL
+import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ENONET
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil.makeTestNetworkSpecifier
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
@@ -42,6 +57,8 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.net.NetworkInterface
+import java.net.SocketException
import java.time.Duration
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -65,6 +82,92 @@
conf: NetworkAgentConfig
) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ companion object {
+
+ /**
+ * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
+ * [createOnInterface].
+ */
+ fun makeNetworkRequestForInterface(ifaceName: String) = NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ .build()
+
+ /**
+ * Convenience method to initialize a [TestableNetworkAgent] on a given interface.
+ *
+ * This waits for link-local addresses to be setup and ensures LinkProperties are updated
+ * with the addresses.
+ */
+ fun createOnInterface(
+ context: Context,
+ looper: Looper,
+ ifaceName: String,
+ timeoutMs: Long
+ ): TestableNetworkAgent {
+ val lp = LinkProperties().apply {
+ interfaceName = ifaceName
+ }
+ val agent = TestableNetworkAgent(
+ context,
+ looper,
+ NetworkCapabilities().apply {
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ addTransportType(TRANSPORT_TEST)
+ setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
+ val network = agent.register()
+ agent.markConnected()
+ if (isAtLeastS()) {
+ // OnNetworkCreated was added in S
+ agent.eventuallyExpect<OnNetworkCreated>()
+ }
+
+ // Wait until the link-local address can be used. Address flags are not available
+ // without elevated permissions, so check that bindSocket works.
+ assertEventuallyTrue("No usable v6 address after $timeoutMs ms", timeoutMs) {
+ // To avoid race condition between socket connection succeeding and interface
+ // returning a non-empty address list. Verify that interface returns a non-empty
+ // list, before trying the socket connection.
+ if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
+ return@assertEventuallyTrue false
+ }
+
+ val sock = Os.socket(OsConstants.AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+ tryTest {
+ network.bindSocket(sock)
+ Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
+ true
+ }.catch<ErrnoException> {
+ if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
+ throw it
+ }
+ false
+ }.catch<SocketException> {
+ // OnNetworkCreated does not exist on R, so a SocketException caused by ENONET
+ // may be seen before the network is created
+ if (isAtLeastS()) throw it
+ val cause = it.cause as? ErrnoException ?: throw it
+ if (cause.errno != ENONET) {
+ throw it
+ }
+ false
+ } cleanup {
+ Os.close(sock)
+ }
+ }
+
+ agent.lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
+ LinkAddress(it.address, it.networkPrefixLength.toInt())
+ })
+ agent.sendLinkProperties(agent.lp)
+ return agent
+ }
+ }
val DEFAULT_TIMEOUT_MS = 5000L
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ad98a29..ac60b0f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.net.TetheringInterface;
import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiSsid;
@@ -37,6 +38,7 @@
public class TetheringTest {
private CtsTetheringUtils mCtsTetheringUtils;
private TetheringHelperClient mTetheringHelperClient;
+ private TestTetheringEventCallback mTetheringEventCallback;
@Before
public void setUp() throws Exception {
@@ -44,11 +46,14 @@
mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
mTetheringHelperClient = new TetheringHelperClient(targetContext);
mTetheringHelperClient.bind();
+ mTetheringEventCallback = mCtsTetheringUtils.registerTetheringEventCallback();
}
@After
public void tearDown() throws Exception {
mTetheringHelperClient.unbind();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(mTetheringEventCallback);
+ mCtsTetheringUtils.stopAllTethering();
}
/**
@@ -57,24 +62,20 @@
*/
@Test
public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
- final CtsTetheringUtils.TestTetheringEventCallback tetherEventCallback =
- mCtsTetheringUtils.registerTetheringEventCallback();
+ mTetheringEventCallback.assumeWifiTetheringSupported(
+ getInstrumentation().getTargetContext());
SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
.setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
.getBytes(StandardCharsets.UTF_8))).build();
final TetheringInterface tetheringInterface =
- mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+ mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
assertNotNull(tetheringInterface);
assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
- try {
- TetheringInterface tetheringInterfaceForApp2 =
- mTetheringHelperClient.getTetheredWifiInterface();
- assertNotNull(tetheringInterfaceForApp2);
- assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
- assertEquals(
- tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
- } finally {
- mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
- }
+ TetheringInterface tetheringInterfaceForApp2 =
+ mTetheringHelperClient.getTetheredWifiInterface();
+ assertNotNull(tetheringInterfaceForApp2);
+ assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+ assertEquals(
+ tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
}
}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 320622b..dd0665e 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -21,7 +21,6 @@
import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.Manifest.permission.WRITE_DEVICE_CONFIG
-import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
@@ -170,8 +169,8 @@
private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
val packageManager = context.getPackageManager()
val userManager = context.getSystemService(UserManager::class.java)!!
- return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
- && userManager.isVisibleBackgroundUsersSupported)
+ return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE) &&
+ userManager.isVisibleBackgroundUsersSupported)
}
@BeforeClass
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
new file mode 100644
index 0000000..b1b6e0d
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
@@ -0,0 +1,183 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.net.DnsResolver
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.RouteInfo
+import android.os.CancellationSignal
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_NETD_NATIVE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.testutils.AutoReleaseNetworkCallbackRule
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.DnsResolverModuleTest
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReaderRule
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestDnsPacket
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.runAsShell
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_DNSSERVER_MAC = MacAddress.fromString("00:11:22:33:44:55")
+private val TAG = DnsResolverTapTest::class.java.simpleName
+private const val TEST_TIMEOUT_MS = 10_000L
+
+@DnsResolverModuleTest
+@RunWith(AndroidJUnit4::class)
+class DnsResolverTapTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val handlerThread = HandlerThread(TAG)
+
+ @get:Rule(order = 1)
+ val deviceConfigRule = DeviceConfigRule()
+
+ @get:Rule(order = 2)
+ val featureFlagsRule = SetFeatureFlagsRule(
+ setFlagsMethod = { name, enabled ->
+ val value = when (enabled) {
+ null -> null
+ true -> "1"
+ false -> "0"
+ }
+ deviceConfigRule.setConfig(NAMESPACE_NETD_NATIVE, name, value)
+ },
+ getFlagsMethod = {
+ runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getInt(NAMESPACE_NETD_NATIVE, it, 0) == 1
+ }
+ }
+ )
+
+ @get:Rule(order = 3)
+ val packetReaderRule = TapPacketReaderRule()
+
+ @get:Rule(order = 4)
+ val cbRule = AutoReleaseNetworkCallbackRule()
+
+ private val ndResponder by lazy { RouterAdvertisementResponder(packetReaderRule.reader) }
+ private val dnsServerAddr by lazy {
+ parseNumericAddress("fe80::124%${packetReaderRule.iface.interfaceName}") as Inet6Address
+ }
+ private lateinit var agent: TestableNetworkAgent
+
+ @Before
+ fun setUp() {
+ handlerThread.start()
+ val interfaceName = packetReaderRule.iface.interfaceName
+ val cb = cbRule.requestNetwork(TestableNetworkAgent.makeNetworkRequestForInterface(
+ interfaceName))
+ agent = runAsShell(MANAGE_TEST_NETWORKS) {
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ interfaceName, TEST_TIMEOUT_MS)
+ }
+ ndResponder.addNeighborEntry(TEST_DNSSERVER_MAC, dnsServerAddr)
+ ndResponder.start()
+ agent.lp.apply {
+ addDnsServer(dnsServerAddr)
+ // A default route is needed for DnsResolver.java to send queries over IPv6
+ // (see usage of DnsUtils.haveIpv6).
+ addRoute(RouteInfo(IpPrefix("::/0"), null, null))
+ }
+ agent.sendLinkProperties(agent.lp)
+ cb.eventuallyExpect<LinkPropertiesChanged> { it.lp.dnsServers.isNotEmpty() }
+ }
+
+ @After
+ fun tearDown() {
+ ndResponder.stop()
+ if (::agent.isInitialized) {
+ agent.unregister()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private class DnsCallback : DnsResolver.Callback<List<InetAddress>> {
+ override fun onAnswer(answer: List<InetAddress>, rcode: Int) = Unit
+ override fun onError(error: DnsResolver.DnsException) = Unit
+ }
+
+ /**
+ * Run a cancellation test.
+ *
+ * @param domain Domain name to query
+ * @param waitTimeForNoRetryAfterCancellationMs If positive, cancel the query and wait for that
+ * delay to check no retry is sent.
+ * @return The duration it took to receive all expected replies.
+ */
+ fun doCancellationTest(domain: String, waitTimeForNoRetryAfterCancellationMs: Long): Long {
+ val cancellationSignal = CancellationSignal()
+ val dnsCb = DnsCallback()
+ val queryStart = SystemClock.elapsedRealtime()
+ DnsResolver.getInstance().query(
+ agent.network, domain, 0 /* flags */,
+ Runnable::run /* executor */, cancellationSignal, dnsCb
+ )
+
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ cancellationSignal.cancel()
+ }
+ // Filter for queries on UDP port 53 for the specified domain
+ val filter = IPv6UdpFilter(dstPort = 53).and {
+ TestDnsPacket(
+ it.copyOfRange(ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size),
+ dstAddr = dnsServerAddr
+ ).isQueryFor(domain, DnsResolver.TYPE_AAAA)
+ }
+
+ val reader = packetReaderRule.reader
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Original query not found")
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ assertNull(reader.poll(waitTimeForNoRetryAfterCancellationMs, filter),
+ "Expected no retry query")
+ } else {
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Retry query not found")
+ }
+ return SystemClock.elapsedRealtime() - queryStart
+ }
+
+ @SetFeatureFlagsRule.FeatureFlag("no_retry_after_cancel", true)
+ @Test
+ fun testCancellation() {
+ val timeWithRetryWhenNotCancelled = doCancellationTest("test1.example.com",
+ waitTimeForNoRetryAfterCancellationMs = 0L)
+ doCancellationTest("test2.example.com",
+ waitTimeForNoRetryAfterCancellationMs = timeWithRetryWhenNotCancelled + 50L)
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
new file mode 100644
index 0000000..b593baf
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 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.net.cts
+
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.MacAddress
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkSpecifierTest {
+ @Test
+ fun testParcelUnparcel() {
+ val remoteMac = MacAddress.fromString("01:02:03:04:05:06")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertParcelingIsLossless(specifier)
+ }
+
+ @Test
+ fun testGetters() {
+ val remoteMac = MacAddress.fromString("11:22:33:44:55:66")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(123)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertEquals(ROLE_SERVER, specifier.getRole())
+ assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression())
+ assertEquals(123, specifier.getPsm())
+ assertEquals(remoteMac, specifier.getRemoteAddress())
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 1ca5a77..2fb140a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -33,6 +33,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
@@ -103,6 +104,16 @@
}
}
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testParceling() {
+ NetworkCapabilities nc = new NetworkCapabilities.Builder().build();
+ NetworkRequest request = new NetworkRequest(nc, TYPE_NONE, 42 /* rId */,
+ NetworkRequest.Type.RESERVATION);
+
+ assertParcelingIsLossless(request);
+ }
+
@Test
public void testCapabilities() {
assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
new file mode 100644
index 0000000..0b10ef6
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2025 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.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.runAsShell
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "NetworkReservationTest"
+
+private val NETWORK_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+// TODO: integrate with CSNetworkReservationTest and move to common tests.
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkReservationTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+
+ @Before
+ fun setUp() {
+ runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkProvider(provider)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ runAsShell(NETWORK_SETTINGS) {
+ // unregisterNetworkProvider unregisters all associated NetworkOffers.
+ cm.unregisterNetworkProvider(provider)
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ @Test
+ fun testReserveNetwork() {
+ // register blanket offer
+ val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
+ }
+
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, handler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved reservation offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, reservedCaps, handler::post, reservedOffer)
+ }
+
+ // validate onReserved was sent to the app
+ val appObservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedCaps, appObservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOffer.expectOnNetworkNeeded(reservedCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOffer)
+ cb.expect<Unavailable>()
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index c981a1b..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,14 +22,10 @@
import android.net.ConnectivityManager.NetworkCallback
import android.net.DnsResolver
import android.net.InetAddresses.parseNumericAddress
-import android.net.LinkAddress
-import android.net.LinkProperties
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.MacAddress
import android.net.Network
-import android.net.NetworkAgentConfig
-import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -53,16 +49,10 @@
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_TETHERING
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants.AF_INET6
-import android.system.OsConstants.EADDRNOTAVAIL
-import android.system.OsConstants.ENETUNREACH
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.RT_SCOPE_LINK
-import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -106,7 +96,6 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestDnsPacket
import com.android.testutils.TestableNetworkAgent
-import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
@@ -247,16 +236,12 @@
val tnm = context.getSystemService(TestNetworkManager::class.java)!!
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
- val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
cm.requestNetwork(
- NetworkRequest.Builder()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(testNetworkSpecifier)
- .build(),
+ TestableNetworkAgent.makeNetworkRequestForInterface(iface.interfaceName),
cb
)
- val agent = registerTestNetworkAgent(iface.interfaceName)
+ val agent = TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ iface.interfaceName, TIMEOUT_MS)
val network = agent.network ?: fail("Registered agent should have a network")
cb.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
@@ -271,57 +256,6 @@
return TestTapNetwork(iface, cb, agent, network)
}
- private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
- val lp = LinkProperties().apply {
- interfaceName = ifaceName
- }
- val agent = TestableNetworkAgent(
- context,
- handlerThread.looper,
- NetworkCapabilities().apply {
- removeCapability(NET_CAPABILITY_TRUSTED)
- addTransportType(TRANSPORT_TEST)
- setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- },
- lp,
- NetworkAgentConfig.Builder().build()
- )
- val network = agent.register()
- agent.markConnected()
- agent.expectCallback<OnNetworkCreated>()
-
- // Wait until the link-local address can be used. Address flags are not available without
- // elevated permissions, so check that bindSocket works.
- PollingCheck.check("No usable v6 address on interface after $TIMEOUT_MS ms", TIMEOUT_MS) {
- // To avoid race condition between socket connection succeeding and interface returning
- // a non-empty address list. Verify that interface returns a non-empty list, before
- // trying the socket connection.
- if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
- return@check false
- }
-
- val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
- tryTest {
- network.bindSocket(sock)
- Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
- true
- }.catch<ErrnoException> {
- if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
- throw it
- }
- false
- } cleanup {
- Os.close(sock)
- }
- }
-
- lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
- LinkAddress(it.address, it.networkPrefixLength.toInt())
- })
- agent.sendLinkProperties(lp)
- return agent
- }
-
private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
it.serviceType = serviceType
it.serviceName = serviceName
@@ -576,7 +510,9 @@
assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
- registerTestNetworkAgent(testNetwork1.iface.interfaceName)
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ testNetwork1.iface.interfaceName,
+ TIMEOUT_MS)
}
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
index 7b6c995..e698930 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -17,12 +17,16 @@
package com.android.server
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P
import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
import android.net.NetworkRequest
import android.net.NetworkScore
import android.os.Build
@@ -35,16 +39,26 @@
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
import kotlin.test.assertEquals
import kotlin.test.assertNull
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-
private val ETHERNET_SCORE = NetworkScore.Builder().build()
private val ETHERNET_CAPS = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
.addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
.build()
private const val TIMEOUT_MS = 5_000L
@@ -53,42 +67,53 @@
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.R)
class CSNetworkReservationTest : CSTest() {
+ private lateinit var provider: NetworkProvider
+ private val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+ @Before
+ fun subclassSetUp() {
+ provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+ cm.registerNetworkProvider(provider)
+
+ // register a blanket offer for use in tests.
+ provider.registerNetworkOffer(ETHERNET_SCORE, BLANKET_CAPS, blanketOffer)
+ }
+
fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
it.reservationId = resId
}
+ fun NetworkProvider.registerNetworkOffer(
+ score: NetworkScore,
+ caps: NetworkCapabilities,
+ cb: NetworkOfferCallback
+ ) {
+ registerNetworkOffer(score, caps, {r -> r.run()}, cb)
+ }
+
@Test
fun testReservationRequest() {
- val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
- val blanketOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
-
- cm.registerNetworkProvider(provider)
-
- val blanketCaps = ETHERNET_CAPS.copyWithReservationId(RES_ID_MATCH_ALL_RESERVATIONS)
- provider.registerNetworkOffer(ETHERNET_SCORE, blanketCaps, {r -> r.run()}, blanketOfferCb)
-
- val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
val cb = TestableNetworkCallback()
- cm.reserveNetwork(req, csHandler, cb)
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
// validate the reservation matches the blanket offer.
- val reservationReq = blanketOfferCb.expectOnNetworkNeeded(blanketCaps).request
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
val reservationId = reservationReq.networkCapabilities.reservationId
- // bring up specific reservation offer
- val specificCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
- val specificOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
- provider.registerNetworkOffer(ETHERNET_SCORE, specificCaps, {r -> r.run()}, specificOfferCb)
+ // bring up reserved reservation offer
+ val reservedOfferCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedOfferCaps, reservedOfferCb)
// validate onReserved was sent to the app
- val reservedCaps = cb.expect<Reserved>().caps
- assertEquals(specificCaps, reservedCaps)
+ val onReservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedOfferCaps, onReservedCaps)
- // validate the reservation matches the specific offer.
- specificOfferCb.expectOnNetworkNeeded(specificCaps)
+ // validate the reservation matches the reserved offer.
+ reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
- // Specific offer goes away
- provider.unregisterNetworkOffer(specificOfferCb)
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOfferCb)
cb.expect<Unavailable>()
}
@@ -101,19 +126,168 @@
@Test
fun testReservationRequest_notDeliveredToRegularOffer() {
- val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
-
- cm.registerNetworkProvider(provider)
provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, {r -> r.run()}, offerCb)
- val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
val cb = TestableNetworkCallback()
- cm.reserveNetwork(req, csHandler, cb)
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
// validate the offer does not receive onNetworkNeeded for reservation request
offerCb.expectNoCallbackWhere {
it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
}
}
+
+ @Test
+ fun testReservedOffer_preventReservationIdUpdate() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // try to update the offer's reservationId by reusing the same callback object.
+ // first file a new request to try and match the offer later.
+ val cb2 = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb2)
+
+ val reservationReq2 = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId2 = reservationReq2.networkCapabilities.reservationId
+
+ // try to update the offer's reservationId to an existing reservationId.
+ val updatedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId2)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ // validate the original offer disappeared.
+ cb.expect<Unavailable>()
+ // validate the new offer was rejected by CS.
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ // validate cb2 never sees onReserved().
+ cb2.assertNoCallback()
+ }
+
+ @Test
+ fun testReservedOffer_capabilitiesCannotBeUpdated() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // update reserved offer capabilities
+ val updatedCaps = NetworkCapabilities(reservedCaps).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ cb.expect<Unavailable>()
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_updateAllowed() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS)
+
+ val updatedCaps = NetworkCapabilities(BLANKET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, blanketOffer)
+ blanketOffer.assertNoCallback()
+
+ // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+ // changes the underlying request.
+ val p2pRequest = NetworkRequest.Builder(NetworkRequest(ETHERNET_REQUEST))
+ .addCapability(NET_CAPABILITY_WIFI_P2P)
+ .build()
+ cm.reserveNetwork(p2pRequest, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(updatedCaps)
+ }
+
+ @Test
+ fun testReservationOffer_onlyAllowSingleOffer() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ val caps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, offerCb)
+ offerCb.expectOnNetworkNeeded(caps)
+ cb.expect<Reserved>()
+
+ val newOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, newOfferCb)
+ newOfferCb.assertNoCallback()
+ cb.assertNoCallback()
+
+ // File a regular request and validate only the old offer gets onNetworkNeeded.
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(caps)
+ newOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_updateScore() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ cb.expect<Reserved>()
+
+ // update reserved offer capabilities
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, reservedCaps, reservedOfferCb)
+ cb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_regularOfferCanBeUpdated() {
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, offerCb)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+
+ val updatedCaps = NetworkCapabilities(ETHERNET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, updatedCaps, offerCb)
+ offerCb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index c55096b..c2959f4 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -623,12 +623,14 @@
mNat64CidrController.maybeUpdateNat64Cidr();
}
- private static OtDaemonConfiguration newOtDaemonConfig(
- @NonNull ThreadConfiguration threadConfig) {
+ private OtDaemonConfiguration newOtDaemonConfig(ThreadConfiguration threadConfig) {
+ int srpServerConfig = R.bool.config_thread_srp_server_wait_for_border_routing_enabled;
+ boolean srpServerWaitEnabled = mResources.get().getBoolean(srpServerConfig);
return new OtDaemonConfiguration.Builder()
.setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
.setNat64Enabled(threadConfig.isNat64Enabled())
.setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+ .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
.build();
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 316f570..801e21e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -39,6 +39,7 @@
import android.os.SystemClock
import android.system.OsConstants
import android.system.OsConstants.IPPROTO_ICMP
+import android.util.Log
import androidx.test.core.app.ApplicationProvider
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.net.module.util.IpUtils
@@ -84,6 +85,8 @@
/** Utilities for Thread integration tests. */
object IntegrationTestUtils {
+ private val TAG = IntegrationTestUtils::class.simpleName
+
// The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
// every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
// seconds to be safe
@@ -483,6 +486,7 @@
val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceFound: $serviceInfo")
serviceInfoFuture.complete(serviceInfo)
}
}
@@ -530,6 +534,7 @@
val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceUpdated: $serviceInfo")
if (predicate.test(serviceInfo)) {
resolvedServiceInfoFuture.complete(serviceInfo)
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index dcbb3f5..1d44ccb 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -231,6 +231,9 @@
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -285,6 +288,9 @@
@Test
public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(false);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -297,6 +303,7 @@
mService.initialize();
mTestLooper.dispatchAll();
+ assertThat(mFakeOtDaemon.getConfiguration().srpServerWaitForBorderRoutingEnabled).isFalse();
MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);