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