1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.system.virtualmachine;
18 
19 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
20 
21 import android.app.job.JobInfo;
22 import android.app.job.JobParameters;
23 import android.app.job.JobScheduler;
24 import android.app.job.JobService;
25 import android.content.ComponentName;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PackageManager.ApplicationInfoFlags;
29 import android.os.RemoteException;
30 import android.os.ServiceSpecificException;
31 import android.os.UserHandle;
32 import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
33 import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
34 import android.util.Log;
35 
36 import com.android.server.LocalServices;
37 import com.android.server.pm.UserManagerInternal;
38 
39 import java.util.Arrays;
40 import java.util.List;
41 import java.util.concurrent.atomic.AtomicReference;
42 
43 /**
44  * A job scheduler service responsible for triggering the Virtualization Service reconciliation
45  * process when scheduled. The job is scheduled to run once per day while idle and charging.
46  *
47  * <p>The reconciliation process ensures that Secretkeeper secrets belonging to apps or users that
48  * have been removed get deleted.
49  *
50  * @hide
51  */
52 public class SecretkeeperJobService extends JobService {
53     private static final String TAG = SecretkeeperJobService.class.getName();
54     private static final String JOBSCHEDULER_NAMESPACE = "VirtualizationSystemService";
55     private static final int JOB_ID = 1;
56     private static final AtomicReference<SecretkeeperJob> sJob = new AtomicReference<>();
57 
scheduleJob(JobScheduler scheduler)58     static void scheduleJob(JobScheduler scheduler) {
59         try {
60             ComponentName serviceName =
61                     new ComponentName("android", SecretkeeperJobService.class.getName());
62             scheduler = scheduler.forNamespace(JOBSCHEDULER_NAMESPACE);
63             if (scheduler.schedule(
64                             new JobInfo.Builder(JOB_ID, serviceName)
65                                     // We consume CPU and power
66                                     .setRequiresDeviceIdle(true)
67                                     .setRequiresCharging(true)
68                                     .setPeriodic(24 * 60 * 60 * 1000L)
69                                     .build())
70                     != JobScheduler.RESULT_SUCCESS) {
71                 Log.e(TAG, "Unable to schedule job");
72                 return;
73             }
74             Log.i(TAG, "Scheduled job");
75         } catch (Exception e) {
76             Log.e(TAG, "Failed to schedule job", e);
77         }
78     }
79 
80     @Override
onStartJob(JobParameters params)81     public boolean onStartJob(JobParameters params) {
82         Log.i(TAG, "Starting job");
83 
84         SecretkeeperJob job = new SecretkeeperJob(getPackageManager());
85         sJob.set(job);
86 
87         new Thread("SecretkeeperJob") {
88             @Override
89             public void run() {
90                 try {
91                     job.run();
92                     Log.i(TAG, "Job finished");
93                 } catch (Exception e) {
94                     Log.e(TAG, "Job failed", e);
95                 }
96                 sJob.set(null);
97                 // We don't reschedule on error, we will try again the next day anyway.
98                 jobFinished(params, /*wantReschedule=*/ false);
99             }
100         }.start();
101 
102         return true; // Job is running in the background
103     }
104 
105     @Override
onStopJob(JobParameters params)106     public boolean onStopJob(JobParameters params) {
107         Log.i(TAG, "Stopping job");
108         SecretkeeperJob job = sJob.getAndSet(null);
109         if (job != null) {
110             job.stop();
111         }
112         return false; // Idle jobs get rescheduled anyway
113     }
114 
115     private static class SecretkeeperJob {
116         private final UserManagerInternal mUserManager =
117                 LocalServices.getService(UserManagerInternal.class);
118         private volatile boolean mStopRequested = false;
119         private PackageManager mPackageManager;
120 
SecretkeeperJob(PackageManager packageManager)121         public SecretkeeperJob(PackageManager packageManager) {
122             mPackageManager = packageManager;
123         }
124 
run()125         public void run() throws RemoteException {
126             IVirtualizationMaintenance maintenance =
127                     VirtualizationSystemService.connectToMaintenanceService();
128             maintenance.performReconciliation(new Callback());
129         }
130 
stop()131         public void stop() {
132             mStopRequested = true;
133         }
134 
135         class Callback extends IVirtualizationReconciliationCallback.Stub {
136             @Override
doUsersExist(int[] userIds)137             public boolean[] doUsersExist(int[] userIds) {
138                 checkForStop();
139                 int[] currentUsers = mUserManager.getUserIds();
140                 boolean[] results = new boolean[userIds.length];
141                 for (int i = 0; i < userIds.length; i++) {
142                     // The total number of users is likely to be small, so no need to make this
143                     // better than O(N).
144                     for (int user : currentUsers) {
145                         if (user == userIds[i]) {
146                             results[i] = true;
147                             break;
148                         }
149                     }
150                 }
151                 return results;
152             }
153 
154             @Override
doAppsExist(int userId, int[] appIds)155             public boolean[] doAppsExist(int userId, int[] appIds) {
156                 checkForStop();
157 
158                 // If an app has been uninstalled but its data is still present we want to include
159                 // it, since that might include a VM which will be used in the future.
160                 ApplicationInfoFlags flags = ApplicationInfoFlags.of(MATCH_UNINSTALLED_PACKAGES);
161                 List<ApplicationInfo> appInfos =
162                         mPackageManager.getInstalledApplicationsAsUser(flags, userId);
163                 int[] currentAppIds = new int[appInfos.size()];
164                 for (int i = 0; i < appInfos.size(); i++) {
165                     currentAppIds[i] = UserHandle.getAppId(appInfos.get(i).uid);
166                 }
167                 Arrays.sort(currentAppIds);
168 
169                 boolean[] results = new boolean[appIds.length];
170                 for (int i = 0; i < appIds.length; i++) {
171                     results[i] = Arrays.binarySearch(currentAppIds, appIds[i]) >= 0;
172                 }
173 
174                 return results;
175             }
176 
checkForStop()177             private void checkForStop() {
178                 if (mStopRequested) {
179                     throw new ServiceSpecificException(ERROR_STOP_REQUESTED, "Stop requested");
180                 }
181             }
182         }
183     }
184 }
185