2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package com.android.server.companion;
20 import static com.android.internal.util.CollectionUtils.size;
21 import static com.android.internal.util.Preconditions.checkArgument;
22 import static com.android.internal.util.Preconditions.checkNotNull;
23 import static com.android.internal.util.Preconditions.checkState;
24 import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
26 import android.Manifest;
27 import android.annotation.CheckResult;
28 import android.annotation.Nullable;
29 import android.app.PendingIntent;
30 import android.companion.AssociationRequest;
31 import android.companion.CompanionDeviceManager;
32 import android.companion.ICompanionDeviceDiscoveryService;
33 import android.companion.ICompanionDeviceDiscoveryServiceCallback;
34 import android.companion.ICompanionDeviceManager;
35 import android.companion.IFindDeviceCallback;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.ServiceConnection;
40 import android.content.pm.FeatureInfo;
41 import android.content.pm.PackageInfo;
42 import android.content.pm.PackageManager;
43 import android.net.NetworkPolicyManager;
44 import android.os.Binder;
45 import android.os.Environment;
46 import android.os.Handler;
47 import android.os.IBinder;
48 import android.os.IDeviceIdleController;
49 import android.os.IInterface;
50 import android.os.Parcel;
51 import android.os.Process;
52 import android.os.RemoteException;
53 import android.os.ResultReceiver;
54 import android.os.ServiceManager;
55 import android.os.ShellCallback;
56 import android.os.ShellCommand;
57 import android.os.UserHandle;
58 import android.provider.Settings;
59 import android.provider.SettingsStringUtil.ComponentNameSet;
60 import android.text.BidiFormatter;
61 import android.util.ArraySet;
62 import android.util.AtomicFile;
63 import android.util.ExceptionUtils;
64 import android.util.Log;
65 import android.util.Slog;
66 import android.util.Xml;
68 import com.android.internal.app.IAppOpsService;
69 import com.android.internal.content.PackageMonitor;
70 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
71 import com.android.internal.util.ArrayUtils;
72 import com.android.internal.util.CollectionUtils;
73 import com.android.internal.util.function.pooled.PooledLambda;
74 import com.android.server.FgThread;
75 import com.android.server.SystemService;
77 import org.xmlpull.v1.XmlPullParser;
78 import org.xmlpull.v1.XmlPullParserException;
79 import org.xmlpull.v1.XmlSerializer;
82 import java.io.FileDescriptor;
83 import java.io.FileInputStream;
84 import java.io.IOException;
85 import java.nio.charset.StandardCharsets;
86 import java.util.ArrayList;
87 import java.util.List;
88 import java.util.Objects;
90 import java.util.concurrent.ConcurrentHashMap;
91 import java.util.concurrent.ConcurrentMap;
92 import java.util.function.Function;
94 //TODO onStop schedule unbind in 5 seconds
95 //TODO make sure APIs are only callable from currently focused app
96 //TODO schedule stopScan on activity destroy(except if configuration change)
97 //TODO on associate called again after configuration change -> replace old callback with new
98 //TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example)
100 public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
102 private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
103 CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
104 ".DeviceDiscoveryService");
106 private static final boolean DEBUG = false;
107 private static final String LOG_TAG = "CompanionDeviceManagerService";
109 private static final String XML_TAG_ASSOCIATIONS = "associations";
110 private static final String XML_TAG_ASSOCIATION = "association";
111 private static final String XML_ATTR_PACKAGE = "package";
112 private static final String XML_ATTR_DEVICE = "device";
113 private static final String XML_FILE_NAME = "companion_device_manager_associations.xml";
115 private final CompanionDeviceManagerImpl mImpl;
116 private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>();
117 private IDeviceIdleController mIdleController;
118 private ServiceConnection mServiceConnection;
119 private IAppOpsService mAppOpsManager;
121 private IFindDeviceCallback mFindDeviceCallback;
122 private AssociationRequest mRequest;
123 private String mCallingPackage;
125 private final Object mLock = new Object();
127 public CompanionDeviceManagerService(Context context) {
129 mImpl = new CompanionDeviceManagerImpl();
130 mIdleController = IDeviceIdleController.Stub.asInterface(
131 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
132 mAppOpsManager = IAppOpsService.Stub.asInterface(
133 ServiceManager.getService(Context.APP_OPS_SERVICE));
134 registerPackageMonitor();
137 private void registerPackageMonitor() {
138 new PackageMonitor() {
140 public void onPackageRemoved(String packageName, int uid) {
142 as -> CollectionUtils.filter(as,
143 a -> !Objects.equals(a.companionAppPackage, packageName)),
144 getChangingUserId());
148 public void onPackageModified(String packageName) {
149 int userId = getChangingUserId();
150 if (!ArrayUtils.isEmpty(readAllAssociations(userId, packageName))) {
151 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
155 }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
159 public void onStart() {
160 publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
164 public void binderDied() {
165 Handler.getMain().post(this::cleanup);
168 private void cleanup() {
169 synchronized (mLock) {
170 mServiceConnection = unbind(mServiceConnection);
171 mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
173 mCallingPackage = null;
178 * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
182 private static <T extends IInterface> T unlinkToDeath(T iinterface,
183 IBinder.DeathRecipient deathRecipient, int flags) {
184 if (iinterface != null) {
185 iinterface.asBinder().unlinkToDeath(deathRecipient, flags);
192 private ServiceConnection unbind(@Nullable ServiceConnection conn) {
194 getContext().unbindService(conn);
199 class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
202 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
203 throws RemoteException {
205 return super.onTransact(code, data, reply, flags);
206 } catch (Throwable e) {
207 Slog.e(LOG_TAG, "Error during IPC", e);
208 throw ExceptionUtils.propagate(e, RemoteException.class);
213 public void associate(
214 AssociationRequest request,
215 IFindDeviceCallback callback,
216 String callingPackage) throws RemoteException {
218 Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
219 + ", callingPackage = " + callingPackage + ")");
221 checkNotNull(request, "Request cannot be null");
222 checkNotNull(callback, "Callback cannot be null");
223 checkCallerIsSystemOr(callingPackage);
224 int userId = getCallingUserId();
225 checkUsesFeature(callingPackage, userId);
226 final long callingIdentity = Binder.clearCallingIdentity();
228 getContext().bindServiceAsUser(
229 new Intent().setComponent(SERVICE_TO_BIND_TO),
230 createServiceConnection(request, callback, callingPackage),
231 Context.BIND_AUTO_CREATE,
232 UserHandle.of(userId));
234 Binder.restoreCallingIdentity(callingIdentity);
239 public void stopScan(AssociationRequest request,
240 IFindDeviceCallback callback,
241 String callingPackage) {
242 if (Objects.equals(request, mRequest)
243 && Objects.equals(callback, mFindDeviceCallback)
244 && Objects.equals(callingPackage, mCallingPackage)) {
250 public List<String> getAssociations(String callingPackage, int userId)
251 throws RemoteException {
252 checkCallerIsSystemOr(callingPackage, userId);
253 checkUsesFeature(callingPackage, getCallingUserId());
254 return new ArrayList<>(CollectionUtils.map(
255 readAllAssociations(userId, callingPackage),
256 a -> a.deviceAddress));
259 //TODO also revoke notification access
261 public void disassociate(String deviceMacAddress, String callingPackage)
262 throws RemoteException {
263 checkNotNull(deviceMacAddress);
264 checkCallerIsSystemOr(callingPackage);
265 checkUsesFeature(callingPackage, getCallingUserId());
266 removeAssociation(getCallingUserId(), callingPackage, deviceMacAddress);
269 private void checkCallerIsSystemOr(String pkg) throws RemoteException {
270 checkCallerIsSystemOr(pkg, getCallingUserId());
273 private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
274 if (isCallerSystem()) {
278 checkArgument(getCallingUserId() == userId,
279 "Must be called by either same user or system");
280 mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);
284 public PendingIntent requestNotificationAccess(ComponentName component)
285 throws RemoteException {
286 String callingPackage = component.getPackageName();
287 checkCanCallNotificationApi(callingPackage);
288 int userId = getCallingUserId();
289 String packageTitle = BidiFormatter.getInstance().unicodeWrap(
290 getPackageInfo(callingPackage, userId)
292 .loadSafeLabel(getContext().getPackageManager())
294 long identity = Binder.clearCallingIdentity();
296 return PendingIntent.getActivity(getContext(),
297 0 /* request code */,
298 NotificationAccessConfirmationActivityContract.launcherIntent(
299 userId, component, packageTitle),
300 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
301 | PendingIntent.FLAG_CANCEL_CURRENT);
303 Binder.restoreCallingIdentity(identity);
308 public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
309 checkCanCallNotificationApi(component.getPackageName());
310 String setting = Settings.Secure.getString(getContext().getContentResolver(),
311 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
312 return new ComponentNameSet(setting).contains(component);
315 private void checkCanCallNotificationApi(String callingPackage) throws RemoteException {
316 checkCallerIsSystemOr(callingPackage);
317 int userId = getCallingUserId();
318 checkState(!ArrayUtils.isEmpty(readAllAssociations(userId, callingPackage)),
319 "App must have an association before calling this API");
320 checkUsesFeature(callingPackage, userId);
323 private void checkUsesFeature(String pkg, int userId) {
324 if (isCallerSystem()) {
325 // Drop the requirement for calls from system process
329 FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
330 String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
331 int numFeatures = ArrayUtils.size(reqFeatures);
332 for (int i = 0; i < numFeatures; i++) {
333 if (requiredFeature.equals(reqFeatures[i].name)) return;
335 throw new IllegalStateException("Must declare uses-feature "
337 + " in manifest to use this API");
341 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
342 String[] args, ShellCallback callback, ResultReceiver resultReceiver)
343 throws RemoteException {
344 new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver);
348 private static int getCallingUserId() {
349 return UserHandle.getUserId(Binder.getCallingUid());
352 private static boolean isCallerSystem() {
353 return Binder.getCallingUid() == Process.SYSTEM_UID;
356 private ServiceConnection createServiceConnection(
357 final AssociationRequest request,
358 final IFindDeviceCallback findDeviceCallback,
359 final String callingPackage) {
360 mServiceConnection = new ServiceConnection() {
362 public void onServiceConnected(ComponentName name, IBinder service) {
365 "onServiceConnected(name = " + name + ", service = "
369 mFindDeviceCallback = findDeviceCallback;
371 mCallingPackage = callingPackage;
374 mFindDeviceCallback.asBinder().linkToDeath(
375 CompanionDeviceManagerService.this, 0);
376 } catch (RemoteException e) {
382 ICompanionDeviceDiscoveryService.Stub
383 .asInterface(service)
388 getServiceCallback());
389 } catch (RemoteException e) {
390 Log.e(LOG_TAG, "Error while initiating device discovery", e);
395 public void onServiceDisconnected(ComponentName name) {
396 if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")");
399 return mServiceConnection;
402 private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() {
403 return new ICompanionDeviceDiscoveryServiceCallback.Stub() {
406 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
407 throws RemoteException {
409 return super.onTransact(code, data, reply, flags);
410 } catch (Throwable e) {
411 Slog.e(LOG_TAG, "Error during IPC", e);
412 throw ExceptionUtils.propagate(e, RemoteException.class);
417 public void onDeviceSelected(String packageName, int userId, String deviceAddress) {
418 addAssociation(userId, packageName, deviceAddress);
423 public void onDeviceSelectionCancel() {
429 void addAssociation(int userId, String packageName, String deviceAddress) {
430 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
431 recordAssociation(packageName, deviceAddress);
434 void removeAssociation(int userId, String pkg, String deviceMacAddress) {
435 updateAssociations(associations -> CollectionUtils.remove(associations,
436 new Association(userId, deviceMacAddress, pkg)));
439 private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) {
440 PackageInfo packageInfo = getPackageInfo(packageName, userId);
441 if (packageInfo == null) {
445 Binder.withCleanCallingIdentity(obtainRunnable(CompanionDeviceManagerService::
446 updateSpecialAccessPermissionAsSystem, this, packageInfo).recycleOnUse());
449 private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
451 if (containsEither(packageInfo.requestedPermissions,
452 android.Manifest.permission.RUN_IN_BACKGROUND,
453 android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
454 mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
456 mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
458 } catch (RemoteException e) {
459 /* ignore - local call */
462 NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
463 if (containsEither(packageInfo.requestedPermissions,
464 android.Manifest.permission.USE_DATA_IN_BACKGROUND,
465 android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
466 networkPolicyManager.addUidPolicy(
467 packageInfo.applicationInfo.uid,
468 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
470 networkPolicyManager.removeUidPolicy(
471 packageInfo.applicationInfo.uid,
472 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
476 private static <T> boolean containsEither(T[] array, T a, T b) {
477 return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
481 private PackageInfo getPackageInfo(String packageName, int userId) {
482 return Binder.withCleanCallingIdentity(PooledLambda.obtainSupplier((context, pkg, id) -> {
484 return context.getPackageManager().getPackageInfoAsUser(
486 PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
488 } catch (PackageManager.NameNotFoundException e) {
489 Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + pkg, e);
492 }, getContext(), packageName, userId).recycleOnUse());
495 private void recordAssociation(String priviledgedPackage, String deviceAddress) {
497 Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage
498 + ", deviceAddress = " + deviceAddress + ")");
500 int userId = getCallingUserId();
501 updateAssociations(associations -> CollectionUtils.add(associations,
502 new Association(userId, deviceAddress, priviledgedPackage)));
505 private void updateAssociations(Function<Set<Association>, Set<Association>> update) {
506 updateAssociations(update, getCallingUserId());
509 private void updateAssociations(Function<Set<Association>, Set<Association>> update,
511 final AtomicFile file = getStorageFileForUser(userId);
512 synchronized (file) {
513 Set<Association> associations = readAllAssociations(userId);
514 final Set<Association> old = CollectionUtils.copyOf(associations);
515 associations = update.apply(associations);
516 if (size(old) == size(associations)) return;
518 Set<Association> finalAssociations = associations;
519 file.write((out) -> {
520 XmlSerializer xml = Xml.newSerializer();
522 xml.setOutput(out, StandardCharsets.UTF_8.name());
523 xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
524 xml.startDocument(null, true);
525 xml.startTag(null, XML_TAG_ASSOCIATIONS);
527 CollectionUtils.forEach(finalAssociations, association -> {
528 xml.startTag(null, XML_TAG_ASSOCIATION)
529 .attribute(null, XML_ATTR_PACKAGE, association.companionAppPackage)
530 .attribute(null, XML_ATTR_DEVICE, association.deviceAddress)
531 .endTag(null, XML_TAG_ASSOCIATION);
534 xml.endTag(null, XML_TAG_ASSOCIATIONS);
536 } catch (Exception e) {
537 Slog.e(LOG_TAG, "Error while writing associations file", e);
538 throw ExceptionUtils.propagate(e);
545 private AtomicFile getStorageFileForUser(int uid) {
546 return mUidToStorage.computeIfAbsent(uid, (u) ->
547 new AtomicFile(new File(
548 //TODO deprecated method - what's the right replacement?
549 Environment.getUserSystemDirectory(u),
554 private Set<Association> readAllAssociations(int userId) {
555 return readAllAssociations(userId, null);
559 private Set<Association> readAllAssociations(int userId, @Nullable String packageFilter) {
560 final AtomicFile file = getStorageFileForUser(userId);
562 if (!file.getBaseFile().exists()) return null;
564 ArraySet<Association> result = null;
565 final XmlPullParser parser = Xml.newPullParser();
566 synchronized (file) {
567 try (FileInputStream in = file.openRead()) {
568 parser.setInput(in, StandardCharsets.UTF_8.name());
570 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
571 if (type != XmlPullParser.START_TAG
572 && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue;
574 final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE);
575 final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE);
577 if (appPackage == null || deviceAddress == null) continue;
578 if (packageFilter != null && !packageFilter.equals(appPackage)) continue;
580 result = ArrayUtils.add(result,
581 new Association(userId, deviceAddress, appPackage));
584 } catch (XmlPullParserException | IOException e) {
585 Slog.e(LOG_TAG, "Error while reading associations file", e);
593 private class Association {
594 public final int uid;
595 public final String deviceAddress;
596 public final String companionAppPackage;
598 private Association(int uid, String deviceAddress, String companionAppPackage) {
600 this.deviceAddress = checkNotNull(deviceAddress);
601 this.companionAppPackage = checkNotNull(companionAppPackage);
605 public boolean equals(Object o) {
606 if (this == o) return true;
607 if (o == null || getClass() != o.getClass()) return false;
609 Association that = (Association) o;
611 if (uid != that.uid) return false;
612 if (!deviceAddress.equals(that.deviceAddress)) return false;
613 return companionAppPackage.equals(that.companionAppPackage);
618 public int hashCode() {
620 result = 31 * result + deviceAddress.hashCode();
621 result = 31 * result + companionAppPackage.hashCode();
626 private class ShellCmd extends ShellCommand {
627 public static final String USAGE = "help\n"
629 + "associate USER_ID PACKAGE MAC_ADDRESS\n"
630 + "disassociate USER_ID PACKAGE MAC_ADDRESS";
633 getContext().enforceCallingOrSelfPermission(
634 android.Manifest.permission.MANAGE_COMPANION_DEVICES, "ShellCmd");
638 public int onCommand(String cmd) {
641 CollectionUtils.forEach(
642 readAllAssociations(getNextArgInt()),
643 a -> getOutPrintWriter()
644 .println(a.companionAppPackage + " " + a.deviceAddress));
648 addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
651 case "disassociate": {
652 removeAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
655 default: return handleDefaultCommands(cmd);
660 private int getNextArgInt() {
661 return Integer.parseInt(getNextArgRequired());
665 public void onHelp() {
666 getOutPrintWriter().println(USAGE);