blob: 12d9c7f52fb5f9075da8a0ad12ea1bc025d3d64d [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.job.controllers;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.UserHandle;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.server.job.JobSchedulerService;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* Controller that tracks changes in the service component's enabled state.
*/
public class ComponentController extends StateController {
private static final String TAG = "JobScheduler.Component";
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action == null) {
Slog.wtf(TAG, "Intent action was null");
return;
}
switch (action) {
case Intent.ACTION_PACKAGE_ADDED:
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
// Only do this for app updates since new installs won't have any jobs
// scheduled.
final Uri uri = intent.getData();
final String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
if (pkg != null) {
final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
final int userId = UserHandle.getUserId(pkgUid);
updateComponentStateForPackage(userId, pkg);
}
}
break;
case Intent.ACTION_PACKAGE_CHANGED:
final Uri uri = intent.getData();
final String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
final String[] changedComponents = intent.getStringArrayExtra(
Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
if (pkg != null && changedComponents != null && changedComponents.length > 0) {
final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
final int userId = UserHandle.getUserId(pkgUid);
updateComponentStateForPackage(userId, pkg);
}
break;
case Intent.ACTION_USER_UNLOCKED:
case Intent.ACTION_USER_STOPPED:
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
updateComponentStateForUser(userId);
break;
}
}
};
private final SparseArrayMap<ComponentName, ServiceInfo> mServiceInfoCache =
new SparseArrayMap<>();
private final ComponentStateUpdateFunctor mComponentStateUpdateFunctor =
new ComponentStateUpdateFunctor();
public ComponentController(JobSchedulerService service) {
super(service);
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mContext.registerReceiverAsUser(
mBroadcastReceiver, UserHandle.ALL, filter, null, null);
final IntentFilter userFilter = new IntentFilter();
userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
userFilter.addAction(Intent.ACTION_USER_STOPPED);
mContext.registerReceiverAsUser(
mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
}
@Override
@GuardedBy("mLock")
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
updateComponentEnabledStateLocked(jobStatus);
}
@Override
public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
boolean forUpdate) {
}
@Override
@GuardedBy("mLock")
public void onAppRemovedLocked(String packageName, int uid) {
clearComponentsForPackageLocked(UserHandle.getUserId(uid), packageName);
}
@Override
@GuardedBy("mLock")
public void onUserRemovedLocked(int userId) {
mServiceInfoCache.delete(userId);
}
@Nullable
@GuardedBy("mLock")
private ServiceInfo getServiceInfoLocked(JobStatus jobStatus) {
final ComponentName service = jobStatus.getServiceComponent();
final int userId = jobStatus.getUserId();
if (mServiceInfoCache.contains(userId, service)) {
// Return whatever is in the cache, even if it's null. When something changes, we
// clear the cache.
return mServiceInfoCache.get(userId, service);
}
ServiceInfo si;
try {
// createContextAsUser may potentially be expensive
// TODO: cache user context or improve ContextImpl implementation if this becomes
// a problem
si = mContext.createContextAsUser(UserHandle.of(userId), 0)
.getPackageManager()
.getServiceInfo(service, PackageManager.MATCH_DIRECT_BOOT_AUTO);
} catch (NameNotFoundException e) {
if (mService.areUsersStartedLocked(jobStatus)) {
// User is fully unlocked but PM still says the package doesn't exist.
Slog.e(TAG, "Job exists for non-existent package: " + service.getPackageName());
}
// Write null to the cache so we don't keep querying PM.
si = null;
}
mServiceInfoCache.add(userId, service, si);
return si;
}
@GuardedBy("mLock")
private boolean updateComponentEnabledStateLocked(JobStatus jobStatus) {
final ServiceInfo service = getServiceInfoLocked(jobStatus);
if (DEBUG && service == null) {
Slog.v(TAG, jobStatus.toShortString() + " component not present");
}
final ServiceInfo ogService = jobStatus.serviceInfo;
jobStatus.serviceInfo = service;
return !Objects.equals(ogService, service);
}
@GuardedBy("mLock")
private void clearComponentsForPackageLocked(final int userId, final String pkg) {
final int uIdx = mServiceInfoCache.indexOfKey(userId);
for (int c = mServiceInfoCache.numElementsForKey(userId) - 1; c >= 0; --c) {
final ComponentName cn = mServiceInfoCache.keyAt(uIdx, c);
if (cn.getPackageName().equals(pkg)) {
mServiceInfoCache.delete(userId, cn);
}
}
}
private void updateComponentStateForPackage(final int userId, final String pkg) {
synchronized (mLock) {
clearComponentsForPackageLocked(userId, pkg);
updateComponentStatesLocked(jobStatus -> {
// Using user ID instead of source user ID because the service will run under the
// user ID, not source user ID.
return jobStatus.getUserId() == userId
&& jobStatus.getServiceComponent().getPackageName().equals(pkg);
});
}
}
private void updateComponentStateForUser(final int userId) {
synchronized (mLock) {
mServiceInfoCache.delete(userId);
updateComponentStatesLocked(jobStatus -> {
// Using user ID instead of source user ID because the service will run under the
// user ID, not source user ID.
return jobStatus.getUserId() == userId;
});
}
}
@GuardedBy("mLock")
private void updateComponentStatesLocked(@NonNull Predicate<JobStatus> filter) {
mComponentStateUpdateFunctor.reset();
mService.getJobStore().forEachJob(filter, mComponentStateUpdateFunctor);
if (mComponentStateUpdateFunctor.mChanged) {
mStateChangedListener.onControllerStateChanged();
}
}
final class ComponentStateUpdateFunctor implements Consumer<JobStatus> {
@GuardedBy("mLock")
boolean mChanged;
@Override
@GuardedBy("mLock")
public void accept(JobStatus jobStatus) {
mChanged |= updateComponentEnabledStateLocked(jobStatus);
}
@GuardedBy("mLock")
private void reset() {
mChanged = false;
}
}
@Override
@GuardedBy("mLock")
public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
for (int u = 0; u < mServiceInfoCache.numMaps(); ++u) {
final int userId = mServiceInfoCache.keyAt(u);
for (int p = 0; p < mServiceInfoCache.numElementsForKey(userId); ++p) {
final ComponentName componentName = mServiceInfoCache.keyAt(u, p);
pw.print(userId);
pw.print("-");
pw.print(componentName);
pw.print(": ");
pw.print(mServiceInfoCache.valueAt(u, p));
pw.println();
}
}
}
@Override
@GuardedBy("mLock")
public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
Predicate<JobStatus> predicate) {
}
}