blob: 2a0adeb715592b09e925903bb59b17600b43183a [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.companion;
import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static com.android.internal.util.CollectionUtils.filter;
import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
import static com.android.server.companion.CompanionDeviceManagerService.getCallingUserId;
import static java.util.Collections.unmodifiableMap;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.companion.Association;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
import android.companion.ICompanionDeviceDiscoveryService;
import android.companion.IFindDeviceCallback;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.Signature;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.PackageUtils;
import android.util.Slog;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.PerUser;
import com.android.internal.infra.ServiceConnector;
import com.android.internal.util.ArrayUtils;
import com.android.server.FgThread;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
class AssociationRequestsProcessor {
private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor";
private static final Map<String, String> DEVICE_PROFILE_TO_PERMISSION;
static {
final Map<String, String> map = new ArrayMap<>();
map.put(DEVICE_PROFILE_WATCH, Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH);
map.put(DEVICE_PROFILE_APP_STREAMING,
Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING);
DEVICE_PROFILE_TO_PERMISSION = unmodifiableMap(map);
}
private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
".CompanionDeviceDiscoveryService");
private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
private final Context mContext;
private final CompanionDeviceManagerService mService;
private AssociationRequest mRequest;
private IFindDeviceCallback mFindDeviceCallback;
private String mCallingPackage;
private AndroidFuture<?> mOngoingDeviceDiscovery;
private PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
AssociationRequestsProcessor(CompanionDeviceManagerService service) {
mContext = service.getContext();
mService = service;
final Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() {
@Override
protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
return new ServiceConnector.Impl<>(
mContext,
serviceIntent, 0/* bindingFlags */, userId,
ICompanionDeviceDiscoveryService.Stub::asInterface);
}
};
}
void process(AssociationRequest request, IFindDeviceCallback callback, String callingPackage)
throws RemoteException {
if (DEBUG) {
Slog.d(TAG, "process(request=" + request + ", from=" + callingPackage + ")");
}
checkNotNull(request, "Request cannot be null");
checkNotNull(callback, "Callback cannot be null");
mService.checkCallerIsSystemOr(callingPackage);
int userId = getCallingUserId();
mService.checkUsesFeature(callingPackage, userId);
final String deviceProfile = request.getDeviceProfile();
validateDeviceProfileAndCheckPermission(deviceProfile);
mFindDeviceCallback = callback;
mRequest = request;
mCallingPackage = callingPackage;
request.setCallingPackage(callingPackage);
if (mayAssociateWithoutPrompt(callingPackage, userId)) {
Slog.i(TAG, "setSkipPrompt(true)");
request.setSkipPrompt(true);
}
callback.asBinder().linkToDeath(mBinderDeathRecipient /* recipient */, 0);
mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile)
.thenComposeAsync(description -> {
if (DEBUG) {
Slog.d(TAG, "fetchProfileDescription done: " + description);
}
request.setDeviceProfilePrivilegesDescription(description);
return mServiceConnectors.forUser(userId).postAsync(service -> {
if (DEBUG) {
Slog.d(TAG, "Connected to CDM service -> "
+ "Starting discovery for " + request);
}
AndroidFuture<String> future = new AndroidFuture<>();
service.startDiscovery(request, callingPackage, callback, future);
return future;
}).cancelTimeout();
}, FgThread.getExecutor()).whenComplete(uncheckExceptions((deviceAddress, err) -> {
if (err == null) {
mService.createAssociationInternal(
userId, deviceAddress, callingPackage, deviceProfile);
} else {
Slog.e(TAG, "Failed to discover device(s)", err);
callback.onFailure("No devices found: " + err.getMessage());
}
cleanup();
}));
}
void stopScan(AssociationRequest request, IFindDeviceCallback callback, String callingPackage) {
if (DEBUG) {
Slog.d(TAG, "stopScan(request = " + request + ")");
}
if (Objects.equals(request, mRequest)
&& Objects.equals(callback, mFindDeviceCallback)
&& Objects.equals(callingPackage, mCallingPackage)) {
cleanup();
}
}
private void validateDeviceProfileAndCheckPermission(@Nullable String deviceProfile) {
// Device profile can be null.
if (deviceProfile == null) return;
if (DEVICE_PROFILE_APP_STREAMING.equals(deviceProfile)) {
// TODO: remove, when properly supporting this profile.
throw new UnsupportedOperationException(
"DEVICE_PROFILE_APP_STREAMING is not fully supported yet.");
}
if (!DEVICE_PROFILE_TO_PERMISSION.containsKey(deviceProfile)) {
throw new IllegalArgumentException("Unsupported device profile: " + deviceProfile);
}
final String permission = DEVICE_PROFILE_TO_PERMISSION.get(deviceProfile);
if (mContext.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
throw new SecurityException("Application must hold " + permission + " to associate "
+ "with a device with " + deviceProfile + " profile.");
}
}
private void cleanup() {
if (DEBUG) {
Slog.d(TAG, "cleanup(); discovery = "
+ mOngoingDeviceDiscovery + ", request = " + mRequest);
}
synchronized (mService.mLock) {
AndroidFuture<?> ongoingDeviceDiscovery = mOngoingDeviceDiscovery;
if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) {
ongoingDeviceDiscovery.cancel(true);
}
if (mFindDeviceCallback != null) {
mFindDeviceCallback.asBinder().unlinkToDeath(mBinderDeathRecipient, 0);
mFindDeviceCallback = null;
}
mRequest = null;
mCallingPackage = null;
}
}
private boolean mayAssociateWithoutPrompt(String packageName, int userId) {
String[] sameOemPackages = mContext.getResources()
.getStringArray(com.android.internal.R.array.config_companionDevicePackages);
if (!ArrayUtils.contains(sameOemPackages, packageName)) {
Slog.w(TAG, packageName
+ " can not silently create associations due to no package found."
+ " Packages from OEM: " + Arrays.toString(sameOemPackages)
);
return false;
}
// Throttle frequent associations
long now = System.currentTimeMillis();
Set<Association> recentAssociations = filter(
mService.getAllAssociations(userId, packageName),
a -> now - a.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS);
if (recentAssociations.size() >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
Slog.w(TAG, "Too many associations. " + packageName
+ " already associated " + recentAssociations.size()
+ " devices within the last " + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS
+ "ms: " + recentAssociations);
return false;
}
String[] sameOemCerts = mContext.getResources()
.getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
Signature[] signatures = mService.mPackageManagerInternal
.getPackage(packageName).getSigningDetails().getSignatures();
String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
Set<String> sameOemPackageCerts =
getSameOemPackageCerts(packageName, sameOemPackages, sameOemCerts);
for (String cert : apkCerts) {
if (sameOemPackageCerts.contains(cert)) {
return true;
}
}
Slog.w(TAG, packageName
+ " can not silently create associations. " + packageName
+ " has SHA256 certs from APK: " + Arrays.toString(apkCerts)
+ " and from OEM: " + Arrays.toString(sameOemCerts)
);
return false;
}
@NonNull
private AndroidFuture<String> getDeviceProfilePermissionDescription(
@Nullable String deviceProfile) {
if (deviceProfile == null) {
return AndroidFuture.completedFuture(null);
}
final AndroidFuture<String> result = new AndroidFuture<>();
mService.mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
deviceProfile, FgThread.getExecutor(), desc -> {
try {
result.complete(String.valueOf(desc));
} catch (Exception e) {
result.completeExceptionally(e);
}
});
return result;
}
void dump(@NonNull PrintWriter pw) {
pw.append("Discovery Service State:").append('\n');
for (int i = 0, size = mServiceConnectors.size(); i < size; i++) {
int userId = mServiceConnectors.keyAt(i);
pw.append(" ")
.append("u").append(Integer.toString(userId)).append(": ")
.append(Objects.toString(mServiceConnectors.valueAt(i)))
.append('\n');
}
}
private final IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (DEBUG) {
Slog.d(TAG, "binderDied()");
}
mService.mMainHandler.post(AssociationRequestsProcessor.this::cleanup);
}
};
private static Set<String> getSameOemPackageCerts(
String packageName, String[] oemPackages, String[] sameOemCerts) {
Set<String> sameOemPackageCerts = new HashSet<>();
// Assume OEM may enter same package name in the parallel string array with
// multiple APK certs corresponding to it
for (int i = 0; i < oemPackages.length; i++) {
if (oemPackages[i].equals(packageName)) {
sameOemPackageCerts.add(sameOemCerts[i].replaceAll(":", ""));
}
}
return sameOemPackageCerts;
}
}