Add daily CertificateTransparencyJob

Following a conversation with the Experiments team (b/374692404), it
appears we will not be able to use device config flags to trigger the
download of CT log lists.

This CL is the first of a series that will allow us to move away from
flags and instead rely on a daily job execution. For scheduling we rely
on the Android AlarmManager.

I previously attempted to schedule the daily job using the Android
JobScheduler. However, those attempts were unsuccessful beacause the
Tethering apex is not on the system_server classpath, so the system
would crash when attempting to load the class to execute the job.

Flag: com.android.net.ct.flags.certificate_transparency_job
Bug: 374692404
Test: NetworkSecurityUnitTests
Change-Id: I6ce44dcea464cab20d2df73b065e35c0f76dcbab
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
index 6438ba4..4a83af4 100644
--- a/common/networksecurity_flags.aconfig
+++ b/common/networksecurity_flags.aconfig
@@ -8,3 +8,12 @@
     bug: "319829948"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "certificate_transparency_job"
+    is_exported: true
+    namespace: "network_security"
+    description: "Enable daily job service for certificate transparency instead of flags listener"
+    bug: "319829948"
+    is_fixed_read_only: true
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index f86d127..d53f007 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -88,12 +88,17 @@
     }
 
     void setPublicKey(String publicKey) throws GeneralSecurityException {
-        mPublicKey =
-                Optional.of(
-                        KeyFactory.getInstance("RSA")
-                                .generatePublic(
-                                        new X509EncodedKeySpec(
-                                                Base64.getDecoder().decode(publicKey))));
+        try {
+            mPublicKey =
+                    Optional.of(
+                            KeyFactory.getInstance("RSA")
+                                    .generatePublic(
+                                            new X509EncodedKeySpec(
+                                                    Base64.getDecoder().decode(publicKey))));
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Invalid public key Base64 encoding", e);
+            mPublicKey = Optional.empty();
+        }
     }
 
     @VisibleForTesting
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 0ae982d..93a7064 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -16,7 +16,6 @@
 package com.android.server.net.ct;
 
 import android.annotation.RequiresApi;
-import android.content.Context;
 import android.os.Build;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
@@ -35,10 +34,11 @@
     private final DataStore mDataStore;
     private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
 
-    CertificateTransparencyFlagsListener(Context context) {
-        mDataStore = new DataStore(Config.PREFERENCES_FILE);
-        mCertificateTransparencyDownloader =
-                new CertificateTransparencyDownloader(context, mDataStore);
+    CertificateTransparencyFlagsListener(
+            DataStore dataStore,
+            CertificateTransparencyDownloader certificateTransparencyDownloader) {
+        mDataStore = dataStore;
+        mCertificateTransparencyDownloader = certificateTransparencyDownloader;
     }
 
     void initialize() {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
new file mode 100644
index 0000000..6fbf0ba
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import android.annotation.RequiresApi;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.SystemClock;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import java.util.HashMap;
+
+/** Implementation of the Certificate Transparency job */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyJob extends BroadcastReceiver {
+
+    private static final String TAG = "CertificateTransparencyJob";
+
+    private static final String ACTION_JOB_START = "com.android.server.net.ct.action.JOB_START";
+
+    private final Context mContext;
+    private final DataStore mDataStore;
+    private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+    // TODO(b/374692404): remove dependency to flags.
+    private final CertificateTransparencyFlagsListener mFlagsListener;
+    private final AlarmManager mAlarmManager;
+
+    /** Creates a new {@link CertificateTransparencyJob} object. */
+    public CertificateTransparencyJob(
+            Context context,
+            DataStore dataStore,
+            CertificateTransparencyDownloader certificateTransparencyDownloader,
+            CertificateTransparencyFlagsListener flagsListener) {
+        mContext = context;
+        mFlagsListener = flagsListener;
+        mDataStore = dataStore;
+        mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+        mAlarmManager = context.getSystemService(AlarmManager.class);
+    }
+
+    void initialize() {
+        mDataStore.load();
+        mCertificateTransparencyDownloader.initialize();
+
+        mContext.registerReceiver(
+                this, new IntentFilter(ACTION_JOB_START), Context.RECEIVER_EXPORTED);
+        mAlarmManager.setInexactRepeating(
+                AlarmManager.ELAPSED_REALTIME,
+                SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
+                AlarmManager.INTERVAL_DAY,
+                PendingIntent.getBroadcast(
+                        mContext, 0, new Intent(ACTION_JOB_START), PendingIntent.FLAG_IMMUTABLE));
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (!ACTION_JOB_START.equals(intent.getAction())) {
+            Log.w(TAG, "Received unexpected broadcast with action " + intent);
+            return;
+        }
+        mFlagsListener.onPropertiesChanged(
+                new DeviceConfig.Properties(Config.NAMESPACE_NETWORK_SECURITY, new HashMap<>()));
+    }
+}
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 edf7c56..ac55e44 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -28,7 +28,10 @@
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
 
+    private final DataStore mDataStore;
+    private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
     private final CertificateTransparencyFlagsListener mFlagsListener;
+    private final CertificateTransparencyJob mCertificateTransparencyJob;
 
     /**
      * @return true if the CertificateTransparency service is enabled.
@@ -41,7 +44,15 @@
 
     /** Creates a new {@link CertificateTransparencyService} object. */
     public CertificateTransparencyService(Context context) {
-        mFlagsListener = new CertificateTransparencyFlagsListener(context);
+        mDataStore = new DataStore(Config.PREFERENCES_FILE);
+        mCertificateTransparencyDownloader =
+                new CertificateTransparencyDownloader(context, mDataStore);
+        mFlagsListener =
+                new CertificateTransparencyFlagsListener(
+                        mDataStore, mCertificateTransparencyDownloader);
+        mCertificateTransparencyJob =
+                new CertificateTransparencyJob(
+                        context, mDataStore, mCertificateTransparencyDownloader, mFlagsListener);
     }
 
     /**
@@ -53,7 +64,11 @@
 
         switch (phase) {
             case SystemService.PHASE_BOOT_COMPLETED:
-                mFlagsListener.initialize();
+                if (Flags.certificateTransparencyJob()) {
+                    mCertificateTransparencyJob.initialize();
+                } else {
+                    mFlagsListener.initialize();
+                }
                 break;
             default:
         }