/* * 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. * *

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 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 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"); } } } } }