summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt2
-rw-r--r--api/system-current.txt2
-rw-r--r--api/test-current.txt2
-rw-r--r--core/java/android/companion/CompanionDeviceManager.java41
-rw-r--r--core/java/android/companion/ICompanionDeviceDiscoveryService.aidl1
-rw-r--r--core/java/android/companion/ICompanionDeviceDiscoveryServiceCallback.aidl2
-rw-r--r--core/java/android/companion/ICompanionDeviceManager.aidl3
-rw-r--r--core/java/android/util/AtomicFile.java19
-rw-r--r--core/java/android/util/ExceptionUtils.java10
-rw-r--r--core/java/com/android/internal/util/ArrayUtils.java16
-rw-r--r--packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java4
-rw-r--r--packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java22
-rw-r--r--services/print/java/com/android/server/print/CompanionDeviceManagerService.java200
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;
+ }
+ }
+
}