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"));
+    }
+}