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