Merge "Add daily reconciliation" into main
diff --git a/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java b/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
new file mode 100644
index 0000000..473fbfb
--- /dev/null
+++ b/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
@@ -0,0 +1,184 @@
+/*
+ * 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.system.virtualmachine;
+
+import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
+import android.util.Log;
+
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A job scheduler service responsible for triggering the Virtualization Service reconciliation
+ * process when scheduled. The job is scheduled to run once per day while idle and charging.
+ *
+ * <p>The reconciliation process ensures that Secretkeeper secrets belonging to apps or users that
+ * have been removed get deleted.
+ *
+ * @hide
+ */
+public class SecretkeeperJobService extends JobService {
+    private static final String TAG = SecretkeeperJobService.class.getName();
+    private static final String JOBSCHEDULER_NAMESPACE = "VirtualizationSystemService";
+    private static final int JOB_ID = 1;
+    private static final AtomicReference<SecretkeeperJob> sJob = new AtomicReference<>();
+
+    static void scheduleJob(JobScheduler scheduler) {
+        try {
+            ComponentName serviceName =
+                    new ComponentName("android", SecretkeeperJobService.class.getName());
+            scheduler = scheduler.forNamespace(JOBSCHEDULER_NAMESPACE);
+            if (scheduler.schedule(
+                            new JobInfo.Builder(JOB_ID, serviceName)
+                                    // We consume CPU and power
+                                    .setRequiresDeviceIdle(true)
+                                    .setRequiresCharging(true)
+                                    .setPeriodic(24 * 60 * 60 * 1000L)
+                                    .build())
+                    != JobScheduler.RESULT_SUCCESS) {
+                Log.e(TAG, "Unable to schedule job");
+                return;
+            }
+            Log.i(TAG, "Scheduled job");
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to schedule job", e);
+        }
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Log.i(TAG, "Starting job");
+
+        SecretkeeperJob job = new SecretkeeperJob(getPackageManager());
+        sJob.set(job);
+
+        new Thread("SecretkeeperJob") {
+            @Override
+            public void run() {
+                try {
+                    job.run();
+                    Log.i(TAG, "Job finished");
+                } catch (Exception e) {
+                    Log.e(TAG, "Job failed", e);
+                }
+                sJob.set(null);
+                // We don't reschedule on error, we will try again the next day anyway.
+                jobFinished(params, /*wantReschedule=*/ false);
+            }
+        }.start();
+
+        return true; // Job is running in the background
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        Log.i(TAG, "Stopping job");
+        SecretkeeperJob job = sJob.getAndSet(null);
+        if (job != null) {
+            job.stop();
+        }
+        return false; // Idle jobs get rescheduled anyway
+    }
+
+    private static class SecretkeeperJob {
+        private final UserManagerInternal mUserManager =
+                LocalServices.getService(UserManagerInternal.class);
+        private volatile boolean mStopRequested = false;
+        private PackageManager mPackageManager;
+
+        public SecretkeeperJob(PackageManager packageManager) {
+            mPackageManager = packageManager;
+        }
+
+        public void run() throws RemoteException {
+            IVirtualizationMaintenance maintenance =
+                    VirtualizationSystemService.connectToMaintenanceService();
+            maintenance.performReconciliation(new Callback());
+        }
+
+        public void stop() {
+            mStopRequested = true;
+        }
+
+        class Callback extends IVirtualizationReconciliationCallback.Stub {
+            @Override
+            public boolean[] doUsersExist(int[] userIds) {
+                checkForStop();
+                int[] currentUsers = mUserManager.getUserIds();
+                boolean[] results = new boolean[userIds.length];
+                for (int i = 0; i < userIds.length; i++) {
+                    // The total number of users is likely to be small, so no need to make this
+                    // better than O(N).
+                    for (int user : currentUsers) {
+                        if (user == userIds[i]) {
+                            results[i] = true;
+                            break;
+                        }
+                    }
+                }
+                return results;
+            }
+
+            @Override
+            public boolean[] doAppsExist(int userId, int[] appIds) {
+                checkForStop();
+
+                // If an app has been uninstalled but its data is still present we want to include
+                // it, since that might include a VM which will be used in the future.
+                ApplicationInfoFlags flags = ApplicationInfoFlags.of(MATCH_UNINSTALLED_PACKAGES);
+                List<ApplicationInfo> appInfos =
+                        mPackageManager.getInstalledApplicationsAsUser(flags, userId);
+                int[] currentAppIds = new int[appInfos.size()];
+                for (int i = 0; i < appInfos.size(); i++) {
+                    currentAppIds[i] = UserHandle.getAppId(appInfos.get(i).uid);
+                }
+                Arrays.sort(currentAppIds);
+
+                boolean[] results = new boolean[appIds.length];
+                for (int i = 0; i < appIds.length; i++) {
+                    results[i] = Arrays.binarySearch(currentAppIds, appIds[i]) >= 0;
+                }
+
+                return results;
+            }
+
+            private void checkForStop() {
+                if (mStopRequested) {
+                    throw new ServiceSpecificException(ERROR_STOP_REQUESTED, "Stop requested");
+                }
+            }
+        }
+    }
+}
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 3f973b4..2461755 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -16,6 +16,7 @@
 
 package com.android.system.virtualmachine;
 
