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