1 /*
2  * Copyright (C) 2020 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.job.controllers;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.BroadcastReceiver;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.pm.PackageManager;
27 import android.content.pm.PackageManager.NameNotFoundException;
28 import android.content.pm.ServiceInfo;
29 import android.net.Uri;
30 import android.os.UserHandle;
31 import android.util.IndentingPrintWriter;
32 import android.util.Log;
33 import android.util.Slog;
34 import android.util.SparseArrayMap;
35 import android.util.proto.ProtoOutputStream;
36 
37 import com.android.internal.annotations.GuardedBy;
38 import com.android.server.job.JobSchedulerService;
39 
40 import java.util.Objects;
41 import java.util.function.Consumer;
42 import java.util.function.Predicate;
43 
44 /**
45  * Controller that tracks changes in the service component's enabled state.
46  */
47 public class ComponentController extends StateController {
48     private static final String TAG = "JobScheduler.Component";
49     private static final boolean DEBUG = JobSchedulerService.DEBUG
50             || Log.isLoggable(TAG, Log.DEBUG);
51 
52     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
53 
54         @Override
55         public void onReceive(Context context, Intent intent) {
56             final String action = intent.getAction();
57             if (action == null) {
58                 Slog.wtf(TAG, "Intent action was null");
59                 return;
60             }
61             switch (action) {
62                 case Intent.ACTION_PACKAGE_ADDED:
63                     if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
64                         // Only do this for app updates since new installs won't have any jobs
65                         // scheduled.
66                         final Uri uri = intent.getData();
67                         final String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
68                         if (pkg != null) {
69                             final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
70                             final int userId = UserHandle.getUserId(pkgUid);
71                             updateComponentStateForPackage(userId, pkg);
72                         }
73                     }
74                     break;
75                 case Intent.ACTION_PACKAGE_CHANGED:
76                     final Uri uri = intent.getData();
77                     final String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
78                     final String[] changedComponents = intent.getStringArrayExtra(
79                             Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
80                     if (pkg != null && changedComponents != null && changedComponents.length > 0) {
81                         final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
82                         final int userId = UserHandle.getUserId(pkgUid);
83                         updateComponentStateForPackage(userId, pkg);
84                     }
85                     break;
86                 case Intent.ACTION_USER_UNLOCKED:
87                 case Intent.ACTION_USER_STOPPED:
88                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
89                     updateComponentStateForUser(userId);
90                     break;
91             }
92         }
93     };
94 
95     private final SparseArrayMap<ComponentName, ServiceInfo> mServiceInfoCache =
96             new SparseArrayMap<>();
97 
98     private final ComponentStateUpdateFunctor mComponentStateUpdateFunctor =
99             new ComponentStateUpdateFunctor();
100 
ComponentController(JobSchedulerService service)101     public ComponentController(JobSchedulerService service) {
102         super(service);
103 
104         final IntentFilter filter = new IntentFilter();
105         filter.addAction(Intent.ACTION_PACKAGE_ADDED);
106         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
107         filter.addDataScheme("package");
108         mContext.registerReceiverAsUser(
109                 mBroadcastReceiver, UserHandle.ALL, filter, null, null);
110         final IntentFilter userFilter = new IntentFilter();
111         userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
112         userFilter.addAction(Intent.ACTION_USER_STOPPED);
113         mContext.registerReceiverAsUser(
114                 mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
115 
116     }
117 
118     @Override
119     @GuardedBy("mLock")
maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob)120     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
121         updateComponentEnabledStateLocked(jobStatus);
122     }
123 
124     @Override
maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate)125     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
126             boolean forUpdate) {
127     }
128 
129     @Override
130     @GuardedBy("mLock")
onAppRemovedLocked(String packageName, int uid)131     public void onAppRemovedLocked(String packageName, int uid) {
132         clearComponentsForPackageLocked(UserHandle.getUserId(uid), packageName);
133     }
134 
135     @Override
136     @GuardedBy("mLock")
onUserRemovedLocked(int userId)137     public void onUserRemovedLocked(int userId) {
138         mServiceInfoCache.delete(userId);
139     }
140 
141     @Nullable
142     @GuardedBy("mLock")
getServiceInfoLocked(JobStatus jobStatus)143     private ServiceInfo getServiceInfoLocked(JobStatus jobStatus) {
144         final ComponentName service = jobStatus.getServiceComponent();
145         final int userId = jobStatus.getUserId();
146         if (mServiceInfoCache.contains(userId, service)) {
147             // Return whatever is in the cache, even if it's null. When something changes, we
148             // clear the cache.
149             return mServiceInfoCache.get(userId, service);
150         }
151 
152         ServiceInfo si;
153         try {
154             // createContextAsUser may potentially be expensive
155             // TODO: cache user context or improve ContextImpl implementation if this becomes
156             // a problem
157             si = mContext.createContextAsUser(UserHandle.of(userId), 0)
158                     .getPackageManager()
159                     .getServiceInfo(service, PackageManager.MATCH_DIRECT_BOOT_AUTO);
160         } catch (NameNotFoundException e) {
161             if (mService.areUsersStartedLocked(jobStatus)) {
162                 // User is fully unlocked but PM still says the package doesn't exist.
163                 Slog.e(TAG, "Job exists for non-existent package: " + service.getPackageName());
164             }
165             // Write null to the cache so we don't keep querying PM.
166             si = null;
167         }
168         mServiceInfoCache.add(userId, service, si);
169 
170         return si;
171     }
172 
173     @GuardedBy("mLock")
updateComponentEnabledStateLocked(JobStatus jobStatus)174     private boolean updateComponentEnabledStateLocked(JobStatus jobStatus) {
175         final ServiceInfo service = getServiceInfoLocked(jobStatus);
176 
177         if (DEBUG && service == null) {
178             Slog.v(TAG, jobStatus.toShortString() + " component not present");
179         }
180         final ServiceInfo ogService = jobStatus.serviceInfo;
181         jobStatus.serviceInfo = service;
182         return !Objects.equals(ogService, service);
183     }
184 
185     @GuardedBy("mLock")
clearComponentsForPackageLocked(final int userId, final String pkg)186     private void clearComponentsForPackageLocked(final int userId, final String pkg) {
187         final int uIdx = mServiceInfoCache.indexOfKey(userId);
188         for (int c = mServiceInfoCache.numElementsForKey(userId) - 1; c >= 0; --c) {
189             final ComponentName cn = mServiceInfoCache.keyAt(uIdx, c);
190             if (cn.getPackageName().equals(pkg)) {
191                 mServiceInfoCache.delete(userId, cn);
192             }
193         }
194     }
195 
updateComponentStateForPackage(final int userId, final String pkg)196     private void updateComponentStateForPackage(final int userId, final String pkg) {
197         synchronized (mLock) {
198             clearComponentsForPackageLocked(userId, pkg);
199             updateComponentStatesLocked(jobStatus -> {
200                 // Using user ID instead of source user ID because the service will run under the
201                 // user ID, not source user ID.
202                 return jobStatus.getUserId() == userId
203                         && jobStatus.getServiceComponent().getPackageName().equals(pkg);
204             });
205         }
206     }
207 
updateComponentStateForUser(final int userId)208     private void updateComponentStateForUser(final int userId) {
209         synchronized (mLock) {
210             mServiceInfoCache.delete(userId);
211             updateComponentStatesLocked(jobStatus -> {
212                 // Using user ID instead of source user ID because the service will run under the
213                 // user ID, not source user ID.
214                 return jobStatus.getUserId() == userId;
215             });
216         }
217     }
218 
219     @GuardedBy("mLock")
updateComponentStatesLocked(@onNull Predicate<JobStatus> filter)220     private void updateComponentStatesLocked(@NonNull Predicate<JobStatus> filter) {
221         mComponentStateUpdateFunctor.reset();
222         mService.getJobStore().forEachJob(filter, mComponentStateUpdateFunctor);
223         if (mComponentStateUpdateFunctor.mChanged) {
224             mStateChangedListener.onControllerStateChanged();
225         }
226     }
227 
228     final class ComponentStateUpdateFunctor implements Consumer<JobStatus> {
229         @GuardedBy("mLock")
230         boolean mChanged;
231 
232         @Override
233         @GuardedBy("mLock")
accept(JobStatus jobStatus)234         public void accept(JobStatus jobStatus) {
235             mChanged |= updateComponentEnabledStateLocked(jobStatus);
236         }
237 
238         @GuardedBy("mLock")
reset()239         private void reset() {
240             mChanged = false;
241         }
242     }
243 
244     @Override
245     @GuardedBy("mLock")
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)246     public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
247         for (int u = 0; u < mServiceInfoCache.numMaps(); ++u) {
248             final int userId = mServiceInfoCache.keyAt(u);
249             for (int p = 0; p < mServiceInfoCache.numElementsForKey(userId); ++p) {
250                 final ComponentName componentName = mServiceInfoCache.keyAt(u, p);
251                 pw.print(userId);
252                 pw.print("-");
253                 pw.print(componentName);
254                 pw.print(": ");
255                 pw.print(mServiceInfoCache.valueAt(u, p));
256                 pw.println();
257             }
258         }
259     }
260 
261     @Override
262     @GuardedBy("mLock")
dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)263     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
264             Predicate<JobStatus> predicate) {
265 
266     }
267 }
268