blob: 3ca8a5a1f5544d096ab6c653220aa47943740391 [file] [log] [blame]
/*
* Copyright (C) 2021 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.servicewatcher;
import static android.content.pm.PackageManager.GET_META_DATA;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.UserHandle.USER_SYSTEM;
import android.annotation.BoolRes;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.app.ActivityManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.util.Log;
import com.android.internal.util.Preconditions;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.servicewatcher.ServiceWatcher.ServiceChangedListener;
import com.android.server.servicewatcher.ServiceWatcher.ServiceSupplier;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* Supplies services based on the current active user and version as defined in the service
* manifest. This implementation uses {@link android.content.pm.PackageManager#MATCH_SYSTEM_ONLY} to
* ensure only system (ie, privileged) services are matched. It also handles services that are not
* direct boot aware, and will automatically pick the best service as the user's direct boot state
* changes.
*
* <p>Optionally, two permissions may be specified: (1) a caller permission - any service that does
* not require callers to hold this permission is rejected (2) a service permission - any service
* whose package does not hold this permission is rejected.
*/
public class CurrentUserServiceSupplier extends BroadcastReceiver implements
ServiceSupplier<CurrentUserServiceSupplier.BoundServiceInfo> {
private static final String TAG = "CurrentUserServiceSupplier";
private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser";
private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
if (o1 == o2) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
}
// ServiceInfos with higher version numbers always win. if version numbers are equal
// then we prefer components that work for all users vs components that only work for a
// single user at a time. otherwise everything's equal.
int ret = Integer.compare(o1.getVersion(), o2.getVersion());
if (ret == 0) {
if (o1.getUserId() != USER_SYSTEM && o2.getUserId() == USER_SYSTEM) {
ret = -1;
} else if (o1.getUserId() == USER_SYSTEM && o2.getUserId() != USER_SYSTEM) {
ret = 1;
}
}
return ret;
};
/** Bound service information with version information. */
public static class BoundServiceInfo extends ServiceWatcher.BoundServiceInfo {
private static int parseUid(ResolveInfo resolveInfo) {
int uid = resolveInfo.serviceInfo.applicationInfo.uid;
Bundle metadata = resolveInfo.serviceInfo.metaData;
if (metadata != null && metadata.getBoolean(EXTRA_SERVICE_IS_MULTIUSER, false)) {
// reconstruct a uid for the same app but with the system user - hope this exists
uid = UserHandle.getUid(USER_SYSTEM, UserHandle.getAppId(uid));
}
return uid;
}
private static int parseVersion(ResolveInfo resolveInfo) {
int version = Integer.MIN_VALUE;
if (resolveInfo.serviceInfo.metaData != null) {
version = resolveInfo.serviceInfo.metaData.getInt(EXTRA_SERVICE_VERSION, version);
}
return version;
}
private final int mVersion;
private final @Nullable Bundle mMetadata;
protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
this(action, parseUid(resolveInfo), resolveInfo.serviceInfo.getComponentName(),
parseVersion(resolveInfo), resolveInfo.serviceInfo.metaData);
}
protected BoundServiceInfo(String action, int uid, ComponentName componentName, int version,
@Nullable Bundle metadata) {
super(action, uid, componentName);
mVersion = version;
mMetadata = metadata;
}
public int getVersion() {
return mVersion;
}
public @Nullable Bundle getMetadata() {
return mMetadata;
}
@Override
public String toString() {
return super.toString() + "@" + mVersion;
}
}
private static @Nullable String retrieveExplicitPackage(Context context,
@BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) {
Resources resources = context.getResources();
boolean enableOverlay = resources.getBoolean(enableOverlayResId);
if (!enableOverlay) {
return resources.getString(nonOverlayPackageResId);
} else {
return null;
}
}
private final Context mContext;
private final ActivityManagerInternal mActivityManager;
private final Intent mIntent;
// a permission that the service forces callers (ie ServiceWatcher/system server) to hold
private final @Nullable String mCallerPermission;
// a permission that the service package should hold
private final @Nullable String mServicePermission;
private volatile ServiceChangedListener mListener;
public CurrentUserServiceSupplier(Context context, String action) {
this(context, action, null, null, null);
}
public CurrentUserServiceSupplier(Context context, String action,
@BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) {
this(context, action,
retrieveExplicitPackage(context, enableOverlayResId, nonOverlayPackageResId), null,
null);
}
public CurrentUserServiceSupplier(Context context, String action,
@BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId,
@Nullable String callerPermission, @Nullable String servicePermission) {
this(context, action,
retrieveExplicitPackage(context, enableOverlayResId, nonOverlayPackageResId),
callerPermission, servicePermission);
}
public CurrentUserServiceSupplier(Context context, String action,
@Nullable String explicitPackage, @Nullable String callerPermission,
@Nullable String servicePermission) {
mContext = context;
mActivityManager = Objects.requireNonNull(
LocalServices.getService(ActivityManagerInternal.class));
mIntent = new Intent(action);
if (explicitPackage != null) {
mIntent.setPackage(explicitPackage);
}
mCallerPermission = callerPermission;
mServicePermission = servicePermission;
}
@Override
public boolean hasMatchingService() {
List<ResolveInfo> resolveInfos = mContext.getPackageManager()
.queryIntentServicesAsUser(mIntent,
MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY,
UserHandle.USER_SYSTEM);
return !resolveInfos.isEmpty();
}
@Override
public void register(ServiceChangedListener listener) {
Preconditions.checkState(mListener == null);
mListener = listener;
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
mContext.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null,
FgThread.getHandler());
}
@Override
public void unregister() {
Preconditions.checkArgument(mListener != null);
mListener = null;
mContext.unregisterReceiver(this);
}
@Override
public BoundServiceInfo getServiceInfo() {
BoundServiceInfo bestServiceInfo = null;
// only allow privileged services in the correct direct boot state to match
int currentUserId = mActivityManager.getCurrentUserId();
List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
mIntent,
GET_META_DATA | MATCH_DIRECT_BOOT_AUTO | MATCH_SYSTEM_ONLY,
currentUserId);
for (ResolveInfo resolveInfo : resolveInfos) {
ServiceInfo service = Objects.requireNonNull(resolveInfo.serviceInfo);
if (mCallerPermission != null) {
if (!mCallerPermission.equals(service.permission)) {
Log.d(TAG, service.getComponentName().flattenToShortString()
+ " disqualified due to not requiring " + mCallerPermission);
continue;
}
}
BoundServiceInfo serviceInfo = new BoundServiceInfo(mIntent.getAction(), resolveInfo);
if (mServicePermission != null) {
if (PermissionManager.checkPackageNamePermission(mServicePermission,
service.packageName, serviceInfo.getUserId()) != PERMISSION_GRANTED) {
Log.d(TAG, serviceInfo.getComponentName().flattenToShortString()
+ " disqualified due to not holding " + mCallerPermission);
continue;
}
}
if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) {
bestServiceInfo = serviceInfo;
}
}
return bestServiceInfo;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) {
return;
}
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
if (userId == UserHandle.USER_NULL) {
return;
}
ServiceChangedListener listener = mListener;
if (listener == null) {
return;
}
switch (action) {
case Intent.ACTION_USER_SWITCHED:
listener.onServiceChanged();
break;
case Intent.ACTION_USER_UNLOCKED:
// user unlocked implies direct boot mode may have changed
if (userId == mActivityManager.getCurrentUserId()) {
listener.onServiceChanged();
}
break;
default:
break;
}
}
}