Loading services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java 0 → 100644 +320 −0 Original line number Diff line number Diff line /* * 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; } } services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +46 −284 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java 0 → 100644 +320 −0 Original line number Diff line number Diff line /* * 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; } }
services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +46 −284 File changed.Preview size limit exceeded, changes collapsed. Show changes