+import android.app.job.JobScheduler;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -58,6 +59,8 @@
 
         mHandler = BackgroundThread.getHandler();
         new Receiver().registerForBroadcasts();
+
+        SecretkeeperJobService.scheduleJob(getContext().getSystemService(JobScheduler.class));
     }
 
     private void notifyAppRemoved(int uid) {
@@ -78,7 +81,7 @@
         }
     }
 
-    private static IVirtualizationMaintenance connectToMaintenanceService() {
+    static IVirtualizationMaintenance connectToMaintenanceService() {
         IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
         IVirtualizationMaintenance maintenance =
                 IVirtualizationMaintenance.Stub.asInterface(binder);
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
index 76d7309..08d61c1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
@@ -16,6 +16,8 @@
 
 package android.system.virtualizationmaintenance;
 
+import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
+
 interface IVirtualizationMaintenance {
     /**
      * Notification that an app has been permanently removed, to allow related global state to
@@ -32,5 +34,13 @@
      */
     void userRemoved(int userId);
 
-    // TODO(b/294177871): Something for daily reconciliation
+    /*
+     * Requests virtualization service to perform reconciliation of Secretkeeper secrets.
+     * Secrets belonging to apps or users that no longer exist should be deleted.
+     * The supplied callback allows for querying of existence.
+     * This method should return on successful completion of the reconciliation process.
+     * It should throw an exception if there is any failure, or if any of the callback
+     * functions return {@code ERROR_STOP_REQUESTED}.
+     */
+    void performReconciliation(IVirtualizationReconciliationCallback callback);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
new file mode 100644
index 0000000..6466aa2
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright 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 android.system.virtualizationmaintenance;
+
+/*
+ * Callback interface provided when reconciliation is performed to allow verifying whether users
+ * and apps currently exist.
+ */
+interface IVirtualizationReconciliationCallback {
+    /*
+     * Service-specific error code indicating that the job scheduler has requested that we
+     * stop
+     */
+    const int ERROR_STOP_REQUESTED = 1;
+
+    /*
+     * Determine whether users with selected IDs currently exist. The result is an array of booleans
+     * which indicate whether the corresponding entry in the {@code userIds} array is a valid
+     * user ID.
+     */
+    boolean[] doUsersExist(in int[] userIds);
+
+    /*
+     * Determine whether apps with selected app IDs currently exist for a specific user.
+     * The result is an array of booleans which indicate whether the corresponding entry in the
+     * {@code appIds} array is a current app ID for the user.
+     */
+    boolean[] doAppsExist(int userId, in int[] appIds);
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index c0024f1..bbfb220 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -51,7 +51,10 @@
 use std::sync::{Arc, Mutex, Weak};
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use virtualizationcommon::Certificate::Certificate;
-use virtualizationmaintenance::IVirtualizationMaintenance::IVirtualizationMaintenance;
+use virtualizationmaintenance::{
+    IVirtualizationMaintenance::IVirtualizationMaintenance,
+    IVirtualizationReconciliationCallback::IVirtualizationReconciliationCallback,
+};
 use virtualizationservice::{
     AssignableDevice::AssignableDevice, VirtualMachineDebugInfo::VirtualMachineDebugInfo,
 };
@@ -427,6 +430,14 @@
         }
         Ok(())
     }
+
+    fn performReconciliation(
+        &self,
+        _callback: &Strong<dyn IVirtualizationReconciliationCallback>,
+    ) -> binder::Result<()> {
+        Err(anyhow!("performReconciliation not supported"))
+            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+    }
 }
 
 // KEEP IN SYNC WITH assignable_devices.xsd