Merge "Give CtsHostsideNetworkTests helper apps a min_sdk_version." into main
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"));
+ }
+}