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:
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.companion.AssociationRequest;
|
||||
import android.companion.ICompanionDeviceDiscoveryServiceCallback;
|
||||
import android.companion.IFindDeviceCallback;
|
||||
|
||||
|
||||
/** @hide */
|
||||
interface ICompanionDeviceDiscoveryService {
|
||||
void startDiscovery(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user