diff options
13 files changed, 300 insertions, 24 deletions
diff --git a/api/current.txt b/api/current.txt index d10a201a006d..d004b9a88686 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8046,6 +8046,8 @@ package android.companion { public final class CompanionDeviceManager { method public void associate(android.companion.AssociationRequest<?>, android.companion.CompanionDeviceManager.Callback, android.os.Handler); + method public void disassociate(java.lang.String); + method public java.util.List<java.lang.String> getAssociations(); field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE"; } diff --git a/api/system-current.txt b/api/system-current.txt index 189de25dd5ff..59e224bd7479 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -8546,6 +8546,8 @@ package android.companion { public final class CompanionDeviceManager { method public void associate(android.companion.AssociationRequest<?>, android.companion.CompanionDeviceManager.Callback, android.os.Handler); + method public void disassociate(java.lang.String); + method public java.util.List<java.lang.String> getAssociations(); field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE"; } diff --git a/api/test-current.txt b/api/test-current.txt index b7ff239b98bc..4375accf720b 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -8073,6 +8073,8 @@ package android.companion { public final class CompanionDeviceManager { method public void associate(android.companion.AssociationRequest<?>, android.companion.CompanionDeviceManager.Callback, android.os.Handler); + method public void disassociate(java.lang.String); + method public java.util.List<java.lang.String> getAssociations(); field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE"; } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index c0c1a4dd198b..6fa32b4b6944 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -26,6 +26,8 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import java.util.List; + /** * System level service for managing companion devices * @@ -102,6 +104,11 @@ public final class CompanionDeviceManager { * special capabilities have a negative effect on the device's battery and user's data * usage, therefore you should requested them when absolutely necessary.</p> * + * <p>You can call {@link #getAssociations} to get the list of currently associated + * devices, and {@link #disassociate} to remove an association. Consider doing so when the + * association is no longer relevant to avoid unnecessary battery and/or data drain resulting + * from special privileges that the association provides</p> + * * @param request specific details about this request * @param callback will be called once there's at least one device found for user to choose from * @param handler A handler to control which thread the callback will be delivered on, or null, @@ -119,6 +126,8 @@ public final class CompanionDeviceManager { try { mService.associate( request, + //TODO implicit pointer to outer class -> =null onDestroy + //TODO onStop if isFinishing -> stopScan new IFindDeviceCallback.Stub() { @Override public void onSuccess(PendingIntent launcher) { @@ -138,6 +147,38 @@ public final class CompanionDeviceManager { } } + /** + * @return a list of MAC addresses of devices that have been previously associated with the + * current app. You can use these with {@link #disassociate} + */ + @NonNull + public List<String> getAssociations() { + try { + return mService.getAssociations(mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove the association between this app and the device with the given mac address. + * + * <p>Any privileges provided via being associated with a given device will be revoked</p> + * + * <p>Consider doing so when the + * association is no longer relevant to avoid unnecessary battery and/or data drain resulting + * from special privileges that the association provides</p> + * + * @param deviceMacAddress the MAC address of device to disassociate from this app + */ + public void disassociate(@NonNull String deviceMacAddress) { + try { + mService.disassociate(deviceMacAddress, mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** @hide */ public void requestNotificationAccess() { //TODO implement diff --git a/core/java/android/companion/ICompanionDeviceDiscoveryService.aidl b/core/java/android/companion/ICompanionDeviceDiscoveryService.aidl index 4d779639888e..5398c3cea68d 100644 --- a/core/java/android/companion/ICompanionDeviceDiscoveryService.aidl +++ b/core/java/android/companion/ICompanionDeviceDiscoveryService.aidl @@ -20,6 +20,7 @@ import android.companion.AssociationRequest; import android.companion.ICompanionDeviceDiscoveryServiceCallback; import android.companion.IFindDeviceCallback; + /** @hide */ interface ICompanionDeviceDiscoveryService { void startDiscovery( diff --git a/core/java/android/companion/ICompanionDeviceDiscoveryServiceCallback.aidl b/core/java/android/companion/ICompanionDeviceDiscoveryServiceCallback.aidl index 7af708e94d97..6bbb58da9938 100644 --- a/core/java/android/companion/ICompanionDeviceDiscoveryServiceCallback.aidl +++ b/core/java/android/companion/ICompanionDeviceDiscoveryServiceCallback.aidl @@ -18,5 +18,5 @@ package android.companion; /** @hide */ interface ICompanionDeviceDiscoveryServiceCallback { - void onDeviceSelected(String packageName, int userId); + oneway void onDeviceSelected(String packageName, int userId, String deviceAddress); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 1d30ada25c62..495141d75a44 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -29,6 +29,9 @@ interface ICompanionDeviceManager { in IFindDeviceCallback callback, in String callingPackage); + List<String> getAssociations(String callingPackage); + void disassociate(String deviceMacAddress, String callingPackage); + //TODO add these // boolean haveNotificationAccess(String packageName); // oneway void requestNotificationAccess(String packageName); diff --git a/core/java/android/util/AtomicFile.java b/core/java/android/util/AtomicFile.java index 3aa3447a42d8..2f1abe9a4c8d 100644 --- a/core/java/android/util/AtomicFile.java +++ b/core/java/android/util/AtomicFile.java @@ -17,13 +17,15 @@ package android.util; import android.os.FileUtils; -import android.util.Log; + +import libcore.io.IoUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.util.function.Consumer; /** * Helper class for performing atomic operations on a file by creating a @@ -244,4 +246,19 @@ public class AtomicFile { stream.close(); } } + + /** @hide */ + public void write(Consumer<FileOutputStream> writeContent) { + FileOutputStream out = null; + try { + out = startWrite(); + writeContent.accept(out); + finishWrite(out); + } catch (Throwable t) { + failWrite(out); + throw ExceptionUtils.propagate(t); + } finally { + IoUtils.closeQuietly(out); + } + } } diff --git a/core/java/android/util/ExceptionUtils.java b/core/java/android/util/ExceptionUtils.java index da0b609dbd9b..87231e106ca3 100644 --- a/core/java/android/util/ExceptionUtils.java +++ b/core/java/android/util/ExceptionUtils.java @@ -16,8 +16,11 @@ package android.util; +import android.annotation.NonNull; import android.os.ParcelableException; +import com.android.internal.util.Preconditions; + import java.io.IOException; /** @@ -51,4 +54,11 @@ public class ExceptionUtils { public static String getCompleteMessage(Throwable t) { return getCompleteMessage(null, t); } + + public static RuntimeException propagate(@NonNull Throwable t) { + Preconditions.checkNotNull(t); + if (t instanceof Error) throw (Error)t; + if (t instanceof RuntimeException) throw (RuntimeException)t; + throw new RuntimeException(t); + } } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 2b1625432d52..be69d9f808e2 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.Function; /** * ArrayUtils contains some methods that you can call to find out @@ -474,12 +475,25 @@ public class ArrayUtils { } } + public static int size(@Nullable Collection<?> cur) { + return cur != null ? cur.size() : 0; + } + + public static @NonNull <I, O> List<O> map(@Nullable List<I> cur, + Function<? super I, ? extends O> f) { + if (cur == null || cur.isEmpty()) return Collections.emptyList(); + final ArrayList<O> result = new ArrayList<>(); + for (int i = 0; i < cur.size(); i++) { + result.add(f.apply(cur.get(i))); + } + return result; + } + /** * Returns the given list, or an immutable empty list if the provided list is null * * @see Collections#emptyList */ - public static @NonNull <T> List<T> emptyIfNull(@Nullable List<T> cur) { return cur == null ? Collections.emptyList() : cur; } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java index 25127efad502..12bab18c88c9 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java @@ -92,7 +92,7 @@ public class DeviceChooserActivity extends Activity { try { final PackageManager packageManager = getPackageManager(); return packageManager.getApplicationLabel( - packageManager.getApplicationInfo(getService().mCallingPackage, 0)); + packageManager.getApplicationInfo(getCallingPackage(), 0)); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } @@ -128,7 +128,7 @@ public class DeviceChooserActivity extends Activity { } protected void onPairTapped(BluetoothDevice selectedDevice) { - getService().onDeviceSelected(); + getService().onDeviceSelected(getCallingPackage(), selectedDevice.getAddress()); setResult(RESULT_OK, new Intent().putExtra(CompanionDeviceManager.EXTRA_DEVICE, selectedDevice)); finish(); diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java index 11c722d7676c..f0f910848943 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java @@ -33,6 +33,7 @@ import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.companion.AssociationRequest; import android.companion.BluetoothLEDeviceFilter; +import android.companion.CompanionDeviceManager; import android.companion.ICompanionDeviceDiscoveryService; import android.companion.ICompanionDeviceDiscoveryServiceCallback; import android.companion.IFindDeviceCallback; @@ -71,7 +72,6 @@ public class DeviceDiscoveryService extends Service { DevicesAdapter mDevicesAdapter; IFindDeviceCallback mFindCallback; ICompanionDeviceDiscoveryServiceCallback mServiceCallback; - String mCallingPackage; private final ICompanionDeviceDiscoveryService mBinder = new ICompanionDeviceDiscoveryService.Stub() { @@ -88,7 +88,6 @@ public class DeviceDiscoveryService extends Service { } mFindCallback = findCallback; mServiceCallback = serviceCallback; - mCallingPackage = callingPackage; DeviceDiscoveryService.this.startDiscovery(request); } }; @@ -174,14 +173,6 @@ public class DeviceDiscoveryService extends Service { return super.onUnbind(intent); } - public void onDeviceSelected() { - try { - mServiceCallback.onDeviceSelected(mCallingPackage, getUserId()); - } catch (RemoteException e) { - Log.e(LOG_TAG, "Error reporting selected device"); - } - } - private void stopScan() { if (DEBUG) Log.i(LOG_TAG, "stopScan() called"); mBluetoothAdapter.cancelDiscovery(); @@ -234,6 +225,17 @@ public class DeviceDiscoveryService extends Service { } } + void onDeviceSelected(String callingPackage, String deviceAddress) { + try { + mServiceCallback.onDeviceSelected( + //TODO is this the right userId? + callingPackage, getUserId(), deviceAddress); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Failed to record association: " + + callingPackage + " <-> " + deviceAddress); + } + } + class DevicesAdapter extends ArrayAdapter<BluetoothDevice> { private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth); diff --git a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java index 9ac8295e816d..ad64e4e6e64d 100644 --- a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java +++ b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java @@ -20,6 +20,7 @@ package com.android.server.print; import static com.android.internal.util.Preconditions.checkNotNull; import android.Manifest; +import android.annotation.Nullable; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; import android.companion.ICompanionDeviceDiscoveryService; @@ -34,16 +35,39 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.NetworkPolicyManager; import android.os.Binder; +import android.os.Environment; import android.os.IBinder; import android.os.IDeviceIdleController; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.AtomicFile; +import android.util.ExceptionUtils; import android.util.Slog; +import android.util.Xml; import com.android.internal.util.ArrayUtils; import com.android.server.SystemService; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + //TODO move to own package! +//TODO un/linkToDeath & onBinderDied - unbind +//TODO onStop schedule unbind in 5 seconds +//TODO Prune association on app uninstall /** @hide */ public class CompanionDeviceManagerService extends SystemService { @@ -54,7 +78,14 @@ public class CompanionDeviceManagerService extends SystemService { private static final boolean DEBUG = false; private static final String LOG_TAG = "CompanionDeviceManagerService"; + private static final String XML_TAG_ASSOCIATIONS = "associations"; + private static final String XML_TAG_ASSOCIATION = "association"; + private static final String XML_ATTR_PACKAGE = "package"; + private static final String XML_ATTR_DEVICE = "device"; + private static final String XML_FILE_NAME = "companion_device_manager_associations.xml"; + private final CompanionDeviceManagerImpl mImpl; + private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>(); public CompanionDeviceManagerService(Context context) { super(context); @@ -89,6 +120,24 @@ public class CompanionDeviceManagerService extends SystemService { Binder.restoreCallingIdentity(callingIdentity); } } + + + @Override + public List<String> getAssociations(String callingPackage) { + return ArrayUtils.map( + readAllAssociations(getUserId(), callingPackage), + (a) -> a.deviceAddress); + } + + @Override + public void disassociate(String deviceMacAddress, String callingPackage) { + updateAssociations((associations) -> ArrayUtils.remove(associations, + new Association(getUserId(), checkNotNull(deviceMacAddress), callingPackage))); + } + } + + private int getUserId() { + return UserHandle.getUserId(Binder.getCallingUid()); } private ServiceConnection getServiceConnection( @@ -125,9 +174,12 @@ public class CompanionDeviceManagerService extends SystemService { private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() { return new ICompanionDeviceDiscoveryServiceCallback.Stub() { + @Override - public void onDeviceSelected(String packageName, int userId) { + public void onDeviceSelected(String packageName, int userId, String deviceAddress) { + //TODO unbind grantSpecialAccessPermissionsIfNeeded(packageName, userId); + recordAssociation(packageName, deviceAddress); } }; } @@ -136,14 +188,14 @@ public class CompanionDeviceManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); final PackageInfo packageInfo; try { - packageInfo = getContext().getPackageManager().getPackageInfoAsUser( - packageName, PackageManager.GET_PERMISSIONS, userId); - } catch (PackageManager.NameNotFoundException e) { - Slog.e(LOG_TAG, "Error granting special access permissions to package:" - + packageName, e); - return; - } - try { + try { + packageInfo = getContext().getPackageManager().getPackageInfoAsUser( + packageName, PackageManager.GET_PERMISSIONS, userId); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(LOG_TAG, "Error granting special access permissions to package:" + + packageName, e); + return; + } if (ArrayUtils.contains(packageInfo.requestedPermissions, Manifest.permission.RUN_IN_BACKGROUND)) { IDeviceIdleController idleController = IDeviceIdleController.Stub.asInterface( @@ -164,4 +216,134 @@ public class CompanionDeviceManagerService extends SystemService { Binder.restoreCallingIdentity(identity); } } + + private void recordAssociation(String priviledgedPackage, String deviceAddress) { + updateAssociations((associations) -> ArrayUtils.add(associations, + new Association(getUserId(), deviceAddress, priviledgedPackage))); + } + + private void updateAssociations( + Function<ArrayList<Association>, ArrayList<Association>> update) { + final int userId = getUserId(); + final AtomicFile file = getStorageFileForUser(userId); + synchronized (file) { + final ArrayList<Association> old = readAllAssociations(userId); + final ArrayList<Association> associations = update.apply(old); + if (Objects.equals(old, associations)) return; + + file.write((out) -> { + XmlSerializer xml = Xml.newSerializer(); + try { + xml.setOutput(out, StandardCharsets.UTF_8.name()); + xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + xml.startDocument(null, true); + xml.startTag(null, XML_TAG_ASSOCIATIONS); + + for (int i = 0; i < ArrayUtils.size(associations); i++) { + Association association = associations.get(i); + xml.startTag(null, XML_TAG_ASSOCIATION) + .attribute(null, XML_ATTR_PACKAGE, association.companionAppPackage) + .attribute(null, XML_ATTR_DEVICE, association.deviceAddress) + .endTag(null, XML_TAG_ASSOCIATION); + } + + xml.endTag(null, XML_TAG_ASSOCIATIONS); + xml.endDocument(); + } catch (Exception e) { + Slog.e(LOG_TAG, "Error while writing associations file", e); + throw ExceptionUtils.propagate(e); + } + + }); + } + + + //TODO Show dialog before recording notification access +// final SettingStringHelper setting = +// new SettingStringHelper( +// getContext().getContentResolver(), +// Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, +// getUserId()); +// setting.write(ColonDelimitedSet.OfStrings.add(setting.read(), priviledgedPackage)); + } + + private AtomicFile getStorageFileForUser(int uid) { + return mUidToStorage.computeIfAbsent(uid, (u) -> + new AtomicFile(new File( + //TODO deprecated method - what's the right replacement? + Environment.getUserSystemDirectory(u), + XML_FILE_NAME))); + } + + @Nullable + private ArrayList<Association> readAllAssociations(int uid) { + return readAllAssociations(uid, null); + } + + @Nullable + private ArrayList<Association> readAllAssociations(int userId, @Nullable String packageFilter) { + final AtomicFile file = getStorageFileForUser(userId); + + if (!file.getBaseFile().exists()) return null; + + ArrayList<Association> result = null; + final XmlPullParser parser = Xml.newPullParser(); + synchronized (file) { + try (FileInputStream in = file.openRead()) { + parser.setInput(in, StandardCharsets.UTF_8.name()); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG + && !XML_TAG_ASSOCIATIONS.equals(parser.getName())) continue; + + final String appPackage = parser.getAttributeValue(null, XML_ATTR_PACKAGE); + final String deviceAddress = parser.getAttributeValue(null, XML_ATTR_DEVICE); + + if (appPackage == null || deviceAddress == null) continue; + if (packageFilter != null && !packageFilter.equals(appPackage)) continue; + + result = ArrayUtils.add(result, + new Association(userId, deviceAddress, appPackage)); + } + return result; + } catch (XmlPullParserException | IOException e) { + Slog.e(LOG_TAG, "Error while reading associations file", e); + return null; + } + } + } + + private class Association { + public final int uid; + public final String deviceAddress; + public final String companionAppPackage; + + private Association(int uid, String deviceAddress, String companionAppPackage) { + this.uid = uid; + this.deviceAddress = checkNotNull(deviceAddress); + this.companionAppPackage = checkNotNull(companionAppPackage); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Association that = (Association) o; + + if (uid != that.uid) return false; + if (!deviceAddress.equals(that.deviceAddress)) return false; + return companionAppPackage.equals(that.companionAppPackage); + + } + + @Override + public int hashCode() { + int result = uid; + result = 31 * result + deviceAddress.hashCode(); + result = 31 * result + companionAppPackage.hashCode(); + return result; + } + } + } |