OSDN Git Service

Rename Companion permissions
[android-x86/frameworks-base.git] / services / companion / java / com / android / server / companion / CompanionDeviceManagerService.java
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
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;
64
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;
72
73 import org.xmlpull.v1.XmlPullParser;
74 import org.xmlpull.v1.XmlPullParserException;
75 import org.xmlpull.v1.XmlSerializer;
76
77 import java.io.File;
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;
88
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)
94 /** @hide */
95 public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
96
97     private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
98             CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
99             ".DeviceDiscoveryService");
100
101     private static final boolean DEBUG = false;
102     private static final String LOG_TAG = "CompanionDeviceManagerService";
103
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";
109
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;
116
117     public CompanionDeviceManagerService(Context context) {
118         super(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();
125     }
126
127     private void registerPackageMonitor() {
128         new PackageMonitor() {
129             @Override
130             public void onPackageRemoved(String packageName, int uid) {
131                 updateAssociations(
132                         as -> CollectionUtils.filter(as,
133                                 a -> !Objects.equals(a.companionAppPackage, packageName)),
134                         getChangingUserId());
135             }
136
137             @Override
138             public void onPackageModified(String packageName) {
139                 int userId = getChangingUserId();
140                 if (!ArrayUtils.isEmpty(readAllAssociations(userId, packageName))) {
141                     updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
142                 }
143             }
144
145         }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true);
146     }
147
148     @Override
149     public void onStart() {
150         publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl);
151     }
152
153     @Override
154     public void binderDied() {
155         Handler.getMain().post(this::cleanup);
156     }
157
158     private void cleanup() {
159         mServiceConnection = unbind(mServiceConnection);
160         mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
161     }
162
163     /**
164      * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
165      */
166     @Nullable
167     @CheckResult
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);
172         }
173         return null;
174     }
175
176     @Nullable
177     @CheckResult
178     private ServiceConnection unbind(@Nullable ServiceConnection conn) {
179         if (conn != null) {
180             getContext().unbindService(conn);
181         }
182         return null;
183     }
184
185     class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
186
187         @Override
188         public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
189                 throws RemoteException {
190             try {
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);
195             }
196         }
197
198         @Override
199         public void associate(
200                 AssociationRequest request,
201                 IFindDeviceCallback callback,
202                 String callingPackage) throws RemoteException {
203             if (DEBUG) {
204                 Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
205                         + ", callingPackage = " + callingPackage + ")");
206             }
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();
213             try {
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));
219             } finally {
220                 Binder.restoreCallingIdentity(callingIdentity);
221             }
222         }
223
224         @Override
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);
232         }
233
234         //TODO also revoke notification access
235         @Override
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);
242         }
243
244         private void checkCallerIsSystemOr(String pkg) throws RemoteException {
245             checkCallerIsSystemOr(pkg, getCallingUserId());
246         }
247
248         private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
249             if (isCallerSystem()) {
250                 return;
251             }
252
253             checkArgument(getCallingUserId() == userId,
254                     "Must be called by either same user or system");
255             mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);
256         }
257
258         @Override
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)
266                             .applicationInfo
267                             .loadSafeLabel(getContext().getPackageManager())
268                             .toString());
269             long identity = Binder.clearCallingIdentity();
270             try {
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);
277             } finally {
278                 Binder.restoreCallingIdentity(identity);
279             }
280         }
281
282         @Override
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);
288         }
289
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);
296         }
297
298         private void checkUsesFeature(String pkg, int userId) {
299             if (isCallerSystem()) {
300                 // Drop the requirement for calls from system process
301                 return;
302             }
303
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;
309             }
310             throw new IllegalStateException("Must declare uses-feature "
311                     + requiredFeature
312                     + " in manifest to use this API");
313         }
314
315         @Override
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);
320         }
321     }
322
323     private static int getCallingUserId() {
324         return UserHandle.getUserId(Binder.getCallingUid());
325     }
326
327     private static boolean isCallerSystem() {
328         return getCallingUserId() == UserHandle.USER_SYSTEM;
329     }
330
331     private ServiceConnection createServiceConnection(
332             final AssociationRequest request,
333             final IFindDeviceCallback findDeviceCallback,
334             final String callingPackage) {
335         mServiceConnection = new ServiceConnection() {
336             @Override
337             public void onServiceConnected(ComponentName name, IBinder service) {
338                 if (DEBUG) {
339                     Slog.i(LOG_TAG,
340                             "onServiceConnected(name = " + name + ", service = "
341                                     + service + ")");
342                 }
343                 mFindDeviceCallback = findDeviceCallback;
344                 try {
345                     mFindDeviceCallback.asBinder().linkToDeath(
346                             CompanionDeviceManagerService.this, 0);
347                 } catch (RemoteException e) {
348                     cleanup();
349                     return;
350                 }
351                 try {
352                     ICompanionDeviceDiscoveryService.Stub
353                             .asInterface(service)
354                             .startDiscovery(
355                                     request,
356                                     callingPackage,
357                                     findDeviceCallback,
358                                     getServiceCallback());
359                 } catch (RemoteException e) {
360                     throw new RuntimeException(e);
361                 }
362             }
363
364             @Override
365             public void onServiceDisconnected(ComponentName name) {
366                 if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")");
367             }
368         };
369         return mServiceConnection;
370     }
371
372     private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() {
373         return new ICompanionDeviceDiscoveryServiceCallback.Stub() {
374
375             @Override
376             public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
377                     throws RemoteException {
378                 try {
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);
383                 }
384             }
385
386             @Override
387             public void onDeviceSelected(String packageName, int userId, String deviceAddress) {
388                 addAssociation(userId, packageName, deviceAddress);
389                 cleanup();
390             }
391
392             @Override
393             public void onDeviceSelectionCancel() {
394                 cleanup();
395             }
396         };
397     }
398
399     void addAssociation(int userId, String packageName, String deviceAddress) {
400         updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
401         recordAssociation(packageName, deviceAddress);
402     }
403
404     void removeAssociation(int userId, String pkg, String deviceMacAddress) {
405         updateAssociations(associations -> CollectionUtils.remove(associations,
406                 new Association(userId, deviceMacAddress, pkg)));
407     }
408
409     private void updateSpecialAccessPermissionForAssociatedPackage(String packageName, int userId) {
410         PackageInfo packageInfo = getPackageInfo(packageName, userId);
411         if (packageInfo == null) {
412             return;
413         }
414
415         Binder.withCleanCallingIdentity(() -> {
416             try {
417                 if (containsEither(packageInfo.requestedPermissions,
418                         Manifest.permission.RUN_IN_BACKGROUND,
419                         Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
420                     mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
421                 } else {
422                     mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
423                 }
424             } catch (RemoteException e) {
425                 /* ignore - local call */
426             }
427
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);
435             } else {
436                 networkPolicyManager.removeUidPolicy(
437                         packageInfo.applicationInfo.uid,
438                         NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
439             }
440         });
441     }
442
443     private static <T> boolean containsEither(T[] array, T a, T b) {
444         return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
445     }
446
447     @Nullable
448     private PackageInfo getPackageInfo(String packageName, int userId) {
449         return Binder.withCleanCallingIdentity(() -> {
450             try {
451                 return getContext().getPackageManager().getPackageInfoAsUser(
452                         packageName,
453                         PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
454                         userId);
455             } catch (PackageManager.NameNotFoundException e) {
456                 Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + packageName, e);
457                 return null;
458             }
459         });
460     }
461
462     private void recordAssociation(String priviledgedPackage, String deviceAddress) {
463         if (DEBUG) {
464             Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage
465                     + ", deviceAddress = " + deviceAddress + ")");
466         }
467         int userId = getCallingUserId();
468         updateAssociations(associations -> CollectionUtils.add(associations,
469                 new Association(userId, deviceAddress, priviledgedPackage)));
470     }
471
472     private void updateAssociations(Function<List<Association>, List<Association>> update) {
473         updateAssociations(update, getCallingUserId());
474     }
475
476     private void updateAssociations(Function<List<Association>, List<Association>> update,
477             int userId) {
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;
484
485             List<Association> finalAssociations = associations;
486             file.write((out) -> {
487                 XmlSerializer xml = Xml.newSerializer();
488                 try {
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);
493
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);
500                     }
501
502                     xml.endTag(null, XML_TAG_ASSOCIATIONS);
503                     xml.endDocument();
504                 } catch (Exception e) {
505                     Slog.e(LOG_TAG, "Error while writing associations file", e);
506                     throw ExceptionUtils.propagate(e);
507                 }
508
509             });
510         }
511     }
512
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),
518                         XML_FILE_NAME)));
519     }
520
521     @Nullable
522     private ArrayList<Association> readAllAssociations(int userId) {
523         return readAllAssociations(userId, null);
524     }
525
526     @Nullable
527     private ArrayList<Association> readAllAssociations(int userId, @Nullable String packageFilter) {
528         final AtomicFile file = getStorageFileForUser(userId);
529
530         if (!file.getBaseFile().exists()) return null;
531
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());
537                 int type;
538                 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
539                     if (type != XmlPullParser.START_TAG
540                             && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue;
541
542                     final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE);
543                     final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE);
544
545                     if (appPackage == null || deviceAddress == null) continue;
546                     if (packageFilter != null && !packageFilter.equals(appPackage)) continue;
547
548                     result = ArrayUtils.add(result,
549                             new Association(userId, deviceAddress, appPackage));
550                 }
551                 return result;
552             } catch (XmlPullParserException | IOException e) {
553                 Slog.e(LOG_TAG, "Error while reading associations file", e);
554                 return null;
555             }
556         }
557     }
558
559
560
561     private class Association {
562         public final int uid;
563         public final String deviceAddress;
564         public final String companionAppPackage;
565
566         private Association(int uid, String deviceAddress, String companionAppPackage) {
567             this.uid = uid;
568             this.deviceAddress = checkNotNull(deviceAddress);
569             this.companionAppPackage = checkNotNull(companionAppPackage);
570         }
571
572         @Override
573         public boolean equals(Object o) {
574             if (this == o) return true;
575             if (o == null || getClass() != o.getClass()) return false;
576
577             Association that = (Association) o;
578
579             if (uid != that.uid) return false;
580             if (!deviceAddress.equals(that.deviceAddress)) return false;
581             return companionAppPackage.equals(that.companionAppPackage);
582
583         }
584
585         @Override
586         public int hashCode() {
587             int result = uid;
588             result = 31 * result + deviceAddress.hashCode();
589             result = 31 * result + companionAppPackage.hashCode();
590             return result;
591         }
592     }
593
594     private class ShellCmd extends ShellCommand {
595         public static final String USAGE = "help\n"
596                 + "list USER_ID\n"
597                 + "associate USER_ID PACKAGE MAC_ADDRESS\n"
598                 + "disassociate USER_ID PACKAGE MAC_ADDRESS";
599
600         @Override
601         public int onCommand(String cmd) {
602             switch (cmd) {
603                 case "list": {
604                     ArrayList<Association> associations = readAllAssociations(getNextArgInt());
605                     for (int i = 0; i < size(associations); i++) {
606                         Association a = associations.get(i);
607                         getOutPrintWriter()
608                                 .println(a.companionAppPackage + " " + a.deviceAddress);
609                     }
610                 } break;
611
612                 case "associate": {
613                     addAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
614                 } break;
615
616                 case "disassociate": {
617                     removeAssociation(getNextArgInt(), getNextArgRequired(), getNextArgRequired());
618                 } break;
619
620                 default: return handleDefaultCommands(cmd);
621             }
622             return 0;
623         }
624
625         private int getNextArgInt() {
626             return Integer.parseInt(getNextArgRequired());
627         }
628
629         @Override
630         public void onHelp() {
631             getOutPrintWriter().println(USAGE);
632         }
633     }
634
635 }