1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17 18package com.android.server.companion; 19 20import static com.android.internal.util.CollectionUtils.size; 21import static com.android.internal.util.Preconditions.checkArgument; 22import static com.android.internal.util.Preconditions.checkNotNull; 23import static com.android.internal.util.Preconditions.checkState; 24 25import android.Manifest; 26import android.annotation.CheckResult; 27import android.annotation.Nullable; 28import android.app.PendingIntent; 29import android.companion.AssociationRequest; 30import android.companion.CompanionDeviceManager; 31import android.companion.ICompanionDeviceDiscoveryService; 32import android.companion.ICompanionDeviceDiscoveryServiceCallback; 33import android.companion.ICompanionDeviceManager; 34import android.companion.IFindDeviceCallback; 35import android.content.ComponentName; 36import android.content.Context; 37import android.content.Intent; 38import android.content.ServiceConnection; 39import android.content.pm.FeatureInfo; 40import android.content.pm.PackageInfo; 41import android.content.pm.PackageManager; 42import android.net.NetworkPolicyManager; 43import android.os.Binder; 44import android.os.Environment; 45import android.os.Handler; 46import android.os.IBinder; 47import android.os.IDeviceIdleController; 48import android.os.IInterface; 49import android.os.Parcel; 50import android.os.Process; 51import android.os.RemoteException; 52import android.os.ResultReceiver; 53import android.os.ServiceManager; 54import android.os.ShellCallback; 55import android.os.ShellCommand; 56import android.os.UserHandle; 57import android.provider.Settings; 58import android.provider.SettingsStringUtil.ComponentNameSet; 59import android.text.BidiFormatter; 60import android.util.AtomicFile; 61import android.util.ExceptionUtils; 62import android.util.Log; 63import android.util.Slog; 64import android.util.Xml; 65 66import com.android.internal.app.IAppOpsService; 67import com.android.internal.content.PackageMonitor; 68import com.android.internal.notification.NotificationAccessConfirmationActivityContract; 69import com.android.internal.util.ArrayUtils; 70import com.android.internal.util.CollectionUtils; 71import com.android.server.FgThread; 72import com.android.server.SystemService; 73 74import org.xmlpull.v1.XmlPullParser; 75import org.xmlpull.v1.XmlPullParserException; 76import org.xmlpull.v1.XmlSerializer; 77 78import java.io.File; 79import java.io.FileDescriptor; 80import java.io.FileInputStream; 81import java.io.IOException; 82import java.nio.charset.StandardCharsets; 83import java.util.ArrayList; 84import java.util.List; 85import java.util.Objects; 86import java.util.concurrent.ConcurrentHashMap; 87import java.util.concurrent.ConcurrentMap; 88import java.util.function.Function; 89 90//TODO onStop schedule unbind in 5 seconds 91//TODO make sure APIs are only callable from currently focused app 92//TODO schedule stopScan on activity destroy(except if configuration change) 93//TODO on associate called again after configuration change -> replace old callback with new 94//TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example) 95/** @hide */ 96public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient { 97 98 private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative( 99 CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, 100 ".DeviceDiscoveryService"); 101 102 private static final boolean DEBUG = false; 103 private static final String LOG_TAG = "CompanionDeviceManagerService"; 104 105 private static final String XML_TAG_ASSOCIATIONS = "associations"; 106 private static final String XML_TAG_ASSOCIATION = "association"; 107 private static final String XML_ATTR_PACKAGE = "package"; 108 private static final String XML_ATTR_DEVICE = "device"; 109 private static final String XML_FILE_NAME = "companion_device_manager_associations.xml"; 110 111 private final CompanionDeviceManagerImpl mImpl; 112 private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>(); 113 private IDeviceIdleController mIdleController; 114 private ServiceConnection mServiceConnection; 115 private IAppOpsService mAppOpsManager; 116 117 private IFindDeviceCallback mFindDeviceCallback; 118 private AssociationRequest mRequest; 119 private String mCallingPackage; 120 121 private final Object mLock = new Object(); 122 123 public CompanionDeviceManagerService(Context context) { 124 super(context); 125 mImpl = new CompanionDeviceManagerImpl(); 126 mIdleController = IDeviceIdleController.Stub.asInterface( 127 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); 128 mAppOpsManager = IAppOpsService.Stub.asInterface( 129 ServiceManager.getService(Context.APP_OPS_SERVICE)); 130 registerPackageMonitor(); 131 } 132 133 private void registerPackageMonitor() { 134 new PackageMonitor() { 135 @Override 136 public void onPackageRemoved(String packageName, int uid) { 137 updateAssociations( 138 as -> CollectionUtils.filter(as, 139 a -> !Objects.equals(a.companionAppPackage, packageName)), 140 getChangingUserId()); 141 } 142 143 @Override 144 public void onPackageModified(String packageName) { 145 int userId = getChangingUserId(); 146 if (!ArrayUtils.isEmpty(readAllAssociations(userId, packageName))) { 147 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId); 148 } 149 } 150 151 }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true); 152 } 153 154 @Override 155 public void onStart() { 156 publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl); 157 } 158 159 @Override 160 public void binderDied() { 161 Handler.getMain().post(this::cleanup); 162 } 163 164 private void cleanup() { 165 synchronized (mLock) { 166 mServiceConnection = unbind(mServiceConnection); 167 mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0); 168 mRequest = null; 169 mCallingPackage = null; 170 } 171 } 172 173 /** 174 * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); } 175 */ 176 @Nullable 177 @CheckResult 178 private static <T extends IInterface> T unlinkToDeath(T iinterface, 179 IBinder.DeathRecipient deathRecipient, int flags) { 180 if (iinterface != null) { 181 iinterface.asBinder().unlinkToDeath(deathRecipient, flags); 182 } 183 return null; 184 } 185 186 @Nullable 187 @CheckResult 188 private ServiceConnection unbind(@Nullable ServiceConnection conn) { 189 if (conn != null) { 190 getContext().unbindService(conn); 191 } 192 return null; 193 } 194 195 class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { 196 197 @Override 198 public boolean onTransact(int code, Parcel data, Parcel reply, int flags) 199 throws RemoteException { 200 try { 201 return super.onTransact(code, data, reply, flags); 202 } catch (Throwable e) { 203 Slog.e(LOG_TAG, "Error during IPC", e); 204 throw ExceptionUtils.propagate(e, RemoteException.class); 205 } 206 } 207 208 @Override 209 public void associate( 210 AssociationRequest request, 211 IFindDeviceCallback callback, 212 String callingPackage) throws RemoteException { 213 if (DEBUG) { 214 Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback 215 + ", callingPackage = " + callingPackage + ")"); 216 } 217 checkNotNull(request, "Request cannot be null"); 218 checkNotNull(callback, "Callback cannot be null"); 219 checkCallerIsSystemOr(callingPackage); 220 int userId = getCallingUserId(); 221 checkUsesFeature(callingPackage, userId); 222 final long callingIdentity = Binder.clearCallingIdentity(); 223 try { 224 getContext().bindServiceAsUser( 225 new Intent().setComponent(SERVICE_TO_BIND_TO), 226 createServiceConnection(request, callback, callingPackage), 227 Context.BIND_AUTO_CREATE, 228 UserHandle.of(userId)); 229 } finally { 230 Binder.restoreCallingIdentity(callingIdentity); 231 } 232 } 233 234 @Override 235 public void stopScan(AssociationRequest request, 236 IFindDeviceCallback callback, 237 String callingPackage) { 238 if (Objects.equals(request, mRequest) 239 && Objects.equals(callback, mFindDeviceCallback) 240 && Objects.equals(callingPackage, mCallingPackage)) { 241 cleanup(); 242 } 243 } 244 245 @Override 246 public List<String> getAssociations(String callingPackage, int userId) 247 throws RemoteException { 248 checkCallerIsSystemOr(callingPackage, userId); 249 checkUsesFeature(callingPackage, getCallingUserId()); 250 return CollectionUtils.map( 251 readAllAssociations(userId, callingPackage), 252 a -> a.deviceAddress); 253 } 254 255 //TODO also revoke notification access 256 @Override 257 public void disassociate(String deviceMacAddress, String callingPackage) 258 throws RemoteException { 259 checkNotNull(deviceMacAddress); 260 checkCallerIsSystemOr(callingPackage); 261 checkUsesFeature(callingPackage, getCallingUserId()); 262 removeAssociation(getCallingUserId(), callingPackage, deviceMacAddress); 263 } 264 265 private void checkCallerIsSystemOr(String pkg) throws RemoteException { 266 checkCallerIsSystemOr(pkg, getCallingUserId()); 267 } 268 269 private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException { 270 if (isCallerSystem()) { 271 return; 272 } 273 274 checkArgument(getCallingUserId() == userId, 275 "Must be called by either same user or system"); 276 mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg); 277 } 278 279 @Override 280 public PendingIntent requestNotificationAccess(ComponentName component) 281 throws RemoteException { 282 String callingPackage = component.getPackageName(); 283 checkCanCallNotificationApi(callingPackage); 284 int userId = getCallingUserId(); 285 String packageTitle = BidiFormatter.getInstance().unicodeWrap( 286 getPackageInfo(callingPackage, userId) 287 .applicationInfo 288 .loadSafeLabel(getContext().getPackageManager()) 289 .toString()); 290 long identity = Binder.clearCallingIdentity(); 291 try { 292 return PendingIntent.getActivity(getContext(), 293 0 /* request code */, 294 NotificationAccessConfirmationActivityContract.launcherIntent( 295 userId, component, packageTitle), 296 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT 297 | PendingIntent.FLAG_CANCEL_CURRENT); 298 } finally { 299 Binder.restoreCallingIdentity(identity); 300 } 301 } 302 303 @Override 304 public boolean hasNotificationAccess(ComponentName component) throws RemoteException { 305 checkCanCallNotificationApi(component.getPackageName()); 306 String setting = Settings.Secure.getString(getContext().getContentResolver(), 307 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); 308 return new ComponentNameSet(setting).contains(component); 309 } 310 311 private void checkCanCallNotificationApi(String callingPackage) throws RemoteException { 312 checkCallerIsSystemOr(callingPackage); 313 int userId = getCallingUserId(); 314 checkState(!ArrayUtils.isEmpty(readAllAssociations(userId, callingPackage)), 315 "App must have an association before calling this API"); 316 checkUsesFeature(callingPackage, userId); 317 } 318 319 private void checkUsesFeature(String pkg, int userId) { 320 if (isCallerSystem()) { 321 // Drop the requirement for calls from system process 322 return; 323 } 324 325 FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures; 326 String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP; 327 int numFeatures = ArrayUtils.size(reqFeatures); 328 for (int i = 0; i < numFeatures; i++) { 329 if (requiredFeature.equals(reqFeatures[i].name)) return; 330 } 331 throw new IllegalStateException("Must declare uses-feature " 332 + requiredFeature 333 + " in manifest to use this API"); 334 } 335 336 @Override 337 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 338 String[] args, ShellCallback callback, ResultReceiver resultReceiver) 339 throws RemoteException { 340 new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver); 341 } 342 } 343 344 private static int getCallingUserId() { 345 return UserHandle.getUserId(Binder.getCallingUid()); 346 } 347 348 private static boolean isCallerSystem() { 349 return Binder.getCallingUid() == Process.SYSTEM_UID; 350 } 351 352 private ServiceConnection createServiceConnection( 353 final AssociationRequest request, 354 final IFindDeviceCallback findDeviceCallback, 355 final String callingPackage) { 356 mServiceConnection = new ServiceConnection() { 357 @Override 358 public void onServiceConnected(ComponentName name, IBinder service) { 359 if (DEBUG) { 360 Slog.i(LOG_TAG, 361 "onServiceConnected(name = " + name + ", service = " 362 + service + ")"); 363 } 364 365 mFindDeviceCallback = findDeviceCallback; 366 mRequest = request; 367 mCallingPackage = callingPackage; 368 369 try { 370 mFindDeviceCallback.asBinder().linkToDeath( 371 CompanionDeviceManagerService.this, 0); 372 } catch (RemoteException e) { 373 cleanup(); 374 return; 375 } 376 377 try { 378 ICompanionDeviceDiscoveryService.Stub 379 .asInterface(service) 380 .startDiscovery( 381 request, 382 callingPackage, 383 findDeviceCallback, 384 getServiceCallback()); 385 } catch (RemoteException e) { 386 Log.e(LOG_TAG, "Error while initiating device discovery", e); 387 } 388 } 389 390 @Override 391 public void onServiceDisconnected(ComponentName name) { 392 if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")"); 393 } 394 }; 395 return mServiceConnection; 396 } 397 398 private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() { 399 return new ICompanionDeviceDiscoveryServiceCallback.Stub() { 400 401 @Override 402 public boolean onTransact(int code, Parcel data, Parcel reply, int flags) 403 throws RemoteException { 404 try { 405 return super.onTransact(code, data, reply, flags); 406 } catch (Throwable e) { 407 Slog.e(LOG_TAG, "Error during IPC", e); 408 throw ExceptionUtils.propagate(e, RemoteException.class); 409 } 410 } 411 412 @Override 413 public void onDeviceSelected(String packageName, int userId, String deviceAddress) { 414 addAssociation(userId, packageName, deviceAddress); 415 cleanup(); 416 } 417 418 @Override 419 public void onDeviceSelectionCancel() { 420 cleanup(); 421 } 422 }; 423 } 424 425 void addAssociation(int userId, String packageName, String deviceAddress) { 426 updateSpecialAccessPermissionForAssociatedPackage(packageName, userId); 427 recordAssociation(packageName, deviceAddress); 428 } 429 430 void removeAssociation(int userId, String pkg, String deviceMacAddress) { 431 updateAssociations(associations -> CollectionUtils.remove(associations, 432 new Association(userId, deviceMacAddress, pkg))); 433 } 434 435 private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) { 436 PackageInfo packageInfo = getPackageInfo(packageName, userId); 437 if (packageInfo == null) { 438 return; 439 } 440 441 Binder.withCleanCallingIdentity(() -> { 442 try { 443 if (containsEither(packageInfo.requestedPermissions, 444 Manifest.permission.RUN_IN_BACKGROUND, 445 Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { 446 mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName); 447 } else { 448 mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName); 449 } 450 } catch (RemoteException e) { 451 /* ignore - local call */ 452 } 453 454 NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext()); 455 if (containsEither(packageInfo.requestedPermissions, 456 Manifest.permission.USE_DATA_IN_BACKGROUND, 457 Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) { 458 networkPolicyManager.addUidPolicy( 459 packageInfo.applicationInfo.uid, 460 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); 461 } else { 462 networkPolicyManager.removeUidPolicy( 463 packageInfo.applicationInfo.uid, 464 NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); 465 } 466 }); 467 } 468 469 private static <T> boolean containsEither(T[] array, T a, T b) { 470 return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b); 471 } 472 473 @Nullable 474 private PackageInfo getPackageInfo(String packageName, int userId) { 475 return Binder.withCleanCallingIdentity(() -> { 476 try { 477 return getContext().getPackageManager().getPackageInfoAsUser( 478 packageName, 479 PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS, 480 userId); 481 } catch (PackageManager.NameNotFoundException e) { 482 Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + packageName, e); 483 return null; 484 } 485 }); 486 } 487 488 private void recordAssociation(String priviledgedPackage, String deviceAddress) { 489 if (DEBUG) { 490 Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage 491 + ", deviceAddress = " + deviceAddress + ")"); 492 } 493 int userId = getCallingUserId(); 494 updateAssociations(associations -> CollectionUtils.add(associations, 495 new Association(userId, deviceAddress, priviledgedPackage))); 496 } 497 498 private void updateAssociations(Function<List<Association>, List<Association>> update) { 499 updateAssociations(update, getCallingUserId()); 500 } 501 502 private void updateAssociations(Function<List<Association>, List<Association>> update, 503 int userId) { 504 final AtomicFile file = getStorageFileForUser(userId); 505 synchronized (file) { 506 List<Association> associations = readAllAssociations(userId); 507 final List<Association> old = CollectionUtils.copyOf(associations); 508 associations = update.apply(associations); 509 if (size(old) == size(associations)) return; 510 511 List<Association> finalAssociations = associations; 512 file.write((out) -> { 513 XmlSerializer xml = Xml.newSerializer(); 514 try { 515 xml.setOutput(out, StandardCharsets.UTF_8.name()); 516 xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 517 xml.startDocument(null, true); 518 xml.startTag(null, XML_TAG_ASSOCIATIONS); 519 520 for (int i = 0; i < size(finalAssociations); i++) { 521 Association association = finalAssociations.get(i); 522 xml.startTag(null, XML_TAG_ASSOCIATION) 523 .attribute(null, XML_ATTR_PACKAGE, association.companionAppPackage) 524 .attribute(null, XML_ATTR_DEVICE, association.deviceAddress) 525 .endTag(null, XML_TAG_ASSOCIATION); 526 } 527 528 xml.endTag(null, XML_TAG_ASSOCIATIONS); 529 xml.endDocument(); 530 } catch (Exception e) { 531 Slog.e(LOG_TAG, "Error while writing associations file", e); 532 throw ExceptionUtils.propagate(e); 533 } 534 535 }); 536 } 537 } 538 539 private AtomicFile getStorageFileForUser(int uid) { 540 return mUidToStorage.computeIfAbsent(uid, (u) -> 541 new AtomicFile(new File( 542 //TODO deprecated method - what's the right replacement? 543 Environment.getUserSystemDirectory(u), 544 XML_FILE_NAME))); 545 } 546 547 @Nullable 548 private ArrayList<Association> readAllAssociations(int userId) { 549 return readAllAssociations(userId, null); 550 } 551 552 @Nullable 553 private ArrayList<Association> readAllAssociations(int userId, @Nullable String packageFilter) { 554 final AtomicFile file = getStorageFileForUser(userId); 555 556 if (!file.getBaseFile().exists()) return null; 557 558 ArrayList<Association> result = null; 559 final XmlPullParser parser = Xml.newPullParser(); 560 synchronized (file) { 561 try (FileInputStream in = file.openRead()) { 562 parser.setInput(in, StandardCharsets.UTF_8.name()); 563 int type; 564 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 565 if (type != XmlPullParser.START_TAG 566 && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue; 567 568 final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE); 569 final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE); 570 571 if (appPackage == null || deviceAddress == null) continue; 572 if (packageFilter != null && !packageFilter.equals(appPackage)) continue; 573 574 result = ArrayUtils.add(result, 575 new Association(userId, deviceAddress, appPackage)); 576 } 577 return result; 578 } catch (XmlPullParserException | IOException e) { 579 Slog.e(LOG_TAG, "Error while reading associations file", e); 580 return null; 581 } 582 } 583 } 584 585 586 587 private class Association { 588 public final int uid; 589 public final String deviceAddress; 590 public final String companionAppPackage; 591 592 private Association(int uid, String deviceAddress, String companionAppPackage) { 593 this.uid = uid; 594 this.deviceAddress = checkNotNull(deviceAddress); 595 this.companionAppPackage = checkNotNull(companionAppPackage); 596 } 597 598 @Override 599 public boolean equals(Object o) { 600 if (this == o) return true; 601 if (o == null || getClass() != o.getClass()) return false; 602 603 Association that = (Association) o; 604 605 if (uid != that.uid) return false; 606 if (!deviceAddress.equals(that.deviceAddress)) return false; 607 return companionAppPackage.equals(that.companionAppPackage); 608 609 } 610 611 @Override 612 public int hashCode() { 613 int result = uid; 614 result = 31 * result + deviceAddress.hashCode(); 615 result = 31 * result + companionAppPackage.hashCode(); 616 return result; 617 } 618 } 619 620 private class ShellCmd extends ShellCommand { 621 public static final String USAGE = "help\n" 622 + "list USER_ID\n" 623 + "associate USER_ID PACKAGE MAC_ADDRESS\n" 624 + "disassociate USER_ID PACKAGE MAC_ADDRESS"; 625 626 @Override 627 public int onCommand(String cmd) { 628 switch (cmd) { 629 case "list": { 630 ArrayList<Association> associations = readAllAssociations(getNextArgInt()); 631 for (int i = 0; i < size(associations); i++) { 632 Association a = associations.get(i); 633 getOutPrintWriter() 634 .println(a.companionAppPackage + " " + a.deviceAddress); 635 } 636 } break; 637 638 case "associate": { 639 addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired()); 640 } break; 641 642 case "disassociate": { 643 removeAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired()); 644 } break; 645 646 default: return handleDefaultCommands(cmd); 647 } 648 return 0; 649 } 650 651 private int getNextArgInt() { 652 return Integer.parseInt(getNextArgRequired()); 653 } 654 655 @Override 656 public void onHelp() { 657 getOutPrintWriter().println(USAGE); 658 } 659 } 660 661} 662