Merge changes from topic "bpf_slash_progs" into main
* changes:
doc: echo >> staticlibs/native/README.md
bpf: git mv staticlibs/native/bpf_syscall_wrappers bpf/syscall_wrappers
bpf: git mv netbpfload bpf/loader
bpf: git mv bpf_progs bpf/progs
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 203d828..6e00756 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -49,7 +49,10 @@
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
- stub_only_libs: ["framework-connectivity.stubs.module_lib"],
+ stub_only_libs: [
+ "framework-connectivity.stubs.module_lib",
+ "sdk_module-lib_current_framework-wifi",
+ ],
jarjar_rules: ":framework-tethering-jarjar-rules",
installable: true,
@@ -97,13 +100,17 @@
srcs: [
":framework-tethering-srcs",
],
- libs: ["framework-connectivity.stubs.module_lib"],
+ libs: [
+ "framework-connectivity.stubs.module_lib",
+ "sdk_module-lib_current_framework-wifi",
+ ],
static_libs: [
"com.android.net.flags-aconfig-java",
],
aidl: {
include_dirs: [
"packages/modules/Connectivity/framework/aidl-export",
+ "packages/modules/Wifi/framework/aidl-export",
],
},
apex_available: ["com.android.tethering"],
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index cccafd5..3efaac2 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -102,6 +102,7 @@
method public int getConnectivityScope();
method @Nullable public android.net.LinkAddress getLocalIpv4Address();
method public boolean getShouldShowEntitlementUi();
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
method public int getTetheringType();
method public boolean isExemptFromEntitlementCheck();
method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -114,6 +115,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 1f6011a..5aca642 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -26,6 +26,8 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Context;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
@@ -744,6 +746,7 @@
mBuilderParcel.exemptFromEntitlementCheck = false;
mBuilderParcel.showProvisioningUi = true;
mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
+ mBuilderParcel.softApConfig = null;
}
/**
@@ -803,6 +806,30 @@
return this;
}
+ /**
+ * Set the desired SoftApConfiguration for {@link #TETHERING_WIFI}. If this is null or
+ * not set, then the persistent tethering SoftApConfiguration from
+ * {@link WifiManager#getSoftApConfiguration()} will be used.
+ * </p>
+ * If TETHERING_WIFI is already enabled and a new request is made with a different
+ * SoftApConfiguration, the request will be accepted if the device can support an
+ * additional tethering Wi-Fi AP interface. Otherwise, the request will be rejected.
+ *
+ * @param softApConfig SoftApConfiguration to use.
+ * @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setSoftApConfiguration(@Nullable SoftApConfiguration softApConfig) {
+ if (mBuilderParcel.tetheringType != TETHERING_WIFI) {
+ throw new IllegalArgumentException(
+ "SoftApConfiguration can only be set for TETHERING_WIFI");
+ }
+ mBuilderParcel.softApConfig = softApConfig;
+ return this;
+ }
+
/** Build {@link TetheringRequest} with the currently set configuration. */
@NonNull
public TetheringRequest build() {
@@ -884,6 +911,15 @@
}
/**
+ * Get the desired SoftApConfiguration of the request, if one was specified.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @Nullable
+ public SoftApConfiguration getSoftApConfiguration() {
+ return mRequestParcel.softApConfig;
+ }
+
+ /**
* Get a TetheringRequestParcel from the configuration
* @hide
*/
@@ -896,9 +932,10 @@
return "TetheringRequest [ type= " + mRequestParcel.tetheringType
+ ", localIPv4Address= " + mRequestParcel.localIPv4Address
+ ", staticClientAddress= " + mRequestParcel.staticClientAddress
- + ", exemptFromEntitlementCheck= "
- + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= "
- + mRequestParcel.showProvisioningUi + " ]";
+ + ", exemptFromEntitlementCheck= " + mRequestParcel.exemptFromEntitlementCheck
+ + ", showProvisioningUi= " + mRequestParcel.showProvisioningUi
+ + ", softApConfig= " + mRequestParcel.softApConfig
+ + " ]";
}
@Override
@@ -912,7 +949,8 @@
&& Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
&& parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
&& parcel.showProvisioningUi == otherParcel.showProvisioningUi
- && parcel.connectivityScope == otherParcel.connectivityScope;
+ && parcel.connectivityScope == otherParcel.connectivityScope
+ && Objects.equals(parcel.softApConfig, otherParcel.softApConfig);
}
@Override
@@ -920,7 +958,7 @@
TetheringRequestParcel parcel = getParcel();
return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
- parcel.showProvisioningUi, parcel.connectivityScope);
+ parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig);
}
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index f13c970..ea7a353 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -17,6 +17,7 @@
package android.net;
import android.net.LinkAddress;
+import android.net.wifi.SoftApConfiguration;
/**
* Configuration details for requesting tethering.
@@ -29,4 +30,5 @@
boolean exemptFromEntitlementCheck;
boolean showProvisioningUi;
int connectivityScope;
+ SoftApConfiguration softApConfig;
}
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
new file mode 100644
index 0000000..20ecbce
--- /dev/null
+++ b/networksecurity/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "NetworkSecurityUnitTests"
+ }
+ ]
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
new file mode 100644
index 0000000..6e787d1
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -0,0 +1,159 @@
+/*
+ * 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.app.DownloadManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Helper class to download certificate transparency log files. */
+class CertificateTransparencyDownloader extends BroadcastReceiver {
+
+ private static final String TAG = "CertificateTransparencyDownloader";
+
+ private final Context mContext;
+ private final DataStore mDataStore;
+ private final DownloadHelper mDownloadHelper;
+
+ @VisibleForTesting
+ CertificateTransparencyDownloader(
+ Context context, DataStore dataStore, DownloadHelper downloadHelper) {
+ mContext = context;
+ mDataStore = dataStore;
+ mDownloadHelper = downloadHelper;
+ }
+
+ CertificateTransparencyDownloader(Context context, DataStore dataStore) {
+ this(context, dataStore, new DownloadHelper(context));
+ }
+
+ void registerReceiver() {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ mContext.registerReceiver(this, intentFilter);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ }
+ }
+
+ void startMetadataDownload(String metadataUrl) {
+ long downloadId = download(metadataUrl);
+ if (downloadId == -1) {
+ Log.e(TAG, "Metadata download request failed for " + metadataUrl);
+ return;
+ }
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
+ mDataStore.store();
+ }
+
+ void startContentDownload(String contentUrl) {
+ long downloadId = download(contentUrl);
+ if (downloadId == -1) {
+ Log.e(TAG, "Content download request failed for " + contentUrl);
+ return;
+ }
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
+ mDataStore.store();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
+ Log.w(TAG, "Received unexpected broadcast with action " + action);
+ return;
+ }
+
+ long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ if (completedId == -1) {
+ Log.e(TAG, "Invalid completed download Id");
+ return;
+ }
+
+ if (isMetadataDownloadId(completedId)) {
+ handleMetadataDownloadCompleted(completedId);
+ return;
+ }
+
+ if (isContentDownloadId(completedId)) {
+ handleContentDownloadCompleted(completedId);
+ return;
+ }
+
+ Log.e(TAG, "Download id " + completedId + " is neither metadata nor content.");
+ }
+
+ private void handleMetadataDownloadCompleted(long downloadId) {
+ if (!mDownloadHelper.isSuccessful(downloadId)) {
+ Log.w(TAG, "Metadata download failed.");
+ // TODO: re-attempt download
+ return;
+ }
+
+ startContentDownload(mDataStore.getProperty(Config.CONTENT_URL));
+ }
+
+ private void handleContentDownloadCompleted(long downloadId) {
+ if (!mDownloadHelper.isSuccessful(downloadId)) {
+ Log.w(TAG, "Content download failed.");
+ // TODO: re-attempt download
+ return;
+ }
+
+ Uri contentUri = getContentDownloadUri();
+ Uri metadataUri = getMetadataDownloadUri();
+ if (contentUri == null || metadataUri == null) {
+ Log.e(TAG, "Invalid URIs");
+ return;
+ }
+
+ // TODO: 1. verify file signature, 2. validate file content, 3. install log file.
+ }
+
+ private long download(String url) {
+ try {
+ return mDownloadHelper.startDownload(url);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Download request failed", e);
+ return -1;
+ }
+ }
+
+ @VisibleForTesting
+ boolean isMetadataDownloadId(long downloadId) {
+ return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+ }
+
+ @VisibleForTesting
+ boolean isContentDownloadId(long downloadId) {
+ return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+ }
+
+ private Uri getMetadataDownloadUri() {
+ return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+ }
+
+ private Uri getContentDownloadUri() {
+ return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index 8dd5951..ecf94d5 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -30,17 +30,25 @@
/** Listener class for the Certificate Transparency Phenotype flags. */
class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
- private static final String TAG = "CertificateTransparency";
+ private static final String TAG = "CertificateTransparencyFlagsListener";
- private static final String VERSION = "version";
- private static final String CONTENT_URL = "content_url";
- private static final String METADATA_URL = "metadata_url";
+ private final DataStore mDataStore;
+ private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
- CertificateTransparencyFlagsListener(Context context) {}
+ CertificateTransparencyFlagsListener(Context context) {
+ mDataStore = new DataStore(Config.PREFERENCES_FILE);
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(context, mDataStore);
+ }
void initialize() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.registerReceiver();
DeviceConfig.addOnPropertiesChangedListener(
NAMESPACE_TETHERING, Executors.newSingleThreadExecutor(), this);
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
+ }
// TODO: handle property changes triggering on boot before registering this listener.
}
@@ -50,18 +58,38 @@
return;
}
- String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, VERSION, "");
- String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, CONTENT_URL, "");
- String newMetadataUrl = DeviceConfig.getString(NAMESPACE_TETHERING, METADATA_URL, "");
+ String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, Config.VERSION, "");
+ String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, Config.CONTENT_URL, "");
+ String newMetadataUrl =
+ DeviceConfig.getString(NAMESPACE_TETHERING, Config.METADATA_URL, "");
if (TextUtils.isEmpty(newVersion)
|| TextUtils.isEmpty(newContentUrl)
|| TextUtils.isEmpty(newMetadataUrl)) {
return;
}
- Log.d(TAG, "newVersion=" + newVersion);
- Log.d(TAG, "newContentUrl=" + newContentUrl);
- Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
- // TODO: start download of URLs.
+ if (Config.DEBUG) {
+ 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;
+ }
+
+ mDataStore.setProperty(Config.VERSION, newVersion);
+ mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
+ mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
+ mDataStore.store();
+
+ mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
}
}
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 406a57f..52478c0 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -19,7 +19,6 @@
import android.content.Context;
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
-import android.util.Log;
import com.android.net.ct.flags.Flags;
import com.android.net.module.util.DeviceConfigUtils;
@@ -29,7 +28,6 @@
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
- private static final String TAG = "CertificateTransparency";
private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
"certificate_transparency_service_enabled";
@@ -59,7 +57,6 @@
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- Log.d(TAG, "setting up flags listeners");
mFlagsListener.initialize();
break;
default:
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
new file mode 100644
index 0000000..e184359
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -0,0 +1,42 @@
+/*
+ * 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.content.ApexEnvironment;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+import java.io.File;
+
+/** Class holding the constants used by the CT feature. */
+final class Config {
+
+ static final boolean DEBUG = false;
+
+ // preferences file
+ private static final File DEVICE_PROTECTED_DATA_DIR =
+ ApexEnvironment.getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ private static final String PREFERENCES_FILE_NAME = "ct.preferences";
+ static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
+
+ // flags and properties names
+ static final String VERSION = "version";
+ static final String CONTENT_URL = "content_url";
+ static final String CONTENT_URL_KEY = "content_url_key";
+ static final String METADATA_URL = "metadata_url";
+ static final String METADATA_URL_KEY = "metadata_url_key";
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
new file mode 100644
index 0000000..cd6aebf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -0,0 +1,67 @@
+/*
+ * 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.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.Properties;
+
+/** Class to persist data needed by CT. */
+class DataStore extends Properties {
+
+ private static final String TAG = "CertificateTransparency";
+
+ private final File mPropertyFile;
+
+ DataStore(File file) {
+ super();
+ mPropertyFile = file;
+ }
+
+ void load() {
+ if (!mPropertyFile.exists()) {
+ return;
+ }
+ try (InputStream in = new FileInputStream(mPropertyFile)) {
+ load(in);
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading property store", e);
+ }
+ }
+
+ void store() {
+ try (OutputStream out = new FileOutputStream(mPropertyFile)) {
+ store(out, "");
+ } catch (IOException e) {
+ Log.e(TAG, "Error storing property store", e);
+ }
+ }
+
+ long getPropertyLong(String key, long defaultValue) {
+ return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
+ }
+
+ Object setPropertyLong(String key, long value) {
+ return setProperty(key, Long.toString(value));
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
new file mode 100644
index 0000000..cc8c4c0
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
@@ -0,0 +1,90 @@
+/*
+ * 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.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Class to handle downloads for Certificate Transparency. */
+public class DownloadHelper {
+
+ private final DownloadManager mDownloadManager;
+
+ @VisibleForTesting
+ DownloadHelper(DownloadManager downloadManager) {
+ mDownloadManager = downloadManager;
+ }
+
+ DownloadHelper(Context context) {
+ this(context.getSystemService(DownloadManager.class));
+ }
+
+ /**
+ * Sends a request to start the download of a provided url.
+ *
+ * @param url the url to download
+ * @return a downloadId if the request was created successfully, -1 otherwise.
+ */
+ public long startDownload(String url) {
+ return mDownloadManager.enqueue(
+ new Request(Uri.parse(url))
+ .setAllowedOverRoaming(false)
+ .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
+ .setRequiresCharging(true));
+ }
+
+ /**
+ * Returns true if the specified download completed successfully.
+ *
+ * @param downloadId the download.
+ * @return true if the download completed successfully.
+ */
+ public boolean isSuccessful(long downloadId) {
+ try (Cursor cursor = mDownloadManager.query(new Query().setFilterById(downloadId))) {
+ if (cursor == null) {
+ return false;
+ }
+ if (cursor.moveToFirst()) {
+ int status =
+ cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
+ if (DownloadManager.STATUS_SUCCESSFUL == status) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the URI of the specified download, or null if the download did not complete
+ * successfully.
+ *
+ * @param downloadId the download.
+ * @return the {@link Uri} if the download completed successfully, null otherwise.
+ */
+ public Uri getUri(long downloadId) {
+ if (downloadId == -1) {
+ return null;
+ }
+ return mDownloadManager.getUriForDownloadedFile(downloadId);
+ }
+}
diff --git a/networksecurity/tests/unit/Android.bp b/networksecurity/tests/unit/Android.bp
new file mode 100644
index 0000000..639f644
--- /dev/null
+++ b/networksecurity/tests/unit/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+ default_team: "trendy_team_platform_security",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "NetworkSecurityUnitTests",
+ defaults: ["mts-target-sdk-version-current"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+
+ srcs: ["src/**/*.java"],
+
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "mockito-target-minus-junit4",
+ "service-networksecurity-pre-jarjar",
+ "truth",
+ ],
+
+ sdk_version: "test_current",
+}
diff --git a/networksecurity/tests/unit/AndroidManifest.xml b/networksecurity/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a3f4b7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.net.ct">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.net.ct"
+ android:label="NetworkSecurity Mainline Module Tests" />
+</manifest>
diff --git a/networksecurity/tests/unit/AndroidTest.xml b/networksecurity/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..3c94df7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<configuration description="Runs NetworkSecurity Mainline unit Tests.">
+ <option name="test-tag" value="NetworkSecurityUnitTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NetworkSecurityUnitTests.apk" />
+ </target_preparer>
+
+ <option name="config-descriptor:metadata" key="mainline-param"
+ value="com.google.android.tethering.next.apex" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.net.ct" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+
+ <!-- Only run in MTS if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+</configuration>
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
new file mode 100644
index 0000000..acd0d36
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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 static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+import android.content.Context;
+import android.content.Intent;
+
+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 org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link CertificateTransparencyDownloader}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyDownloaderTest {
+
+ @Mock private DownloadHelper mDownloadHelper;
+
+ private Context mContext;
+ private File mTempFile;
+ private DataStore mDataStore;
+ private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+ @Before
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ mDataStore.load();
+
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(mContext, mDataStore, mDownloadHelper);
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDownloader_startMetadataDownload() {
+ String metadataUrl = "http://test-metadata.org";
+ long downloadId = 666;
+ when(mDownloadHelper.startDownload(metadataUrl)).thenReturn(downloadId);
+
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
+ mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_startContentDownload() {
+ String contentUrl = "http://test-content.org";
+ long downloadId = 666;
+ when(mDownloadHelper.startDownload(contentUrl)).thenReturn(downloadId);
+
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
+ mCertificateTransparencyDownloader.startContentDownload(contentUrl);
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_handleMetadataCompleteSuccessful() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(true);
+
+ long contentId = 666;
+ String contentUrl = "http://test-content.org";
+ mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+ when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_handleMetadataCompleteFailed() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(false);
+
+ String contentUrl = "http://test-content.org";
+ mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ verify(mDownloadHelper, never()).startDownload(contentUrl);
+ }
+
+ @Test
+ public void testDownloader_handleContentCompleteSuccessful() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+
+ long contentId = 666;
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+ when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mDownloadHelper, times(1)).getUri(metadataId);
+ verify(mDownloadHelper, times(1)).getUri(contentId);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId) {
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
new file mode 100644
index 0000000..d16f138
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link DataStore}. */
+@RunWith(JUnit4.class)
+public class DataStoreTest {
+
+ private File mTempFile;
+ private DataStore mDataStore;
+
+ @Before
+ public void setUp() throws IOException {
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ mDataStore.load();
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDataStore_propertyFileCreatedSuccessfully() {
+ assertThat(mTempFile.exists()).isTrue();
+ assertThat(mDataStore.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testDataStore_propertySet() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+
+ @Test
+ public void testDataStore_propertyStore() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+ mDataStore.store();
+
+ mDataStore.clear();
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.load();
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
new file mode 100644
index 0000000..0b65e3c
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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 static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for the {@link DownloadHelper}. */
+@RunWith(JUnit4.class)
+public class DownloadHelperTest {
+
+ @Mock private DownloadManager mDownloadManager;
+
+ private DownloadHelper mDownloadHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mDownloadHelper = new DownloadHelper(mDownloadManager);
+ }
+
+ @Test
+ public void testDownloadHelper_scheduleDownload() {
+ long downloadId = 666;
+ when(mDownloadManager.enqueue(any())).thenReturn(downloadId);
+
+ assertThat(mDownloadHelper.startDownload("http://test.org")).isEqualTo(downloadId);
+ }
+
+ @Test
+ public void testDownloadHelper_wrongUri() {
+ when(mDownloadManager.enqueue(any())).thenReturn(666L);
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mDownloadHelper.startDownload("not_a_uri"));
+ }
+}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 919e025..798cf98 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -44,6 +44,7 @@
"general-tests",
"sts",
],
+ min_sdk_version: "31",
}
android_test_helper_app {
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 8794847..1454d9a 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -22,6 +22,9 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
@@ -34,6 +37,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -60,7 +64,9 @@
import android.net.cts.util.CtsTetheringUtils;
import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.net.wifi.WifiSsid;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
@@ -71,6 +77,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.ParcelUtils;
import org.junit.After;
@@ -78,6 +85,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -216,12 +224,26 @@
@Test
public void testTetheringRequest() {
- final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ SoftApConfiguration softApConfiguration;
+ if (SdkLevel.isAtLeastT()) {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(
+ "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setSsid("This is an SSID!")
+ .build();
+ }
+ final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfiguration)
+ .build();
assertEquals(TETHERING_WIFI, tr.getTetheringType());
assertNull(tr.getLocalIpv4Address());
assertNull(tr.getClientStaticIpv4Address());
assertFalse(tr.isExemptFromEntitlementCheck());
assertTrue(tr.getShouldShowEntitlementUi());
+ assertEquals(softApConfiguration, tr.getSoftApConfiguration());
final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
@@ -244,15 +266,57 @@
}
@Test
+ public void testTetheringRequestSetSoftApConfigurationFailsWhenNotWifi() {
+ final SoftApConfiguration softApConfiguration;
+ if (SdkLevel.isAtLeastT()) {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(
+ "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setSsid("This is an SSID!")
+ .build();
+ }
+ for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_WIFI_P2P,
+ TETHERING_NCM, TETHERING_ETHERNET)) {
+ try {
+ new TetheringRequest.Builder(type).setSoftApConfiguration(softApConfiguration);
+ fail("Was able to set SoftApConfiguration for tethering type " + type);
+ } catch (IllegalArgumentException e) {
+ // Success
+ }
+ }
+ }
+
+ @Test
public void testTetheringRequestParcelable() {
+ final SoftApConfiguration softApConfiguration;
+ if (SdkLevel.isAtLeastT()) {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(
+ "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ softApConfiguration = new SoftApConfiguration.Builder()
+ .setSsid("This is an SSID!")
+ .build();
+ }
final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
- final TetheringRequest unparceled = new TetheringRequest.Builder(TETHERING_USB)
+ final TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfiguration)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
.setShouldShowEntitlementUi(false).build();
- final TetheringRequest parceled = ParcelUtils.parcelingRoundTrip(unparceled);
- assertEquals(unparceled, parceled);
+ final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+ assertEquals(withConfig, ParcelUtils.parcelingRoundTrip(withConfig));
+ assertEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+ assertNotEquals(withConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+ assertNotEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withConfig));
}
@Test
diff --git a/thread/README.md b/thread/README.md
index 41b73ac..f2bd3b2 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -16,3 +16,7 @@
Open `https://localhost:8443/` in your web browser, you can find the Thread
demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
+
+## More docs
+
+- [Make your Android Border Router](./docs/make-your-android-border-router.md)
diff --git a/thread/docs/android-thread-arch.png b/thread/docs/android-thread-arch.png
new file mode 100644
index 0000000..ea408fa
--- /dev/null
+++ b/thread/docs/android-thread-arch.png
Binary files differ
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
new file mode 100644
index 0000000..257999b
--- /dev/null
+++ b/thread/docs/build-an-android-border-router.md
@@ -0,0 +1,526 @@
+# Build an Android Border Router
+
+If you are not an Android device or Thread chip vendor, you can stop reading
+now.
+
+This document walks you through the steps to build a new Android-based Thread
+Border Router device with the latest AOSP source code. By following this
+document, you will learn:
+
+1. [the overall architecture and status of Thread support in Android](#architecture)
+2. [how to create your own Thread HAL service](#build-your-thread-hal-service)
+3. [how to make your device compatible with Google Home](#be-compatible-with-google-home)
+4. [how to test your Thread Border Router](#testing)
+
+If you need support, file an issue in
+[GitHub](https://github.com/openthread/ot-br-posix/issues) or open a
+[Dicussion](https://github.com/orgs/openthread/discussions) if you have any
+questions.
+
+Note: Before creating an issue or discussion, search to see if it has already
+been reported or asked.
+
+## Overview
+
+The Android Thread stack is based on OpenThread and `ot-br-posix` which are
+open-sourced by Google in GitHub. The same way OpenThread is developed in a
+public GitHub repository, so the Android Thread stack is developed in the
+public [AOSP codebase](https://cs.android.com/). All features and bug fixes
+are submitted first in AOSP. This allows vendors to start adopting the latest
+Thread versions without waiting for regular Android releases.
+
+### Architecture
+
+The whole Android Thread stack consists of two major components: the core
+Thread stack in a generic system partition and the Thread HAL service in a
+vendor partition. Device vendors typically need only to take care and build the
+HAL service.
+
+
+
+Here is a brief summary of how the Android Thread stack works:
+- There is a Java Thread system service in the system server which manages the
+ whole stack - provides the Thread system API, creates the `thread-wpan`
+ tunnel interface, registers the Thread network to the
+ [Connectivity service](https://developer.android.com/reference/android/content/Context#CONNECTIVITY_SERVICE)
+ and implements the Border Routing and Advertising Proxy functionalities.
+- The core Thread / OpenThread stack is hosted in a non-privileged standalone
+ native process which is named `ot-daemon`. `ot-daemon` is directly managed
+ by the Java system service via private AIDL APIs and it accesses the Thread
+ hardware radio through the Thread HAL API.
+- A vendor-provided Thread HAL service MUST implement the Thread HAL API. It
+ typically works as an
+ [RCP](https://openthread.io/platforms/co-processor#radio_co-processor_rcp)
+ and implements the
+ [spinel](https://openthread.io/platforms/co-processor#spinel_protocol)
+ protocol.
+
+Note: Both the Java system service and ot-daemon are delivered in a
+[Tethering](https://source.android.com/docs/core/ota/modular-system/tethering#overview)
+mainline module. For mobile devices, the binary is managed by Google and
+delivered to devices via Google Play monthly. For non-mobile devices such as
+TV, vendors are free to build from source or use a prebuilt.
+
+### Where is the code?
+
+- The Android Thread framework / API and service: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/thread/
+- The Thread HAL API and default service implementation: https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/
+- Imported OpenThread repo: https://cs.android.com/android/platform/superproject/main/+/main:external/openthread/
+- Imported ot-br-posix repo: https://cs.android.com/android/platform/superproject/main/+/main:external/ot-br-posix/
+
+## Set up development environment
+
+Android device vendors who have already established an Android development
+environment for the device can skip this section.
+
+If you are new to Android ecosystem or you are a silicon vendor who wants to
+make your Thread chip compatible with Android and provide support for device
+vendors, keep reading.
+
+### Follow the Android developer codelab
+
+To set up your Android development environment for the first time, use the
+following codelab: https://source.android.com/docs/setup/start. At the end of
+this codelab, you will be able to build and run a simulated Cuttlefish device
+from source code.
+
+## Build your Thread HAL service
+
+### Try Thread in Cuttlefish
+
+[Cuttlefish](https://source.android.com/docs/devices/Cuttlefish) is the virtual
+Android device. Before starting building your own HAL service, it's better to
+try Thread in Cuttlefish to understand how HAL works.
+
+A default Thread HAL service is provided in Cuttlefish and it's implemented
+with the [simulated RCP](https://github.com/openthread/openthread/tree/main/examples/platforms/simulation)
+which transceives packets via UDP socket to and from a simulated
+Thread (802.15.4) radio.
+
+In the Cuttlefish instance, a "ThreadNetworkDemoApp" is pre-installed. Open
+that app to join the Cuttlefish device into a default Thread network.
+
+
+
+Note: The Border Router functionalities will be started (and OMR address will
+be created) only when the Cuttlefish device is connected to a virtual Wi-Fi
+network. You can connect the Wi-Fi network in Settings.
+
+There are also the `ot-ctl` and `ot-cli-ftd` command line tools provided to
+configure your Thread network for testing. Those tools support all the
+OpenThread CLI commands that you may be familiar with already.
+
+You can grep for logs of the Cuttlefish Thread HAL service by:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+
+07-21 10:43:05.048 0 0 I init : Parsing file /apex/com.android.hardware.threadnetwork/etc/threadnetwork-service.rc...
+07-21 10:59:27.233 580 580 W android.hardware.threadnetwork-service: ThreadChip binder is unlinked
+07-21 10:59:27.233 580 580 I android.hardware.threadnetwork-service: Close IThreadChip successfully
+07-21 10:59:27.385 580 580 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+Or grep for ot-daemon logs by:
+
+```
+$ adb logcat | egrep -i ot-daemon
+07-21 10:43:48.741 0 0 I init : starting service 'ot-daemon'...
+07-21 10:43:48.742 0 0 I init : Created socket '/dev/socket/ot-daemon/thread-wpan.sock', mode 660, user 1084, group 1084
+07-21 10:43:48.762 0 0 I init : ... started service 'ot-daemon' has pid 2473
+07-21 10:46:26.320 2473 2473 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-21 10:46:30.290 2473 2473 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-21 10:48:07.264 2473 2473 I ot-daemon: [INFO]-BINDER--: Start joining...
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] Settings------: Saved ActiveDataset
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] DatasetManager: Active dataset set
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] DnssdServer---: Started
+07-21 10:48:07.273 2473 2473 I ot-daemon: [N] Mle-----------: Role disabled -> detached
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Mle-----------: AttachState Idle -> Start
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) [Ip6+ Role LLAddr MLAddr KeySeqCntr Ip6Mult+ Channel PanId NetName ExtPanId ...
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) ... NetworkKey PSKc SecPolicy NetifState ActDset]
+```
+
+Note: You can also capture Thread system server log with the tag
+"ThreadNetworkService".
+
+The Cuttlefish Thread HAL service uses the default Thread HAL service plus the
+OpenThread simulated RCP binary, see the next section for how it works.
+
+### The default HAL service
+
+A [default HAL service](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/aidl/default/)
+is included along with the Thread HAL API. The default HAL service supports
+both simulated and real RCP devices. It receives an optional RCP device URL and
+if the URL is not provided, it defaults to the simulated RCP device.
+
+In file `hardware/interfaces/threadnetwork/aidl/default/threadnetwork-service.rc`:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service
+ class hal
+ user thread_network
+```
+
+This is equivalent to:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+forkpty:///apex/com.android.hardware.threadnetwork/bin/ot-rcp?forkpty-arg=1
+ class hal
+ user thread_network
+```
+
+For real RCP devices, it supports both SPI and UART interace and you can
+specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
+`spinel+socket://` respectively.
+
+Note: `spinel+socket://` is a new spinel interface added in the default Thread
+HAL, it supports transmitting spinel frame via an Unix socket. A full
+socket-based radio URL may be like
+`spinel+socket:///data/vendor/threadnetwork/thread_spinel_socket`.
+
+#### Understand the vendor APEX
+
+Similar to the Thread stack in the Tethering mainline module, the default Thread
+HAL service in Cuttlefish is packaged in an APEX module as well. But it's a
+vendor APEX module which will be installed to `/vendor/apex/` (The artifacts in
+the module will be unzipped to `/apex/com.android.hardware.threadnetwork/`).
+
+```aidl
+apex {
+ name: "com.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ "ot-rcp",
+ ],
+
+ prebuilts: [
+ "threadnetwork-default.xml", // vintf_fragment
+ "threadnetwork-service.rc", // init_rc
+ "android.hardware.thread_network.prebuilt.xml", // permission
+ ],
+}
+```
+
+There are a few important configurations that you will need to pay attention or
+make changes to when building your own HAL APEX module:
+
+- `file_contexts`: This describes the binary / data files delivered in this
+ APEX module or files the HAL service need to access (for example, the RCP
+ device). This allows you to specify specific sepolicy rules for your HAL
+ service to access the hardware RCP device.
+- `binaries`: The binary file delivered in this APEX module
+- `threadnetwork-service.rc`: How the HAL service will be started. You need to
+ specify the RCP device path here.
+- `android.hardware.thread_network.prebuilt.xml`: Defines the
+ `android.hardware.thread_network` hardware feature. This is required for the
+ Android system to know that your device does have Thread hardware support.
+ Otherwise, the Android Thread stack won't be enabled.
+
+### Create your HAL service
+
+Whether you are an Android device developer or a silicon vendor, you should be
+familiar with building OT RCP firmware for your Thread chip. The following
+instructions assume that the hardware chip is correctly wired and
+validated.
+
+The simplest way to build your HAL APEX is to create a new APEX with the
+binaries and prebuilts of the default HAL APEX. For example, if your company is
+Banana and the RCP device on your device is `/dev/ttyACM0`, your Thread HAL
+APEX will look like this:
+
+- `Android.bp`:
+ ```
+ prebuilt_etc {
+ name: "banana-threadnetwork-service.rc",
+ src: "banana-threadnetwork-service.rc",
+ installable: false,
+ }
+
+ apex {
+ name: "com.banana.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ ],
+
+ prebuilts: [
+ "banana-threadnetwork-service.rc",
+ "threadnetwork-default.xml",
+ "android.hardware.thread_network.prebuilt.xml",
+ ],
+ }
+ ```
+- `file_contexts`:
+ ```
+ (/.*)? u:object_r:vendor_file:s0
+ /etc(/.*)? u:object_r:vendor_configs_file:s0
+ /bin/hw/android\.hardware\.threadnetwork-service u:object_r:hal_threadnetwork_default_exec:s0
+ /dev/ttyACM0 u:object_r:threadnetwork_rcp_device:s0
+ ```
+ The file paths in the first column are related to `/apex/com.android.hardware.threadnetwork/`.
+- `threadnetwork-service.rc`:
+ ```
+ service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=115200
+ class hal
+ user root
+ ```
+- `manifest.json`:
+ ```
+ {
+ "name": "com.android.hardware.threadnetwork",
+ "version": 1
+ }
+ ```
+
+Note: The default Thread HAL service is not designed to be a generic system
+component which works for all Android devices. If the default implementation
+can't support your device, you are free to make a copy and change it for your
+needs. In this case, it's just simpler to create a new APEX module without
+overriding the default one.
+
+Assuming you are making a new device named Orange, your device specific
+configuration directory will be like:
+
+```
+device/banana/orange/threadnetwork/
+ sepolicy/
+ Android.bp
+ file_contexts
+ manifest.json
+ threadnetwork-default.xml
+ threadnetwork-service.rc
+```
+
+See the next section for what sepolicy rules should be added in the `sepolicy/`
+sub-directory.
+
+#### Sepolicy rules for RCP device
+
+By default, your Thread HAL service doesn't have access to the RCP device (for
+example `/dev/ttyACM0`), custom sepolicy rules need to be added to the
+`sepolicy/` directory.
+
+Create a new `sepolicy/threadnetwork_hal.te` file with below content:
+
+```
+type threadnetwork_rcp_device, dev_type;
+
+# Allows the Thread HAL service to read / write the Thread RCP device
+allow hal_threadnetwork_default threadnetwork_rcp_device:chr_file rw_file_perms;
+```
+
+#### Put together
+
+Now you have finished almost all the code needs for adding Thread, the last
+step is to add the Thread HAL APEX and sepolicy rules to your device's image.
+
+You can do this by adding below code to your device's `Makefile` (for example,
+`device.mk`):
+
+```
+PRODUCT_PACKAGES += com.banana.hardware.threadnetwork
+BOARD_SEPOLICY_DIRS += device/banana/orange/threadnetwork/sepolicy
+```
+
+Note: Unfortunately, APEX module doesn't support sepolicy rules, so you need
+to explicitly specify the sepolicy directory separately.
+
+If everything works, now you will be able to see the Thread HAL service log similar to:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: ServiceName: android.hardware.threadnetwork.IThreadChip/chip0, Url: spinel+spi
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: Thread Network HAL is running
+08-13 13:26:55.165 477 477 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+And the `ot-daemon` log will be like:
+
+```
+$ adb logcat -s ot-daemon
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Running OTBR_AGENT/Unknown
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread version: 1.3.0
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread interface: thread-wpan
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Backbone interface is not specified
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Radio URL: threadnetwork_hal://binder?none
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-ILS-----: Infra link selected:
+08-13 13:26:55.160 1019 1019 I ot-daemon: [I] Platform------: [HAL] Wait for getting the service android.hardware.threadnetwork.IThreadChip/chip0 ...
+08-13 13:26:55.165 1019 1019 I ot-daemon: [I] Platform------: [HAL] Successfully got the service android.hardware.threadnetwork.IThreadChip/chip0
+08-13 13:26:55.275 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_UNKNOWN
+08-13 13:26:55.276 1019 1019 I ot-daemon: [I] P-RadioSpinel-: Software reset RCP successfully
+08-13 13:26:55.277 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_POWER_ON
+08-13 13:26:55.322 1019 1019 I ot-daemon: [I] ChildSupervsn-: Timeout: 0 -> 190
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Initializing - InfraIfIndex:0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] InfraIf-------: Init infra netif 0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] Settings------: Read BrUlaPrefix fd7b:cc45:ff06::/48
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: BR ULA prefix: fd7b:cc45:ff06::/48 (loaded)
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Generated local OMR prefix: fd7b:cc45:ff06:1::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: Local on-link prefix: fdde:ad00:beef:cafe::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Enabling
+```
+
+## Customization
+
+The Thread mainline module (it's actually a part of the "Tethering" module)
+provides a few [overlayable configurations](https://source.android.com/docs/core/runtime/rros)
+which can be specified by vendors to customize the stack behavior. See
+[config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
+for the full list.
+
+Typically, you must change the `config_thread_vendor_name`,
+`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
+product values. Those values will be included in the `_meshcop._udp` mDNS
+service which is always advertised by a Thread Border Router.
+
+To add the overlay, you need to create a new `ConnectivityOverlayOrange`
+runtime_resource_overlay target for your Orange device. Create a new
+`ConnectivityOverlay/` directory under `device/banana/orange/rro_overlays` and
+create below contents in it:
+
+```
+device/banana/orange/rro_overlays/ConnectivityOverlay/
+ res
+ values
+ config_thread.xml
+ Android.bp
+ AndroidManifest.xml
+```
+
+- `Android.bp`:
+ ```
+ package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ }
+
+ runtime_resource_overlay {
+ name: "ConnectivityOverlayOrange",
+ manifest: "AndroidManifest.xml",
+ resource_dirs: ["res"],
+ certificate: "platform",
+ product_specific: true,
+ sdk_version: "current",
+ }
+ ```
+- `AndroidManifest.xml`:
+ ```
+ <!-- Orange overlays for the Connectivity module -->
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.banana.android.connectivity.resources.orange"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:hasCode="false" />
+
+ <!-- If your device uses google-signed mainline modules, the targetPackage
+ needs to be "com.google.android.connectivity.resources", otherise, it
+ should be "com.android.connectivity.resources"
+ -->
+ <overlay
+ android:targetPackage="com.google.android.connectivity.resources"
+ android:targetName="ServiceConnectivityResourcesConfig"
+ android:isStatic="true"
+ android:priority="1"/>
+ </manifest>
+ ```
+- `config_thread.xml`:
+ ```
+ <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
+ <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
+ <string translatable="false" name="config_thread_model_name">Orange</string>
+ ```
+
+Similar to the HAL APEX, you need to add the overlay app to your `device.mk`
+file:
+
+```
+PRODUCT_PACKAGES += \
+ ConnectivityOverlayOrange
+```
+
+If everything works, you will see that `ot-daemon` logs the vendor and model name
+at the very beginning of the log:
+```
+$ adb logcat -s ot-daemon
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Input: state
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: disabled
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: Done
+07-22 15:31:37.693 1472 1472 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] Cli-----------: Input: factoryreset
+07-22 15:31:50.092 1472 1472 I ot-daemon: [I] Settings------: Wiped all info
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-ADPROXY-: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-DPROXY--: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Stop Thread Border Agent
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Unpublish meshcop service Banana Inc. Orange #4833._meshcop._udp.local
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Removing service Banana Inc. Orange #4833._meshcop._udp
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Unpublishing service Banana Inc. Orange #4833._meshcop._udp listener ID = 0
+```
+
+Note: In case the overlay doesn't work, check https://source.android.com/docs/core/runtime/rro-troubleshoot
+for troubleshooting instructions.
+
+### Be compatible with Google Home
+
+Additionally, if you want to make your Border Router be used by the Google Home
+ecosystem, you can specify this configuration in `config_thread.xml`:
+
+```
+<string-array name="config_thread_mdns_vendor_specific_txts">
+ <item>vgh=1</item>
+</string-array>
+```
+
+## Testing
+
+Your device should be compatible with the Thread 1.3+ Border Router
+specification now. Before sending it to the Thread certification program, there
+are a few Android xTS tests should be exercised to ensure the compatibility.
+
+- The VTS test makes sure Thread HAL service work as expected on your device.
+ You can run the tests with command
+ ```
+ atest VtsHalThreadNetworkTargetTest
+ ```
+- The CTS test makes sure Thread APIs work as expected on your device. You can
+ run the tests with command
+ ```
+ atest CtsThreadNetworkTestCases
+ ```
+- The integration test provides more quality guarantee of how the Thread
+ mainline code works on your device. You can run the tests with command
+ ```
+ atest ThreadNetworkIntegrationTests
+ ```
+
+You can also find more instructions of how to run VTS/CTS/MTS tests with those
+released test suites:
+
+- https://source.android.com/docs/core/tests/vts
+- https://source.android.com/docs/compatibility/cts/run
+- https://docs.partner.android.com/mainline/test/mts (you need to be a partner to access this link)
+
+### Test with the Thread demo app
+
+Similar to the Cuttlefish device, you can add the Thread demo app to your system image:
+
+```
+# ThreadNetworkDemoApp for testing
+PRODUCT_PACKAGES_DEBUG += ThreadNetworkDemoApp
+```
+
+Note that you should add it to only the debug / eng variant (for example,
+`PRODUCT_PACKAGES_DEBUG`) given this is not supposed to be included in user
+build for end consumers.
diff --git a/thread/docs/demoapp-screenshot.png b/thread/docs/demoapp-screenshot.png
new file mode 100644
index 0000000..fa7f079
--- /dev/null
+++ b/thread/docs/demoapp-screenshot.png
Binary files differ