Implement killswitch mechanism for Android CT

Enabling the killswitch will scheduele the CT job and initialize the downloader.

Disabling the killswitch will remove the scheduled job, unregister the downloader and delete the compatibility version directory.

Also, remove the CertificateTransparencyFlagsListener as it is deprecated.

Bug: 384688420
Change-Id: I23d2201e22f305284ac47f521258f856b8a9efc2
Flag: com.android.net.ct.flags.certificate_transparency_service
Test: atest 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
index 79123ee..4584212 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -61,6 +61,8 @@
     private final CertificateTransparencyInstaller mInstaller;
     private final CertificateTransparencyLogger mLogger;
 
+    private boolean started = false;
+
     CertificateTransparencyDownloader(
             Context context,
             DataStore dataStore,
@@ -76,15 +78,32 @@
         mLogger = logger;
     }
 
-    void initialize() {
+    void start() {
+        if (started) {
+            return;
+        }
         mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
-
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
-        mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
+        mContext.registerReceiver(
+                this,
+                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+                Context.RECEIVER_EXPORTED);
+        started = true;
 
         if (Config.DEBUG) {
-            Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+            Log.d(TAG, "CertificateTransparencyDownloader started.");
+        }
+    }
+
+    void stop() {
+        if (!started) {
+            return;
+        }
+        mContext.unregisterReceiver(this);
+        mInstaller.removeCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+        started = false;
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyDownloader stopped.");
         }
     }
 
@@ -216,8 +235,9 @@
 
         String version = null;
         try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
-            version = new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
-                    .getString("version");
+            version =
+                    new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
+                            .getString("version");
         } catch (JSONException | IOException e) {
             Log.e(TAG, "Could not extract version from log list", e);
             return;
@@ -242,7 +262,7 @@
                 mLogger.logCTLogListUpdateFailedEvent(
                         CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
                         mDataStore.getPropertyInt(
-                            Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+                                Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
             }
         }
     }
@@ -251,30 +271,28 @@
         Log.e(TAG, "Download failed with " + status);
 
         if (updateFailureCount()) {
-            int failureCount = mDataStore.getPropertyInt(
-                    Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+            int failureCount =
+                    mDataStore.getPropertyInt(
+                            Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
 
             // HTTP Error
             if (400 <= status.reason() && status.reason() <= 600) {
                 mLogger.logCTLogListUpdateFailedEvent(
                         CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR,
                         failureCount,
-                        status.reason()
-                );
+                        status.reason());
             } else {
                 // TODO(b/384935059): handle blocked domain logging
                 // TODO(b/384936292): add additionalchecks for pending wifi status
                 mLogger.logCTLogListUpdateFailedEvent(
-                        downloadStatusToFailureReason(status.reason()),
-                        failureCount
-                );
+                        downloadStatusToFailureReason(status.reason()), failureCount);
             }
         }
     }
 
     /** Converts DownloadStatus reason into failure reason to log. */
     private int downloadStatusToFailureReason(int downloadStatusReason) {
-        switch(downloadStatusReason) {
+        switch (downloadStatusReason) {
             case DownloadManager.PAUSED_WAITING_TO_RETRY:
             case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
@@ -300,8 +318,9 @@
      * @return whether the failure count exceeds the threshold and should be logged.
      */
     private boolean updateFailureCount() {
-        int failure_count = mDataStore.getPropertyInt(
-                Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+        int failure_count =
+                mDataStore.getPropertyInt(
+                        Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
         int new_failure_count = failure_count + 1;
 
         mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
@@ -309,8 +328,7 @@
 
         boolean shouldReport = new_failure_count >= Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD;
         if (shouldReport) {
-            Log.d(TAG,
-                    "Log list update failure count exceeds threshold: " + new_failure_count);
+            Log.d(TAG, "Log list update failure count exceeds threshold: " + new_failure_count);
         }
         return shouldReport;
     }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
deleted file mode 100644
index 3138ea7..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.os.Build;
-import android.provider.DeviceConfig;
-import android.provider.DeviceConfig.Properties;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.security.GeneralSecurityException;
-import java.util.concurrent.Executors;
-
-/** Listener class for the Certificate Transparency Phenotype flags. */
-@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
-
-    private static final String TAG = "CertificateTransparencyFlagsListener";
-
-    private final DataStore mDataStore;
-    private final SignatureVerifier mSignatureVerifier;
-    private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
-    CertificateTransparencyFlagsListener(
-            DataStore dataStore,
-            SignatureVerifier signatureVerifier,
-            CertificateTransparencyDownloader certificateTransparencyDownloader) {
-        mDataStore = dataStore;
-        mSignatureVerifier = signatureVerifier;
-        mCertificateTransparencyDownloader = certificateTransparencyDownloader;
-    }
-
-    void initialize() {
-        mDataStore.load();
-        mCertificateTransparencyDownloader.initialize();
-        DeviceConfig.addOnPropertiesChangedListener(
-                Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
-        if (Config.DEBUG) {
-            Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
-        }
-        // TODO: handle property changes triggering on boot before registering this listener.
-    }
-
-    @Override
-    public void onPropertiesChanged(Properties properties) {
-        if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
-            return;
-        }
-
-        String newPublicKey =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_PUBLIC_KEY,
-                        /* defaultValue= */ "");
-        String newVersion =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_VERSION,
-                        /* defaultValue= */ "");
-        String newContentUrl =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_CONTENT_URL,
-                        /* defaultValue= */ "");
-        String newMetadataUrl =
-                DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_METADATA_URL,
-                        /* defaultValue= */ "");
-        if (TextUtils.isEmpty(newPublicKey)
-                || TextUtils.isEmpty(newVersion)
-                || TextUtils.isEmpty(newContentUrl)
-                || TextUtils.isEmpty(newMetadataUrl)) {
-            return;
-        }
-
-        if (Config.DEBUG) {
-            Log.d(TAG, "newPublicKey=" + newPublicKey);
-            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;
-        }
-
-        try {
-            mSignatureVerifier.setPublicKey(newPublicKey);
-        } catch (GeneralSecurityException | IllegalArgumentException e) {
-            Log.e(TAG, "Error setting the public Key", e);
-            return;
-        }
-
-        // TODO: handle the case where there is already a pending download.
-
-        mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
-        mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
-        mDataStore.store();
-
-        if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
-            Log.e(TAG, "Metadata download not started.");
-        } else if (Config.DEBUG) {
-            Log.d(TAG, "Metadata download started successfully.");
-        }
-    }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index abede87..baca2e3 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -37,6 +37,7 @@
     private final DataStore mDataStore;
     private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
     private final AlarmManager mAlarmManager;
