Record app<->device association to xml file

Bug: 30932767
Test: Ensure file not exists -> query associations -> ensure result is empty list
  Associate device -> cat xml file -> ensure record appears as extected
  Disassociate device -> cat xml file -> ensure record is no longer present
Change-Id: Ibe456a6d9292e05e2391f5138e43fdaa37f87e1b
This commit is contained in:
Eugene Susla
2017-02-13 12:46:46 -08:00
parent 280a9cfaab
commit 47aafbe033
13 changed files with 300 additions and 24 deletions

View File

@@ -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";
}

View File

@@ -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";
}

View File

@@ -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";
}

View File

@@ -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

View File

@@ -20,6 +20,7 @@ import android.companion.AssociationRequest;
import android.companion.ICompanionDeviceDiscoveryServiceCallback;
import android.companion.IFindDeviceCallback;
/** @hide */
interface ICompanionDeviceDiscoveryService {
void startDiscovery(

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;
}
}
}