Schedule log list download on flag update

Add the CertificateTransparencyDownloader to download the data and
metadata needed by Certificate Transparency.

The download is triggered when either the content URL, the metadata URL,
or the log list flags change.

The version numbers and URLs are stored in an on-disk DataStore to make
sure the same files are not downloaded on repeated flag updates.

Flag: com.android.ct.flags.certificate_transparency_service
Bug: 319829948
Test: atest NetworkSecurityUnitTests
Change-Id: Ic30e2973fdce99ea861d13f7777128161dd4003e
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"));
+    }
+}