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.RemoteException;
51 import android.os.ResultReceiver;
52 import android.os.ServiceManager;
53 import android.os.ShellCallback;
54 import android.os.ShellCommand;
55 import android.os.UserHandle;
56 import android.provider.Settings;
57 import android.provider.SettingsStringUtil.ComponentNameSet;
58 import android.text.BidiFormatter;
59 import android.util.AtomicFile;
60 import android.util.ExceptionUtils;
61 import android.util.Log;
62 import android.util.Slog;
63 import android.util.Xml;
65 import com.android.internal.app.IAppOpsService;
66 import com.android.internal.content.PackageMonitor;
67 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
68 import com.android.internal.util.ArrayUtils;
69 import com.android.internal.util.CollectionUtils;
70 import com.android.server.FgThread;
71 import com.android.server.SystemService;
73 import org.xmlpull.v1.XmlPullParser;
74 import org.xmlpull.v1.XmlPullParserException;
75 import org.xmlpull.v1.XmlSerializer;
78 import java.io.FileDescriptor;
79 import java.io.FileInputStream;
80 import java.io.IOException;
81 import java.nio.charset.StandardCharsets;
82 import java.util.ArrayList;
83 import java.util.List;
84 import java.util.Objects;
85 import java.util.concurrent.ConcurrentHashMap;
86 import java.util.concurrent.ConcurrentMap;
87 import java.util.function.Function;
89 //TODO onStop schedule unbind in 5 seconds
90 //TODO make sure APIs are only callable from currently focused app
91 //TODO schedule stopScan on activity destroy(except if configuration change)
92 //TODO on associate called again after configuration change -> replace old callback with new
93 //TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example)
95 public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
97 private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
98 CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
99 ".DeviceDiscoveryService");
101 private static final boolean DEBUG = false;
102 private static final String LOG_TAG = "CompanionDeviceManagerService";
104 private static final String XML_TAG_ASSOCIATIONS = "associations";
105 private static final String XML_TAG_ASSOCIATION = "association";
106 private static final String XML_ATTR_PACKAGE = "package";
107 private static final String XML_ATTR_DEVICE = "device";
108 private static final String XML_FILE_NAME = "companion_device_manager_associations.xml";
110 private final CompanionDeviceManagerImpl mImpl;
111 private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>();
112 private IDeviceIdleController mIdleController;
113 private IFindDeviceCallback mFindDeviceCallback;
114 private ServiceConnection mServiceConnection;
115 private IAppOpsService mAppOpsManager;
117 public CompanionDeviceManagerService(Context context) {
119 mImpl = new CompanionDeviceManagerImpl();
120 mIdleController = IDeviceIdleController.Stub.asInterface(
121 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
122 mAppOpsManager = IAppOpsService.Stub.asInterface(
123 ServiceManager.getService(Context.APP_OPS_SERVICE));
124 registerPackageMonitor();
127 private void registerPackageMonitor() {
128 new PackageMonitor() {
130 public void onPackageRemoved(String packageName, int uid) {
132 as -> CollectionUtils.filter(as,
133 a -> !Objects.equals(a.companionAppPackage, packageName)),
134 getChangingUserId());
138 public void onPackageModified(String packageName) {
139 int userId = getChangingUserId();
140 if (!ArrayUtils.isEmpty(readAllAssociations(userId, packageName))) {
141 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
145 }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
149 public void onStart() {
150 publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
154 public void binderDied() {
155 Handler.getMain().post(this::cleanup);
158 private void cleanup() {
159 mServiceConnection = unbind(mServiceConnection);
160 mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
164 * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
168 private static <T extends IInterface> T unlinkToDeath(T iinterface,
169 IBinder.DeathRecipient deathRecipient, int flags) {
170 if (iinterface != null) {
171 iinterface.asBinder().unlinkToDeath(deathRecipient, flags);
178 private ServiceConnection unbind(@Nullable ServiceConnection conn) {
180 getContext().unbindService(conn);
185 class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
188 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
189 throws RemoteException {
191 return super.onTransact(code, data, reply, flags);
192 } catch (Throwable e) {
193 Slog.e(LOG_TAG, "Error during IPC", e);
194 throw ExceptionUtils.propagate(e, RemoteException.class);
199 public void associate(
200 AssociationRequest request,
201 IFindDeviceCallback callback,
202 String callingPackage) throws RemoteException {
204 Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
205 + ", callingPackage = " + callingPackage + ")");
207 checkNotNull(request, "Request cannot be null");
208 checkNotNull(callback, "Callback cannot be null");
209 checkCallerIsSystemOr(callingPackage);
210 int userId = getCallingUserId();
211 checkUsesFeature(callingPackage, userId);
212 final long callingIdentity = Binder.clearCallingIdentity();
214 getContext().bindServiceAsUser(
215 new Intent().setComponent(SERVICE_TO_BIND_TO),
216 createServiceConnection(request, callback, callingPackage),
217 Context.BIND_AUTO_CREATE,
218 UserHandle.of(userId));
220 Binder.restoreCallingIdentity(callingIdentity);
225 public List<String> getAssociations(String callingPackage, int userId)
226 throws RemoteException {
227 checkCallerIsSystemOr(callingPackage, userId);
228 checkUsesFeature(callingPackage, getCallingUserId());
229 return CollectionUtils.map(
230 readAllAssociations(userId, callingPackage),
231 a -> a.deviceAddress);
234 //TODO also revoke notification access
236 public void disassociate(String deviceMacAddress, String callingPackage)
237 throws RemoteException {
238 checkNotNull(deviceMacAddress);
239 checkCallerIsSystemOr(callingPackage);
240 checkUsesFeature(callingPackage, getCallingUserId());
241 removeAssociation(getCallingUserId(), callingPackage, deviceMacAddress);
244 private void checkCallerIsSystemOr(String pkg) throws RemoteException {
245 checkCallerIsSystemOr(pkg, getCallingUserId());
248 private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
249 if (isCallerSystem()) {
253 checkArgument(getCallingUserId() == userId,
254 "Must be called by either same user or system");
255 mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);
259 public PendingIntent requestNotificationAccess(ComponentName component)
260 throws RemoteException {
261 String callingPackage = component.getPackageName();
262 checkCanCallNotificationApi(callingPackage);
263 int userId = getCallingUserId();
264 String packageTitle = BidiFormatter.getInstance().unicodeWrap(
265 getPackageInfo(callingPackage, userId)
267 .loadSafeLabel(getContext().getPackageManager())
269 long identity = Binder.clearCallingIdentity();
271 return PendingIntent.getActivity(getContext(),
272 0 /* request code */,
273 NotificationAccessConfirmationActivityContract.launcherIntent(
274 userId, component, packageTitle),
275 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
276 | PendingIntent.FLAG_CANCEL_CURRENT);
278 Binder.restoreCallingIdentity(identity);
283 public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
284 checkCanCallNotificationApi(component.getPackageName());
285 String setting = Settings.Secure.getString(getContext().getContentResolver(),
286 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
287 return new ComponentNameSet(setting).contains(component);
290 private void checkCanCallNotificationApi(String callingPackage) throws RemoteException {
291 checkCallerIsSystemOr(callingPackage);
292 int userId = getCallingUserId();
293 checkState(!ArrayUtils.isEmpty(readAllAssociations(userId, callingPackage)),
294 "App must have an association before calling this API");
295 checkUsesFeature(callingPackage, userId);
298 private void checkUsesFeature(String pkg, int userId) {
299 if (isCallerSystem()) {
300 // Drop the requirement for calls from system process
304 FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
305 String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
306 int numFeatures = ArrayUtils.size(reqFeatures);
307 for (int i = 0; i < numFeatures; i++) {
308 if (requiredFeature.equals(reqFeatures[i].name)) return;
310 throw new IllegalStateException("Must declare uses-feature "
312 + " in manifest to use this API");
316 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
317 String[] args, ShellCallback callback, ResultReceiver resultReceiver)
318 throws RemoteException {
319 new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver);
323 private static int getCallingUserId() {
324 return UserHandle.getUserId(Binder.getCallingUid());
327 private static boolean isCallerSystem() {
328 return getCallingUserId() == UserHandle.USER_SYSTEM;
331 private ServiceConnection createServiceConnection(
332 final AssociationRequest request,
333 final IFindDeviceCallback findDeviceCallback,
334 final String callingPackage) {
335 mServiceConnection = new ServiceConnection() {
337 public void onServiceConnected(ComponentName name, IBinder service) {
340 "onServiceConnected(name = " + name + ", service = "
343 mFindDeviceCallback = findDeviceCallback;
345 mFindDeviceCallback.asBinder().linkToDeath(
346 CompanionDeviceManagerService.this, 0);
347 } catch (RemoteException e) {
352 ICompanionDeviceDiscoveryService.Stub
353 .asInterface(service)
358 getServiceCallback());
359 } catch (RemoteException e) {
360 throw new RuntimeException(e);
365 public void onServiceDisconnected(ComponentName name) {
366 if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")");
369 return mServiceConnection;
372 private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() {
373 return new ICompanionDeviceDiscoveryServiceCallback.Stub() {
376 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
377 throws RemoteException {
379 return super.onTransact(code, data, reply, flags);
380 } catch (Throwable e) {
381 Slog.e(LOG_TAG, "Error during IPC", e);
382 throw ExceptionUtils.propagate(e, RemoteException.class);
387 public void onDeviceSelected(String packageName, int userId, String deviceAddress) {
388 addAssociation(userId, packageName, deviceAddress);
393 public void onDeviceSelectionCancel() {
399 void addAssociation(int userId, String packageName, String deviceAddress) {
400 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
401 recordAssociation(packageName, deviceAddress);
404 void removeAssociation(int userId, String pkg, String deviceMacAddress) {
405 updateAssociations(associations -> CollectionUtils.remove(associations,
406 new Association(userId, deviceMacAddress, pkg)));
409 private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) {
410 PackageInfo packageInfo = getPackageInfo(packageName, userId);
411 if (packageInfo == null) {
415 Binder.withCleanCallingIdentity(() -> {
417 if (containsEither(packageInfo.requestedPermissions,
418 Manifest.permission.RUN_IN_BACKGROUND,
419 Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
420 mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
422 mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
424 } catch (RemoteException e) {
425 /* ignore - local call */
428 NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
429 if (containsEither(packageInfo.requestedPermissions,
430 Manifest.permission.USE_DATA_IN_BACKGROUND,
431 Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
432 networkPolicyManager.addUidPolicy(
433 packageInfo.applicationInfo.uid,
434 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
436 networkPolicyManager.removeUidPolicy(
437 packageInfo.applicationInfo.uid,
438 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
443 private static <T> boolean containsEither(T[] array, T a, T b) {
444 return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
448 private PackageInfo getPackageInfo(String packageName, int userId) {
449 return Binder.withCleanCallingIdentity(() -> {
451 return getContext().getPackageManager().getPackageInfoAsUser(
453 PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
455 } catch (PackageManager.NameNotFoundException e) {
456 Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + packageName, e);
462 private void recordAssociation(String priviledgedPackage, String deviceAddress) {
464 Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage
465 + ", deviceAddress = " + deviceAddress + ")");
467 int userId = getCallingUserId();
468 updateAssociations(associations -> CollectionUtils.add(associations,
469 new Association(userId, deviceAddress, priviledgedPackage)));
472 private void updateAssociations(Function<List<Association>, List<Association>> update) {
473 updateAssociations(update, getCallingUserId());
476 private void updateAssociations(Function<List<Association>, List<Association>> update,
478 final AtomicFile file = getStorageFileForUser(userId);
479 synchronized (file) {
480 List<Association> associations = readAllAssociations(userId);
481 final List<Association> old = CollectionUtils.copyOf(associations);
482 associations = update.apply(associations);
483 if (size(old) == size(associations)) return;
485 List<Association> finalAssociations = associations;
486 file.write((out) -> {
487 XmlSerializer xml = Xml.newSerializer();
489 xml.setOutput(out, StandardCharsets.UTF_8.name());
490 xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
491 xml.startDocument(null, true);
492 xml.startTag(null, XML_TAG_ASSOCIATIONS);
494 for (int i = 0; i < size(finalAssociations); i++) {
495 Association association = finalAssociations.get(i);
496 xml.startTag(null, XML_TAG_ASSOCIATION)
497 .attribute(null, XML_ATTR_PACKAGE, association.companionAppPackage)
498 .attribute(null, XML_ATTR_DEVICE, association.deviceAddress)
499 .endTag(null, XML_TAG_ASSOCIATION);
502 xml.endTag(null, XML_TAG_ASSOCIATIONS);
504 } catch (Exception e) {
505 Slog.e(LOG_TAG, "Error while writing associations file", e);
506 throw ExceptionUtils.propagate(e);
513 private AtomicFile getStorageFileForUser(int uid) {
514 return mUidToStorage.computeIfAbsent(uid, (u) ->
515 new AtomicFile(new File(
516 //TODO deprecated method - what's the right replacement?
517 Environment.getUserSystemDirectory(u),
522 private ArrayList<Association> readAllAssociations(int userId) {
523 return readAllAssociations(userId, null);
527 private ArrayList<Association> readAllAssociations(int userId, @Nullable String packageFilter) {
528 final AtomicFile file = getStorageFileForUser(userId);
530 if (!file.getBaseFile().exists()) return null;
532 ArrayList<Association> result = null;
533 final XmlPullParser parser = Xml.newPullParser();
534 synchronized (file) {
535 try (FileInputStream in = file.openRead()) {
536 parser.setInput(in, StandardCharsets.UTF_8.name());
538 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
539 if (type != XmlPullParser.START_TAG
540 && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue;
542 final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE);
543 final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE);
545 if (appPackage == null || deviceAddress == null) continue;
546 if (packageFilter != null && !packageFilter.equals(appPackage)) continue;
548 result = ArrayUtils.add(result,
549 new Association(userId, deviceAddress, appPackage));
552 } catch (XmlPullParserException | IOException e) {
553 Slog.e(LOG_TAG, "Error while reading associations file", e);
561 private class Association {
562 public final int uid;
563 public final String deviceAddress;
564 public final String companionAppPackage;
566 private Association(int uid, String deviceAddress, String companionAppPackage) {
568 this.deviceAddress = checkNotNull(deviceAddress);
569 this.companionAppPackage = checkNotNull(companionAppPackage);
573 public boolean equals(Object o) {
574 if (this == o) return true;
575 if (o == null || getClass() != o.getClass()) return false;
577 Association that = (Association) o;
579 if (uid != that.uid) return false;
580 if (!deviceAddress.equals(that.deviceAddress)) return false;
581 return companionAppPackage.equals(that.companionAppPackage);
586 public int hashCode() {
588 result = 31 * result + deviceAddress.hashCode();
589 result = 31 * result + companionAppPackage.hashCode();
594 private class ShellCmd extends ShellCommand {
595 public static final String USAGE = "help\n"
597 + "associate USER_ID PACKAGE MAC_ADDRESS\n"
598 + "disassociate USER_ID PACKAGE MAC_ADDRESS";
601 public int onCommand(String cmd) {
604 ArrayList<Association> associations = readAllAssociations(getNextArgInt());
605 for (int i = 0; i < size(associations); i++) {
606 Association a = associations.get(i);
608 .println(a.companionAppPackage + " " + a.deviceAddress);
613 addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
616 case "disassociate": {
617 removeAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
620 default: return handleDefaultCommands(cmd);
625 private int getNextArgInt() {
626 return Integer.parseInt(getNextArgRequired());
630 public void onHelp() {
631 getOutPrintWriter().println(USAGE);