1 /*
2  * Copyright (C) 2022 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.server.pm;
18 
19 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
20 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
21 
22 import android.annotation.WorkerThread;
23 import android.app.ActivityManager;
24 import android.app.ActivityManager.RunningAppProcessInfo;
25 import android.app.ActivityThread;
26 import android.app.job.JobInfo;
27 import android.app.job.JobParameters;
28 import android.app.job.JobScheduler;
29 import android.app.job.JobService;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.pm.PackageInstaller.InstallConstraints;
33 import android.content.pm.PackageInstaller.InstallConstraintsResult;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.RemoteException;
37 import android.os.SystemClock;
38 import android.os.SystemProperties;
39 import android.text.format.DateUtils;
40 import android.util.Slog;
41 
42 import com.android.internal.util.IndentingPrintWriter;
43 import com.android.internal.util.Preconditions;
44 
45 import java.util.ArrayDeque;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.concurrent.CompletableFuture;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * A helper class to coordinate install flow for sessions with install constraints.
53  * These sessions will be pending and wait until the constraints are satisfied to
54  * resume installation.
55  */
56 public class GentleUpdateHelper {
57     private static final String TAG = "GentleUpdateHelper";
58     private static final int JOB_ID = 235306967; // bug id
59     // The timeout used to determine whether the device is idle or not.
60     private static final long PENDING_CHECK_MILLIS = TimeUnit.SECONDS.toMillis(10);
61 
62     /**
63      * A wrapper class used by JobScheduler to schedule jobs.
64      */
65     public static class Service extends JobService {
66         @Override
onStartJob(JobParameters params)67         public boolean onStartJob(JobParameters params) {
68             try {
69                 var pis = (PackageInstallerService) ActivityThread.getPackageManager()
70                         .getPackageInstaller();
71                 var helper = pis.getGentleUpdateHelper();
72                 helper.mHandler.post(helper::runIdleJob);
73             } catch (Exception e) {
74                 Slog.e(TAG, "Failed to get PackageInstallerService", e);
75             }
76             return false;
77         }
78 
79         @Override
onStopJob(JobParameters params)80         public boolean onStopJob(JobParameters params) {
81             return false;
82         }
83     }
84 
85     private static class PendingInstallConstraintsCheck {
86         public final List<String> packageNames;
87         public final InstallConstraints constraints;
88         public final CompletableFuture<InstallConstraintsResult> future;
89         private final long mFinishTime;
90 
91         /**
92          * Note {@code timeoutMillis} will be clamped to 0 ~ one week to avoid overflow.
93          */
PendingInstallConstraintsCheck(List<String> packageNames, InstallConstraints constraints, CompletableFuture<InstallConstraintsResult> future, long timeoutMillis)94         PendingInstallConstraintsCheck(List<String> packageNames,
95                 InstallConstraints constraints,
96                 CompletableFuture<InstallConstraintsResult> future,
97                 long timeoutMillis) {
98             this.packageNames = packageNames;
99             this.constraints = constraints;
100             this.future = future;
101 
102             timeoutMillis = Math.max(0, Math.min(DateUtils.WEEK_IN_MILLIS, timeoutMillis));
103             mFinishTime = SystemClock.elapsedRealtime() + timeoutMillis;
104         }
isTimedOut()105         public boolean isTimedOut() {
106             return SystemClock.elapsedRealtime() >= mFinishTime;
107         }
108         /**
109          * The remaining time before this pending check is timed out.
110          */
getRemainingTimeMillis()111         public long getRemainingTimeMillis() {
112             long timeout = mFinishTime - SystemClock.elapsedRealtime();
113             return Math.max(timeout, 0);
114         }
115 
dump(IndentingPrintWriter pw)116         void dump(IndentingPrintWriter pw) {
117             pw.printPair("packageNames", packageNames);
118             pw.println();
119             pw.printPair("finishTime", mFinishTime);
120             pw.println();
121             pw.printPair("constraints notInCallRequired", constraints.isNotInCallRequired());
122             pw.println();
123             pw.printPair("constraints deviceIdleRequired", constraints.isDeviceIdleRequired());
124             pw.println();
125             pw.printPair("constraints appNotForegroundRequired",
126                     constraints.isAppNotForegroundRequired());
127             pw.println();
128             pw.printPair("constraints appNotInteractingRequired",
129                     constraints.isAppNotInteractingRequired());
130             pw.println();
131             pw.printPair("constraints appNotTopVisibleRequired",
132                     constraints.isAppNotTopVisibleRequired());
133         }
134     }
135 
136     private final Context mContext;
137     private final Handler mHandler;
138     private final AppStateHelper mAppStateHelper;
139     // Worker thread only
140     private final ArrayDeque<PendingInstallConstraintsCheck> mPendingChecks = new ArrayDeque<>();
141     private final ArrayList<CompletableFuture<Boolean>> mPendingIdleFutures = new ArrayList<>();
142     private boolean mHasPendingIdleJob;
143 
GentleUpdateHelper(Context context, Looper looper, AppStateHelper appStateHelper)144     GentleUpdateHelper(Context context, Looper looper, AppStateHelper appStateHelper) {
145         mContext = context;
146         mHandler = new Handler(looper);
147         mAppStateHelper = appStateHelper;
148     }
149 
systemReady()150     void systemReady() {
151         var am = mContext.getSystemService(ActivityManager.class);
152         // Monitor top-visible apps
153         am.addOnUidImportanceListener(this::onUidImportance, IMPORTANCE_FOREGROUND);
154         // Monitor foreground apps
155         am.addOnUidImportanceListener(this::onUidImportance, IMPORTANCE_FOREGROUND_SERVICE);
156     }
157 
158     /**
159      * Checks if install constraints are satisfied for the given packages.
160      */
checkInstallConstraints( List<String> packageNames, InstallConstraints constraints, long timeoutMillis)161     CompletableFuture<InstallConstraintsResult> checkInstallConstraints(
162             List<String> packageNames, InstallConstraints constraints,
163             long timeoutMillis) {
164         var resultFuture = new CompletableFuture<InstallConstraintsResult>();
165         mHandler.post(() -> {
166             var pendingCheck = new PendingInstallConstraintsCheck(
167                     packageNames, constraints, resultFuture, timeoutMillis);
168             var deviceIdleFuture = constraints.isDeviceIdleRequired()
169                     ? checkDeviceIdle() : CompletableFuture.completedFuture(false);
170             deviceIdleFuture.thenAccept(isIdle -> {
171                 Preconditions.checkState(mHandler.getLooper().isCurrentThread());
172                 if (!processPendingCheck(pendingCheck, isIdle)) {
173                     // Not resolved. Schedule a job for re-check
174                     mPendingChecks.add(pendingCheck);
175                     scheduleIdleJob();
176                     // Ensure the pending check is resolved after timeout, no matter constraints
177                     // satisfied or not.
178                     mHandler.postDelayed(() -> processPendingCheck(
179                             pendingCheck, false), pendingCheck.getRemainingTimeMillis());
180                 }
181             });
182         });
183         return resultFuture;
184     }
185 
186     /**
187      * Checks if the device is idle or not.
188      * @return A future resolved to {@code true} if the device is idle, or {@code false} if not.
189      */
190     @WorkerThread
checkDeviceIdle()191     private CompletableFuture<Boolean> checkDeviceIdle() {
192         // JobScheduler doesn't provide queries about whether the device is idle.
193         // We schedule 2 tasks here and the task which resolves
194         // the future first will determine whether the device is idle or not.
195         var future = new CompletableFuture<Boolean>();
196         mPendingIdleFutures.add(future);
197         scheduleIdleJob();
198         mHandler.postDelayed(() -> future.complete(false), PENDING_CHECK_MILLIS);
199         return future;
200     }
201 
202     @WorkerThread
scheduleIdleJob()203     private void scheduleIdleJob() {
204         // Simulate idle jobs during test. Otherwise we need to wait for
205         // more than 30 mins for JS to trigger the job.
206         boolean isIdle = SystemProperties.getBoolean("debug.pm.gentle_update_test.is_idle", false);
207         if (isIdle) {
208             mHandler.post(this::runIdleJob);
209             return;
210         }
211 
212         if (mHasPendingIdleJob) {
213             // No need to schedule the job again
214             return;
215         }
216         mHasPendingIdleJob = true;
217         var componentName = new ComponentName(
218                 mContext.getPackageName(), GentleUpdateHelper.Service.class.getName());
219         var jobInfo = new JobInfo.Builder(JOB_ID, componentName)
220                 .setRequiresDeviceIdle(true)
221                 .build();
222         var jobScheduler = mContext.getSystemService(JobScheduler.class);
223         jobScheduler.schedule(jobInfo);
224     }
225 
226     @WorkerThread
runIdleJob()227     private void runIdleJob() {
228         mHasPendingIdleJob = false;
229         processPendingChecksInIdle();
230 
231         for (var f : mPendingIdleFutures) {
232             f.complete(true);
233         }
234         mPendingIdleFutures.clear();
235     }
236 
237     @WorkerThread
areConstraintsSatisfied(List<String> packageNames, InstallConstraints constraints, boolean isIdle)238     private boolean areConstraintsSatisfied(List<String> packageNames,
239             InstallConstraints constraints, boolean isIdle) {
240         return (!constraints.isDeviceIdleRequired() || isIdle)
241                 && (!constraints.isAppNotForegroundRequired()
242                 || !mAppStateHelper.hasForegroundApp(packageNames))
243                 && (!constraints.isAppNotInteractingRequired()
244                 || !mAppStateHelper.hasInteractingApp(packageNames))
245                 && (!constraints.isAppNotTopVisibleRequired()
246                 || !mAppStateHelper.hasTopVisibleApp(packageNames))
247                 && (!constraints.isNotInCallRequired()
248                 || !mAppStateHelper.isInCall());
249     }
250 
251     @WorkerThread
processPendingCheck( PendingInstallConstraintsCheck pendingCheck, boolean isIdle)252     private boolean processPendingCheck(
253             PendingInstallConstraintsCheck pendingCheck, boolean isIdle) {
254         var future = pendingCheck.future;
255         if (future.isDone()) {
256             return true;
257         }
258         var constraints = pendingCheck.constraints;
259         var packageNames = mAppStateHelper.getDependencyPackages(pendingCheck.packageNames);
260         var satisfied = areConstraintsSatisfied(packageNames, constraints, isIdle);
261         if (satisfied || pendingCheck.isTimedOut()) {
262             future.complete(new InstallConstraintsResult((satisfied)));
263             return true;
264         }
265         return false;
266     }
267 
268     @WorkerThread
processPendingChecksInIdle()269     private void processPendingChecksInIdle() {
270         int size = mPendingChecks.size();
271         for (int i = 0; i < size; ++i) {
272             var pendingCheck = mPendingChecks.remove();
273             if (!processPendingCheck(pendingCheck, true)) {
274                 // Not resolved. Put it back in the queue.
275                 mPendingChecks.add(pendingCheck);
276             }
277         }
278         if (!mPendingChecks.isEmpty()) {
279             // Schedule a job for remaining pending checks
280             scheduleIdleJob();
281         }
282     }
283 
284     @WorkerThread
onUidImportance(String packageName, @RunningAppProcessInfo.Importance int importance)285     private void onUidImportance(String packageName,
286             @RunningAppProcessInfo.Importance int importance) {
287         int size = mPendingChecks.size();
288         for (int i = 0; i < size; ++i) {
289             var pendingCheck = mPendingChecks.remove();
290             var dependencyPackages =
291                     mAppStateHelper.getDependencyPackages(pendingCheck.packageNames);
292             if (!dependencyPackages.contains(packageName)
293                     || !processPendingCheck(pendingCheck, false)) {
294                 mPendingChecks.add(pendingCheck);
295             }
296         }
297         if (!mPendingChecks.isEmpty()) {
298             // Schedule a job for remaining pending checks
299             scheduleIdleJob();
300         }
301     }
302 
onUidImportance(int uid, @RunningAppProcessInfo.Importance int importance)303     private void onUidImportance(int uid, @RunningAppProcessInfo.Importance int importance) {
304         var pm = ActivityThread.getPackageManager();
305         try {
306             var packageName = pm.getNameForUid(uid);
307             mHandler.post(() -> onUidImportance(packageName, importance));
308         } catch (RemoteException ignore) {
309         }
310     }
311 
dump(IndentingPrintWriter pw)312     void dump(IndentingPrintWriter pw) {
313         pw.println("Gentle update with constraints info:");
314         pw.increaseIndent();
315         pw.printPair("hasPendingIdleJob", mHasPendingIdleJob);
316         pw.println();
317         pw.printPair("Num of PendingIdleFutures", mPendingIdleFutures.size());
318         pw.println();
319         ArrayDeque<PendingInstallConstraintsCheck> pendingChecks = mPendingChecks.clone();
320         int size = pendingChecks.size();
321         pw.printPair("Num of PendingChecks", size);
322         pw.println();
323         pw.increaseIndent();
324         for (int i = 0; i < size; i++) {
325             pw.print(i); pw.print(":");
326             PendingInstallConstraintsCheck pendingInstallConstraintsCheck = pendingChecks.remove();
327             pendingInstallConstraintsCheck.dump(pw);
328             pw.println();
329         }
330     }
331 }
332