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;
25 import android.Manifest;
26 import android.annotation.CheckResult;
27 import android.annotation.Nullable;
28 import android.app.PendingIntent;
29 import android.companion.AssociationRequest;
30 import android.companion.CompanionDeviceManager;
31 import android.companion.ICompanionDeviceDiscoveryService;
32 import android.companion.ICompanionDeviceDiscoveryServiceCallback;
33 import android.companion.ICompanionDeviceManager;
34 import android.companion.IFindDeviceCallback;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.ServiceConnection;
39 import android.content.pm.FeatureInfo;
40 import android.content.pm.PackageInfo;
41 import android.content.pm.PackageManager;
42 import android.net.NetworkPolicyManager;
43 import android.os.Binder;
44 import android.os.Environment;
45 import android.os.Handler;
46 import android.os.IBinder;
47 import android.os.IDeviceIdleController;
48 import android.os.IInterface;
49 import android.os.Parcel;
50 import android.os.Process;
51 import android.os.RemoteException;
52 import android.os.ResultReceiver;
53 import android.os.ServiceManager;
54 import android.os.ShellCallback;
55 import android.os.ShellCommand;
56 import android.os.UserHandle;
57 import android.provider.Settings;
58 import android.provider.SettingsStringUtil.ComponentNameSet;
59 import android.text.BidiFormatter;
60 import android.util.ArraySet;
61 import android.util.AtomicFile;
62 import android.util.ExceptionUtils;
63 import android.util.Log;
64 import android.util.Slog;
65 import android.util.Xml;
67 import com.android.internal.app.IAppOpsService;
68 import com.android.internal.content.PackageMonitor;
69 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
70 import com.android.internal.util.ArrayUtils;
71 import com.android.internal.util.CollectionUtils;
72 import com.android.server.FgThread;
73 import com.android.server.SystemService;
75 import org.xmlpull.v1.XmlPullParser;
76 import org.xmlpull.v1.XmlPullParserException;
77 import org.xmlpull.v1.XmlSerializer;
80 import java.io.FileDescriptor;
81 import java.io.FileInputStream;
82 import java.io.IOException;
83 import java.nio.charset.StandardCharsets;
84 import java.util.ArrayList;
85 import java.util.List;
86 import java.util.Objects;
88 import java.util.concurrent.ConcurrentHashMap;
89 import java.util.concurrent.ConcurrentMap;
90 import java.util.function.Function;
92 //TODO onStop schedule unbind in 5 seconds
93 //TODO make sure APIs are only callable from currently focused app
94 //TODO schedule stopScan on activity destroy(except if configuration change)
95 //TODO on associate called again after configuration change -> replace old callback with new
96 //TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example)
98 public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
100 private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
101 CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
102 ".DeviceDiscoveryService");
104 private static final boolean DEBUG = false;
105 private static final String LOG_TAG = "CompanionDeviceManagerService";
107 private static final String XML_TAG_ASSOCIATIONS = "associations";
108 private static final String XML_TAG_ASSOCIATION = "association";
109 private static final String XML_ATTR_PACKAGE = "package";
110 private static final String XML_ATTR_DEVICE = "device";
111 private static final String XML_FILE_NAME = "companion_device_manager_associations.xml";
113 private final CompanionDeviceManagerImpl mImpl;
114 private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>();
115 private IDeviceIdleController mIdleController;
116 private ServiceConnection mServiceConnection;
117 private IAppOpsService mAppOpsManager;
119 private IFindDeviceCallback mFindDeviceCallback;
120 private AssociationRequest mRequest;
121 private String mCallingPackage;
123 private final Object mLock = new Object();
125 public CompanionDeviceManagerService(Context context) {
127 mImpl = new CompanionDeviceManagerImpl();
128 mIdleController = IDeviceIdleController.Stub.asInterface(
129 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
130 mAppOpsManager = IAppOpsService.Stub.asInterface(
131 ServiceManager.getService(Context.APP_OPS_SERVICE));
132 registerPackageMonitor();
135 private void registerPackageMonitor() {
136 new PackageMonitor() {
138 public void onPackageRemoved(String packageName, int uid) {
140 as -> CollectionUtils.filter(as,
141 a -> !Objects.equals(a.companionAppPackage, packageName)),
142 getChangingUserId());
146 public void onPackageModified(String packageName) {
147 int userId = getChangingUserId();
148 if (!ArrayUtils.isEmpty(readAllAssociations(userId, packageName))) {
149 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
153 }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
157 public void onStart() {
158 publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
162 public void binderDied() {
163 Handler.getMain().post(this::cleanup);
166 private void cleanup() {
167 synchronized (mLock) {
168 mServiceConnection = unbind(mServiceConnection);
169 mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
171 mCallingPackage = null;
176 * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
180 private static <T extends IInterface> T unlinkToDeath(T iinterface,
181 IBinder.DeathRecipient deathRecipient, int flags) {
182 if (iinterface != null) {
183 iinterface.asBinder().unlinkToDeath(deathRecipient, flags);
190 private ServiceConnection unbind(@Nullable ServiceConnection conn) {
192 getContext().unbindService(conn);
197 class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
200 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
201 throws RemoteException {
203 return super.onTransact(code, data, reply, flags);
204 } catch (Throwable e) {
205 Slog.e(LOG_TAG, "Error during IPC", e);
206 throw ExceptionUtils.propagate(e, RemoteException.class);
211 public void associate(
212 AssociationRequest request,
213 IFindDeviceCallback callback,
214 String callingPackage) throws RemoteException {
216 Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
217 + ", callingPackage = " + callingPackage + ")");
219 checkNotNull(request, "Request cannot be null");
220 checkNotNull(callback, "Callback cannot be null");
221 checkCallerIsSystemOr(callingPackage);
222 int userId = getCallingUserId();
223 checkUsesFeature(callingPackage, userId);
224 final long callingIdentity = Binder.clearCallingIdentity();
226 getContext().bindServiceAsUser(
227 new Intent().setComponent(SERVICE_TO_BIND_TO),
228 createServiceConnection(request, callback, callingPackage),
229 Context.BIND_AUTO_CREATE,
230 UserHandle.of(userId));
232 Binder.restoreCallingIdentity(callingIdentity);
237 public void stopScan(AssociationRequest request,
238 IFindDeviceCallback callback,
239 String callingPackage) {
240 if (Objects.equals(request, mRequest)
241 && Objects.equals(callback, mFindDeviceCallback)
242 && Objects.equals(callingPackage, mCallingPackage)) {
248 public List<String> getAssociations(String callingPackage, int userId)
249 throws RemoteException {
250 checkCallerIsSystemOr(callingPackage, userId);
251 checkUsesFeature(callingPackage, getCallingUserId());
252 return new ArrayList<>(CollectionUtils.map(
253 readAllAssociations(userId, callingPackage),
254 a -> a.deviceAddress));
257 //TODO also revoke notification access
259 public void disassociate(String deviceMacAddress, String callingPackage)
260 throws RemoteException {
261 checkNotNull(deviceMacAddress);
262 checkCallerIsSystemOr(callingPackage);
263 checkUsesFeature(callingPackage, getCallingUserId());
264 removeAssociation(getCallingUserId(), callingPackage, deviceMacAddress);
267 private void checkCallerIsSystemOr(String pkg) throws RemoteException {
268 checkCallerIsSystemOr(pkg, getCallingUserId());
271 private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
272 if (isCallerSystem()) {
276 checkArgument(getCallingUserId() == userId,
277 "Must be called by either same user or system");
278 mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);
282 public PendingIntent requestNotificationAccess(ComponentName component)
283 throws RemoteException {
284 String callingPackage = component.getPackageName();
285 checkCanCallNotificationApi(callingPackage);
286 int userId = getCallingUserId();
287 String packageTitle = BidiFormatter.getInstance().unicodeWrap(
288 getPackageInfo(callingPackage, userId)
290 .loadSafeLabel(getContext().getPackageManager())
292 long identity = Binder.clearCallingIdentity();
294 return PendingIntent.getActivity(getContext(),
295 0 /* request code */,
296 NotificationAccessConfirmationActivityContract.launcherIntent(
297 userId, component, packageTitle),
298 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
299 | PendingIntent.FLAG_CANCEL_CURRENT);
301 Binder.restoreCallingIdentity(identity);
306 public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
307 checkCanCallNotificationApi(component.getPackageName());
308 String setting = Settings.Secure.getString(getContext().getContentResolver(),
309 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
310 return new ComponentNameSet(setting).contains(component);
313 private void checkCanCallNotificationApi(String callingPackage) throws RemoteException {
314 checkCallerIsSystemOr(callingPackage);
315 int userId = getCallingUserId();
316 checkState(!ArrayUtils.isEmpty(readAllAssociations(userId, callingPackage)),
317 "App must have an association before calling this API");
318 checkUsesFeature(callingPackage, userId);
321 private void checkUsesFeature(String pkg, int userId) {
322 if (isCallerSystem()) {
323 // Drop the requirement for calls from system process
327 FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
328 String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
329 int numFeatures = ArrayUtils.size(reqFeatures);
330 for (int i = 0; i < numFeatures; i++) {
331 if (requiredFeature.equals(reqFeatures[i].name)) return;
333 throw new IllegalStateException("Must declare uses-feature "
335 + " in manifest to use this API");
339 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
340 String[] args, ShellCallback callback, ResultReceiver resultReceiver)
341 throws RemoteException {
342 new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver);
346 private static int getCallingUserId() {
347 return UserHandle.getUserId(Binder.getCallingUid());
350 private static boolean isCallerSystem() {
351 return Binder.getCallingUid() == Process.SYSTEM_UID;
354 private ServiceConnection createServiceConnection(
355 final AssociationRequest request,
356 final IFindDeviceCallback findDeviceCallback,
357 final String callingPackage) {
358 mServiceConnection = new ServiceConnection() {
360 public void onServiceConnected(ComponentName name, IBinder service) {
363 "onServiceConnected(name = " + name + ", service = "
367 mFindDeviceCallback = findDeviceCallback;
369 mCallingPackage = callingPackage;
372 mFindDeviceCallback.asBinder().linkToDeath(
373 CompanionDeviceManagerService.this, 0);
374 } catch (RemoteException e) {
380 ICompanionDeviceDiscoveryService.Stub
381 .asInterface(service)
386 getServiceCallback());
387 } catch (RemoteException e) {
388 Log.e(LOG_TAG, "Error while initiating device discovery", e);
393 public void onServiceDisconnected(ComponentName name) {
394 if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")");
397 return mServiceConnection;
400 private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() {
401 return new ICompanionDeviceDiscoveryServiceCallback.Stub() {
404 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
405 throws RemoteException {
407 return super.onTransact(code, data, reply, flags);
408 } catch (Throwable e) {
409 Slog.e(LOG_TAG, "Error during IPC", e);
410 throw ExceptionUtils.propagate(e, RemoteException.class);
415 public void onDeviceSelected(String packageName, int userId, String deviceAddress) {
416 addAssociation(userId, packageName, deviceAddress);
421 public void onDeviceSelectionCancel() {
427 void addAssociation(int userId, String packageName, String deviceAddress) {
428 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
429 recordAssociation(packageName, deviceAddress);
432 void removeAssociation(int userId, String pkg, String deviceMacAddress) {
433 updateAssociations(associations -> CollectionUtils.remove(associations,
434 new Association(userId, deviceMacAddress, pkg)));
437 private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) {
438 PackageInfo packageInfo = getPackageInfo(packageName, userId);
439 if (packageInfo == null) {
443 Binder.withCleanCallingIdentity(() -> {
445 if (containsEither(packageInfo.requestedPermissions,
446 Manifest.permission.RUN_IN_BACKGROUND,
447 Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
448 mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
450 mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
452 } catch (RemoteException e) {
453 /* ignore - local call */
456 NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
457 if (containsEither(packageInfo.requestedPermissions,
458 Manifest.permission.USE_DATA_IN_BACKGROUND,
459 Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
460 networkPolicyManager.addUidPolicy(
461 packageInfo.applicationInfo.uid,
462 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
464 networkPolicyManager.removeUidPolicy(
465 packageInfo.applicationInfo.uid,
466 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
471 private static <T> boolean containsEither(T[] array, T a, T b) {
472 return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
476 private PackageInfo getPackageInfo(String packageName, int userId) {
477 return Binder.withCleanCallingIdentity(() -> {
479 return getContext().getPackageManager().getPackageInfoAsUser(
481 PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
483 } catch (PackageManager.NameNotFoundException e) {
484 Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + packageName, e);
490 private void recordAssociation(String priviledgedPackage, String deviceAddress) {
492 Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage
493 + ", deviceAddress = " + deviceAddress + ")");
495 int userId = getCallingUserId();
496 updateAssociations(associations -> CollectionUtils.add(associations,
497 new Association(userId, deviceAddress, priviledgedPackage)));
500 private void updateAssociations(Function<Set<Association>, Set<Association>> update) {
501 updateAssociations(update, getCallingUserId());
504 private void updateAssociations(Function<Set<Association>, Set<Association>> update,
506 final AtomicFile file = getStorageFileForUser(userId);
507 synchronized (file) {
508 Set<Association> associations = readAllAssociations(userId);
509 final Set<Association> old = CollectionUtils.copyOf(associations);
510 associations = update.apply(associations);
511 if (size(old) == size(associations)) return;
513 Set<Association> finalAssociations = associations;
514 file.write((out) -> {
515 XmlSerializer xml = Xml.newSerializer();
517 xml.setOutput(out, StandardCharsets.UTF_8.name());
518 xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
519 xml.startDocument(null, true);
520 xml.startTag(null, XML_TAG_ASSOCIATIONS);
522 CollectionUtils.forEach(finalAssociations, association -> {
523 xml.startTag(null, XML_TAG_ASSOCIATION)
524 .attribute(null, XML_ATTR_PACKAGE, association.companionAppPackage)
525 .attribute(null, XML_ATTR_DEVICE, association.deviceAddress)
526 .endTag(null, XML_TAG_ASSOCIATION);
529 xml.endTag(null, XML_TAG_ASSOCIATIONS);
531 } catch (Exception e) {
532 Slog.e(LOG_TAG, "Error while writing associations file", e);
533 throw ExceptionUtils.propagate(e);
540 private AtomicFile getStorageFileForUser(int uid) {
541 return mUidToStorage.computeIfAbsent(uid, (u) ->
542 new AtomicFile(new File(
543 //TODO deprecated method - what's the right replacement?
544 Environment.getUserSystemDirectory(u),
549 private Set<Association> readAllAssociations(int userId) {
550 return readAllAssociations(userId, null);
554 private Set<Association> readAllAssociations(int userId, @Nullable String packageFilter) {
555 final AtomicFile file = getStorageFileForUser(userId);
557 if (!file.getBaseFile().exists()) return null;
559 ArraySet<Association> result = null;
560 final XmlPullParser parser = Xml.newPullParser();
561 synchronized (file) {
562 try (FileInputStream in = file.openRead()) {
563 parser.setInput(in, StandardCharsets.UTF_8.name());
565 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
566 if (type != XmlPullParser.START_TAG
567 && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue;
569 final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE);
570 final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE);
572 if (appPackage == null || deviceAddress == null) continue;
573 if (packageFilter != null && !packageFilter.equals(appPackage)) continue;
575 result = ArrayUtils.add(result,
576 new Association(userId, deviceAddress, appPackage));
579 } catch (XmlPullParserException | IOException e) {
580 Slog.e(LOG_TAG, "Error while reading associations file", e);
588 private class Association {
589 public final int uid;
590 public final String deviceAddress;
591 public final String companionAppPackage;
593 private Association(int uid, String deviceAddress, String companionAppPackage) {
595 this.deviceAddress = checkNotNull(deviceAddress);
596 this.companionAppPackage = checkNotNull(companionAppPackage);
600 public boolean equals(Object o) {
601 if (this == o) return true;
602 if (o == null || getClass() != o.getClass()) return false;
604 Association that = (Association) o;
606 if (uid != that.uid) return false;
607 if (!deviceAddress.equals(that.deviceAddress)) return false;
608 return companionAppPackage.equals(that.companionAppPackage);
613 public int hashCode() {
615 result = 31 * result + deviceAddress.hashCode();
616 result = 31 * result + companionAppPackage.hashCode();
621 private class ShellCmd extends ShellCommand {
622 public static final String USAGE = "help\n"
624 + "associate USER_ID PACKAGE MAC_ADDRESS\n"
625 + "disassociate USER_ID PACKAGE MAC_ADDRESS";
628 getContext().enforceCallingOrSelfPermission(
629 android.Manifest.permission.MANAGE_COMPANION_DEVICES, "ShellCmd");
633 public int onCommand(String cmd) {
636 CollectionUtils.forEach(
637 readAllAssociations(getNextArgInt()),
638 a -> getOutPrintWriter()
639 .println(a.companionAppPackage + " " + a.deviceAddress));
643 addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
646 case "disassociate": {
647 removeAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
650 default: return handleDefaultCommands(cmd);
655 private int getNextArgInt() {
656 return Integer.parseInt(getNextArgRequired());
660 public void onHelp() {
661 getOutPrintWriter().println(USAGE);