Cancel periodic provisioner on unsupported devices

If a device is either not configured for RKP (not hostname) or it
has no IRemotelyProvisionedComponent HALs, then we should stop trying
to run the background provisioning job.

Bug: 272304888
Test: RkpdAppIntegrationTests RkpdAppUnitTests RkpdAppStressTests
Change-Id: I3555a40b0f0b5e4f65212acda380b5cd1df041df
diff --git a/app/src/com/android/rkpdapp/BootReceiver.java b/app/src/com/android/rkpdapp/BootReceiver.java
index 6d610cc..1048572 100644
--- a/app/src/com/android/rkpdapp/BootReceiver.java
+++ b/app/src/com/android/rkpdapp/BootReceiver.java
@@ -57,7 +57,7 @@
                         .build();
         WorkManager
                 .getInstance(context)
-                .enqueueUniquePeriodicWork("ProvisioningJob",
+                .enqueueUniquePeriodicWork(PeriodicProvisioner.UNIQUE_WORK_NAME,
                                        ExistingPeriodicWorkPolicy.UPDATE, // Replace on reboot.
                                        workRequest);
 
diff --git a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
index 6097906..40c26c9 100644
--- a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.work.WorkManager;
 import androidx.work.Worker;
 import androidx.work.WorkerParameters;
 
@@ -31,6 +32,7 @@
 import com.android.rkpdapp.interfaces.ServerInterface;
 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
+import com.android.rkpdapp.utils.Settings;
 
 import java.time.Instant;
 
@@ -42,6 +44,7 @@
  * drive that process.
  */
 public class PeriodicProvisioner extends Worker {
+    public static final String UNIQUE_WORK_NAME = "ProvisioningJob";
     private static final String TAG = "RkpdPeriodicProvisioner";
     private final Context mContext;
     private final ProvisionedKeyDao mKeyDao;
@@ -58,6 +61,20 @@
     @Override
     public Result doWork() {
         Log.i(TAG, "Waking up; checking provisioning state.");
+
+        SystemInterface[] irpcs = ServiceManagerInterface.getAllInstances();
+        if (irpcs.length == 0) {
+            Log.i(TAG, "Stopping periodic provisioner: there are no IRPC HALs");
+            WorkManager.getInstance(mContext).cancelWorkById(getId());
+            return Result.success();
+        }
+
+        if (Settings.getDefaultUrl().isEmpty()) {
+            Log.i(TAG, "Stopping periodic provisioner: system has no configured server endpoint");
+            WorkManager.getInstance(mContext).cancelWorkById(getId());
+            return Result.success();
+        }
+
         try (ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(
                 mContext)) {
             // Clean up the expired keys
@@ -82,8 +99,6 @@
                 return Result.success();
             }
 
-            // Figure out each of the IRPCs and get SystemInterface instance for each.
-            SystemInterface[] irpcs = ServiceManagerInterface.getAllInstances();
             Log.i(TAG, "Total services found implementing IRPC: " + irpcs.length);
             Provisioner provisioner = new Provisioner(mContext, mKeyDao);
             Result result = Result.success();
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java b/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
index 8d376a8..9d7c584 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
@@ -30,9 +30,15 @@
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.work.Configuration;
 import androidx.work.ListenableWorker;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+import androidx.work.testing.SynchronousExecutor;
 import androidx.work.testing.TestWorkerBuilder;
+import androidx.work.testing.WorkManagerTestInitHelper;
 
+import com.android.rkpdapp.BootReceiver;
 import com.android.rkpdapp.database.ProvisionedKey;
 import com.android.rkpdapp.database.ProvisionedKeyDao;
 import com.android.rkpdapp.database.RkpKey;
@@ -41,6 +47,7 @@
 import com.android.rkpdapp.interfaces.SystemInterface;
 import com.android.rkpdapp.provisioner.PeriodicProvisioner;
 import com.android.rkpdapp.testutil.FakeRkpServer;
+import com.android.rkpdapp.testutil.SystemPropertySetter;
 import com.android.rkpdapp.utils.Settings;
 
 import org.junit.After;
@@ -51,6 +58,7 @@
 import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executors;
 
 import co.nstant.in.cbor.CborException;
@@ -72,6 +80,12 @@
                 mContext,
                 PeriodicProvisioner.class,
                 Executors.newSingleThreadExecutor()).build();
+
+        Configuration config = new Configuration.Builder()
+                .setExecutor(new SynchronousExecutor())
+                .build();
+        WorkManagerTestInitHelper.initializeTestWorkManager(mContext, config);
+
     }
 
     @After
@@ -81,16 +95,54 @@
         Settings.clearPreferences(mContext);
     }
 