+    private final PendingIntent mPendingIntent;
 
     private boolean mDependenciesReady = false;
 
@@ -49,9 +50,15 @@
         mDataStore = dataStore;
         mCertificateTransparencyDownloader = certificateTransparencyDownloader;
         mAlarmManager = context.getSystemService(AlarmManager.class);
+        mPendingIntent =
+                PendingIntent.getBroadcast(
+                        mContext,
+                        /* requestCode= */ 0,
+                        new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+                        PendingIntent.FLAG_IMMUTABLE);
     }
 
-    void initialize() {
+    void schedule() {
         mContext.registerReceiver(
                 this,
                 new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
@@ -60,14 +67,21 @@
                 AlarmManager.ELAPSED_REALTIME,
                 SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
                 AlarmManager.INTERVAL_DAY,
-                PendingIntent.getBroadcast(
-                        mContext,
-                        0,
-                        new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
-                        PendingIntent.FLAG_IMMUTABLE));
+                mPendingIntent);
 
         if (Config.DEBUG) {
-            Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+            Log.d(TAG, "CertificateTransparencyJob scheduled.");
+        }
+    }
+
+    void cancel() {
+        mContext.unregisterReceiver(this);
+        mAlarmManager.cancel(mPendingIntent);
+        mCertificateTransparencyDownloader.stop();
+        mDependenciesReady = false;
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyJob canceled.");
         }
     }
 
@@ -82,7 +96,7 @@
         }
         if (!mDependenciesReady) {
             mDataStore.load();
-            mCertificateTransparencyDownloader.initialize();
+            mCertificateTransparencyDownloader.start();
             mDependenciesReady = true;
         }
 
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 2a27204..4569628 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -25,27 +25,29 @@
 import android.content.Context;
 import android.net.ct.ICertificateTransparencyManager;
 import android.os.Build;
+import android.util.Log;
 import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
 
 import com.android.server.SystemService;
+import java.util.concurrent.Executors;
 
 /** Implementation of the Certificate Transparency service. */
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
+        implements DeviceConfig.OnPropertiesChangedListener {
 
-    private final CertificateTransparencyFlagsListener mFlagsListener;
+    private static final String TAG = "CertificateTransparencyService";
+
     private final CertificateTransparencyJob mCertificateTransparencyJob;
 
+    private boolean started = false;
+
     /**
      * @return true if the CertificateTransparency service is enabled.
      */
     public static boolean enabled(Context context) {
-        return DeviceConfig.getBoolean(
-                        Config.NAMESPACE_NETWORK_SECURITY,
-                        Config.FLAG_SERVICE_ENABLED,
-                        /* defaultValue= */ true)
-                && certificateTransparencyService()
-                && certificateTransparencyConfiguration();
+        return certificateTransparencyService() && certificateTransparencyConfiguration();
     }
 
     /** Creates a new {@link CertificateTransparencyService} object. */
@@ -61,8 +63,6 @@
                         signatureVerifier,
                         new CertificateTransparencyInstaller(),
                         new CertificateTransparencyLogger());
-        mFlagsListener =
-                new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
         mCertificateTransparencyJob =
                 new CertificateTransparencyJob(context, dataStore, downloader);
     }
@@ -75,13 +75,50 @@
     public void onBootPhase(int phase) {
         switch (phase) {
             case SystemService.PHASE_BOOT_COMPLETED:
-                if (certificateTransparencyJob()) {
-                    mCertificateTransparencyJob.initialize();
-                } else {
-                    mFlagsListener.initialize();
-                }
+                DeviceConfig.addOnPropertiesChangedListener(
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Executors.newSingleThreadExecutor(),
+                        this);
+                onPropertiesChanged(
+                        new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
                 break;
             default:
         }
     }
+
+    @Override
+    public void onPropertiesChanged(Properties properties) {
+        if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+            return;
+        }
+
+        if (DeviceConfig.getBoolean(
+                Config.NAMESPACE_NETWORK_SECURITY,
+                Config.FLAG_SERVICE_ENABLED,
+                /* defaultValue= */ true)) {
+            startService();
+        } else {
+            stopService();
+        }
+    }
+
+    private void startService() {
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyService start");
+        }
+        if (!started) {
+            mCertificateTransparencyJob.schedule();
+            started = true;
+        }
+    }
+
+    private void stopService() {
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyService stop");
+        }
+        if (started) {
+            mCertificateTransparencyJob.cancel();
+            started = false;
+        }
+    }
 }