Merge "Implement killswitch mechanism for Android CT" into main
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 502c449..002ad9a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -64,6 +64,8 @@
     private final CertificateTransparencyInstaller mInstaller;
     private final CertificateTransparencyLogger mLogger;
 
+    private boolean started = false;
+
     CertificateTransparencyDownloader(
             Context context,
             DataStore dataStore,
@@ -79,15 +81,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.");
         }
     }
 
@@ -246,8 +265,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;
@@ -272,7 +292,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));
             }
         }
     }
@@ -281,30 +301,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;
@@ -330,8 +348,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);
@@ -339,8 +358,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;
+        }
+    }
 }