+    private WorkInfo getProvisionerWorkInfo() throws ExecutionException, InterruptedException {
+        WorkManager workManager = WorkManager.getInstance(mContext);
+        List<WorkInfo> infos = workManager.getWorkInfosForUniqueWork(
+                PeriodicProvisioner.UNIQUE_WORK_NAME).get();
+        assertThat(infos.size()).isEqualTo(1);
+        return infos.get(0);
+    }
+
     @Test
-    public void provisionNoop() throws Exception {
-        try (FakeRkpServer fakeRkpServer = new FakeRkpServer(
-                FakeRkpServer.Response.FETCH_EEK_OK,
-                // return error here, because signCerts should never be called
-                FakeRkpServer.Response.INTERNAL_ERROR)) {
-            saveUrlInSettings(fakeRkpServer);
-            ServiceManagerInterface.setInstances(new SystemInterface[0]);
-            assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
+    public void provisionWithNoHals() throws Exception {
+        // setup work with boot receiver
+        new BootReceiver().onReceive(mContext, null);
+
+        WorkInfo worker = getProvisionerWorkInfo();
+        assertThat(worker.getState()).isEqualTo(WorkInfo.State.ENQUEUED);
+        assertThat(worker.getRunAttemptCount()).isEqualTo(0);
+
+        ServiceManagerInterface.setInstances(new SystemInterface[0]);
+        WorkManagerTestInitHelper.getTestDriver(mContext).setAllConstraintsMet(worker.getId());
+
+        // the worker should uninstall itself once it realizes it's not needed on this system
+        worker = getProvisionerWorkInfo();
+        assertThat(worker.getState()).isEqualTo(WorkInfo.State.CANCELLED);
+        assertThat(worker.getRunAttemptCount()).isEqualTo(1);
+
+        // verify the worker doesn't run again
+        WorkManagerTestInitHelper.getTestDriver(mContext).setAllConstraintsMet(worker.getId());
+        worker = getProvisionerWorkInfo();
+        assertThat(worker.getState()).isEqualTo(WorkInfo.State.CANCELLED);
+        assertThat(worker.getRunAttemptCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void provisionWithNoHostName() throws Exception {
+        // setup work with boot receiver
+        new BootReceiver().onReceive(mContext, null);
+
+        try (SystemPropertySetter ignored = SystemPropertySetter.setHostname("")) {
+            SystemInterface mockHal = mock(SystemInterface.class);
+            ServiceManagerInterface.setInstances(new SystemInterface[]{mockHal});
+
+            WorkInfo worker = getProvisionerWorkInfo();
+            WorkManagerTestInitHelper.getTestDriver(mContext).setAllConstraintsMet(worker.getId());
         }
+
+        WorkInfo worker = getProvisionerWorkInfo();
+        assertThat(worker.getState()).isEqualTo(WorkInfo.State.CANCELLED);
+        assertThat(worker.getRunAttemptCount()).isEqualTo(1);
     }
 
     @Test
@@ -161,7 +213,10 @@
                 FakeRkpServer.Response.FETCH_EEK_OK,
                 FakeRkpServer.Response.SIGN_CERTS_OK_VALID_CBOR)) {
             saveUrlInSettings(fakeRkpServer);
-            ServiceManagerInterface.setInstances(new SystemInterface[]{});
+            SystemInterface mockHal = mock(SystemInterface.class);
+            doReturn("test-irpc").when(mockHal).getServiceName();
+            doReturn(new byte[1]).when(mockHal).generateCsr(any(), any(), any());
+            ServiceManagerInterface.setInstances(new SystemInterface[]{mockHal});
             assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
         }