Merge "[MS11] Implement findL2Key"
am: 773f3f5bf3
Change-Id: If429108eddc802f0c13f7b10048b1fe87bd51b22
diff --git a/core/java/android/net/ipmemorystore/NetworkAttributes.java b/core/java/android/net/ipmemorystore/NetworkAttributes.java
index 5397b57..6a9eae0 100644
--- a/core/java/android/net/ipmemorystore/NetworkAttributes.java
+++ b/core/java/android/net/ipmemorystore/NetworkAttributes.java
@@ -252,6 +252,12 @@
}
}
+ /** @hide */
+ public boolean isEmpty() {
+ return (null == assignedV4Address) && (null == groupHint)
+ && (null == dnsAddresses) && (null == mtu);
+ }
+
@Override
public boolean equals(@Nullable final Object o) {
if (!(o instanceof NetworkAttributes)) return false;
diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java
index 32513c1..e99dd4f 100644
--- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java
+++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java
@@ -21,9 +21,12 @@
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQuery;
import android.net.NetworkUtils;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.Status;
@@ -35,6 +38,7 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
+import java.util.StringJoiner;
/**
* Encapsulating class for using the SQLite database backing the memory store.
@@ -46,6 +50,9 @@
*/
public class IpMemoryStoreDatabase {
private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName();
+ // A pair of NetworkAttributes objects is group-close if the confidence that they are
+ // the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse.
+ private static final float GROUPCLOSE_CONFIDENCE = 0.5f;
/**
* Contract class for the Network Attributes table.
@@ -187,30 +194,35 @@
return addresses;
}
+ @NonNull
+ private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
+ final ContentValues values = new ContentValues();
+ if (null == attributes) return values;
+ if (null != attributes.assignedV4Address) {
+ values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
+ NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address));
+ }
+ if (null != attributes.groupHint) {
+ values.put(NetworkAttributesContract.COLNAME_GROUPHINT, attributes.groupHint);
+ }
+ if (null != attributes.dnsAddresses) {
+ values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
+ encodeAddressList(attributes.dnsAddresses));
+ }
+ if (null != attributes.mtu) {
+ values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
+ }
+ return values;
+ }
+
// Convert a NetworkAttributes object to content values to store them in a table compliant
// with the contract defined in NetworkAttributesContract.
@NonNull
private static ContentValues toContentValues(@NonNull final String key,
@Nullable final NetworkAttributes attributes, final long expiry) {
- final ContentValues values = new ContentValues();
+ final ContentValues values = toContentValues(attributes);
values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
- if (null != attributes) {
- if (null != attributes.assignedV4Address) {
- values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
- NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address));
- }
- if (null != attributes.groupHint) {
- values.put(NetworkAttributesContract.COLNAME_GROUPHINT, attributes.groupHint);
- }
- if (null != attributes.dnsAddresses) {
- values.put(NetworkAttributesContract.COLNAME_DNSADDRESSES,
- encodeAddressList(attributes.dnsAddresses));
- }
- if (null != attributes.mtu) {
- values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu);
- }
- }
return values;
}
@@ -228,6 +240,32 @@
return values;
}
+ @Nullable
+ private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) {
+ // Make sure the data hasn't expired
+ final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L);
+ if (expiry < System.currentTimeMillis()) return null;
+
+ final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
+ final int assignedV4AddressInt = getInt(cursor,
+ NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
+ final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT);
+ final byte[] dnsAddressesBlob =
+ getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
+ final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
+ if (0 != assignedV4AddressInt) {
+ builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt));
+ }
+ builder.setGroupHint(groupHint);
+ if (null != dnsAddressesBlob) {
+ builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
+ }
+ if (mtu >= 0) {
+ builder.setMtu(mtu);
+ }
+ return builder.build();
+ }
+
private static final String[] EXPIRY_COLUMN = new String[] {
NetworkAttributesContract.COLNAME_EXPIRYDATE
};
@@ -313,32 +351,9 @@
// result here. 0 results means the key was not found.
if (cursor.getCount() != 1) return null;
cursor.moveToFirst();
-
- // Make sure the data hasn't expired
- final long expiry = cursor.getLong(
- cursor.getColumnIndexOrThrow(NetworkAttributesContract.COLNAME_EXPIRYDATE));
- if (expiry < System.currentTimeMillis()) return null;
-
- final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
- final int assignedV4AddressInt = getInt(cursor,
- NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0);
- final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT);
- final byte[] dnsAddressesBlob =
- getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES);
- final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1);
+ final NetworkAttributes attributes = readNetworkAttributesLine(cursor);
cursor.close();
-
- if (0 != assignedV4AddressInt) {
- builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt));
- }
- builder.setGroupHint(groupHint);
- if (null != dnsAddressesBlob) {
- builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob));
- }
- if (mtu >= 0) {
- builder.setMtu(mtu);
- }
- return builder.build();
+ return attributes;
}
private static final String[] DATA_COLUMN = new String[] {
@@ -365,17 +380,134 @@
return result;
}
+ /**
+ * The following is a horrible hack that is necessary because the Android SQLite API does not
+ * have a way to query a binary blob. This, almost certainly, is an overlook.
+ *
+ * The Android SQLite API has two family of methods : one for query that returns data, and
+ * one for more general SQL statements that can execute any statement but may not return
+ * anything. All the query methods, however, take only String[] for the arguments.
+ *
+ * In principle it is simple to write a function that will encode the binary blob in the
+ * way SQLite expects it. However, because the API forces the argument to be coerced into a
+ * String, the SQLiteQuery object generated by the default query methods will bind all
+ * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types,
+ * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will
+ * be sanitized, changing the contents of the field, and the query will fail to match the
+ * blob.
+ *
+ * As far as I can tell, there are two possible ways around this problem. The first one
+ * is to put the data in the query string and eschew it being an argument. This would
+ * require doing the sanitizing by hand. The other is to call bindBlob directly on the
+ * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out
+ * sanitizing, but also will do the right thing if the underlying format ever changes.
+ *
+ * But none of the methods that take an SQLiteQuery object can return data ; this *must*
+ * be called with SQLiteDatabase#query. This object is not accessible from outside.
+ * However, there is a #query version that accepts a CursorFactory and this is pretty
+ * straightforward to implement as all the arguments are coming in and the SQLiteCursor
+ * class is public API.
+ * With this, it's possible to intercept the SQLiteQuery object, and assuming the args
+ * are available, to bind them directly and work around the API's oblivious coercion into
+ * Strings.
+ *
+ * This is really sad, but I don't see another way of having this work than this or the
+ * hand-rolled sanitizing, and this is the lesser evil.
+ */
+ private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory {
+ @NonNull
+ private final ArrayList<Object> mArgs;
+ CustomCursorFactory(@NonNull final ArrayList<Object> args) {
+ mArgs = args;
+ }
+ @Override
+ public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery,
+ final String editTable,
+ final SQLiteQuery query) {
+ int index = 1; // bind is 1-indexed
+ for (final Object arg : mArgs) {
+ if (arg instanceof String) {
+ query.bindString(index++, (String) arg);
+ } else if (arg instanceof Long) {
+ query.bindLong(index++, (Long) arg);
+ } else if (arg instanceof Integer) {
+ query.bindLong(index++, Long.valueOf((Integer) arg));
+ } else if (arg instanceof byte[]) {
+ query.bindBlob(index++, (byte[]) arg);
+ } else {
+ throw new IllegalStateException("Unsupported type CustomCursorFactory "
+ + arg.getClass().toString());
+ }
+ }
+ return new SQLiteCursor(masterQuery, editTable, query);
+ }
+ }
+
+ // Returns the l2key of the closest match, if and only if it matches
+ // closely enough (as determined by group-closeness).
+ @Nullable
+ static String findClosestAttributes(@NonNull final SQLiteDatabase db,
+ @NonNull final NetworkAttributes attr) {
+ if (attr.isEmpty()) return null;
+ final ContentValues values = toContentValues(attr);
+
+ // Build the selection and args. To cut down on the number of lines to search, limit
+ // the search to those with at least one argument equals to the requested attributes.
+ // This works only because null attributes match only will not result in group-closeness.
+ final StringJoiner sj = new StringJoiner(" OR ");
+ final ArrayList<Object> args = new ArrayList<>();
+ args.add(System.currentTimeMillis());
+ for (final String field : values.keySet()) {
+ sj.add(field + " = ?");
+ args.add(values.get(field));
+ }
+
+ final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND ("
+ + sj.toString() + ")";
+ final Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args),
+ false, // distinct
+ NetworkAttributesContract.TABLENAME,
+ null, // columns, null means everything
+ selection, // selection
+ null, // selectionArgs, horrendously passed to the cursor factory instead
+ null, // groupBy
+ null, // having
+ null, // orderBy
+ null); // limit
+ if (cursor.getCount() <= 0) return null;
+ cursor.moveToFirst();
+ String bestKey = null;
+ float bestMatchConfidence = GROUPCLOSE_CONFIDENCE; // Never return a match worse than this.
+ while (!cursor.isAfterLast()) {
+ final NetworkAttributes read = readNetworkAttributesLine(cursor);
+ final float confidence = read.getNetworkGroupSamenessConfidence(attr);
+ if (confidence > bestMatchConfidence) {
+ bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY);
+ bestMatchConfidence = confidence;
+ }
+ cursor.moveToNext();
+ }
+ cursor.close();
+ return bestKey;
+ }
+
// Helper methods
- static String getString(final Cursor cursor, final String columnName) {
+ private static String getString(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
}
- static byte[] getBlob(final Cursor cursor, final String columnName) {
+ private static byte[] getBlob(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
}
- static int getInt(final Cursor cursor, final String columnName, final int defaultValue) {
+ private static int getInt(final Cursor cursor, final String columnName,
+ final int defaultValue) {
final int columnIndex = cursor.getColumnIndex(columnName);
return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
}
+ private static long getLong(final Cursor cursor, final String columnName,
+ final long defaultValue) {
+ final int columnIndex = cursor.getColumnIndex(columnName);
+ return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue;
+ }
}
diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java
index 8b521f4..d43dc6a 100644
--- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java
+++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java
@@ -250,9 +250,26 @@
* Through the listener, returns the L2 key if one matched, or null.
*/
@Override
- public void findL2Key(@NonNull final NetworkAttributesParcelable attributes,
- @NonNull final IOnL2KeyResponseListener listener) {
- // TODO : implement this.
+ public void findL2Key(@Nullable final NetworkAttributesParcelable attributes,
+ @Nullable final IOnL2KeyResponseListener listener) {
+ if (null == listener) return;
+ mExecutor.execute(() -> {
+ try {
+ if (null == attributes) {
+ listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
+ return;
+ }
+ if (null == mDb) {
+ listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
+ return;
+ }
+ final String key = IpMemoryStoreDatabase.findClosestAttributes(mDb,
+ new NetworkAttributes(attributes));
+ listener.onL2KeyResponse(makeStatus(SUCCESS), key);
+ } catch (final RemoteException e) {
+ // Client at the other end died
+ }
+ });
}
/**
diff --git a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java
index c748d0f..f2ecef9 100644
--- a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java
+++ b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java
@@ -27,6 +27,7 @@
import android.content.Context;
import android.net.ipmemorystore.Blob;
import android.net.ipmemorystore.IOnBlobRetrievedListener;
+import android.net.ipmemorystore.IOnL2KeyResponseListener;
import android.net.ipmemorystore.IOnNetworkAttributesRetrieved;
import android.net.ipmemorystore.IOnSameNetworkResponseListener;
import android.net.ipmemorystore.IOnStatusListener;
@@ -67,7 +68,14 @@
private static final String TEST_CLIENT_ID = "testClientId";
private static final String TEST_DATA_NAME = "testData";
- private static final String[] FAKE_KEYS = { "fakeKey1", "fakeKey2", "fakeKey3", "fakeKey4" };
+ private static final int FAKE_KEY_COUNT = 20;
+ private static final String[] FAKE_KEYS;
+ static {
+ FAKE_KEYS = new String[FAKE_KEY_COUNT];
+ for (int i = 0; i < FAKE_KEYS.length; ++i) {
+ FAKE_KEYS[i] = "fakeKey" + i;
+ }
+ }
@Mock
private Context mMockContext;
@@ -170,6 +178,25 @@
};
}
+ /** Helper method to make an IOnL2KeyResponseListener */
+ private interface OnL2KeyResponseListener {
+ void onL2KeyResponse(Status status, String key);
+ }
+ private IOnL2KeyResponseListener onL2KeyResponse(final OnL2KeyResponseListener functor) {
+ return new IOnL2KeyResponseListener() {
+ @Override
+ public void onL2KeyResponse(final StatusParcelable status, final String key)
+ throws RemoteException {
+ functor.onL2KeyResponse(new Status(status), key);
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+ };
+ }
+
// Helper method to factorize some boilerplate
private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) {
final CountDownLatch latch = new CountDownLatch(1);
@@ -195,12 +222,9 @@
}
@Test
- public void testNetworkAttributes() {
+ public void testNetworkAttributes() throws UnknownHostException {
final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
- try {
- na.setAssignedV4Address(
- (Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4}));
- } catch (UnknownHostException e) { /* Can't happen */ }
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
na.setGroupHint("hint1");
na.setMtu(219);
final String l2Key = FAKE_KEYS[0];
@@ -218,10 +242,8 @@
})));
final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder();
- try {
- na.setDnsAddresses(Arrays.asList(
- new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
- } catch (UnknownHostException e) { /* Still can't happen */ }
+ na.setDnsAddresses(Arrays.asList(
+ new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
final NetworkAttributes attributes2 = na2.build();
storeAttributes("Did not complete storing attributes 2", l2Key, attributes2);
@@ -333,8 +355,93 @@
}
@Test
- public void testFindL2Key() {
- // TODO : implement this
+ public void testFindL2Key() throws UnknownHostException {
+ final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
+ na.setGroupHint("hint0");
+ storeAttributes(FAKE_KEYS[0], na.build());
+
+ na.setDnsAddresses(Arrays.asList(
+ new InetAddress[] {Inet6Address.getByName("8D56:9AF1::08EE:20F1")}));
+ na.setMtu(219);
+ storeAttributes(FAKE_KEYS[1], na.build());
+ na.setMtu(null);
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
+ na.setDnsAddresses(Arrays.asList(
+ new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
+ na.setGroupHint("hint1");
+ storeAttributes(FAKE_KEYS[2], na.build());
+ na.setMtu(219);
+ storeAttributes(FAKE_KEYS[3], na.build());
+ na.setMtu(240);
+ storeAttributes(FAKE_KEYS[4], na.build());
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("5.6.7.8"));
+ storeAttributes(FAKE_KEYS[5], na.build());
+
+ // Matches key 5 exactly
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[5], key);
+ })));
+
+ // MTU matches key 4 but v4 address matches key 5. The latter is stronger.
+ na.setMtu(240);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[5], key);
+ })));
+
+ // Closest to key 3 (indeed, identical)
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
+ na.setMtu(219);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[3], key);
+ })));
+
+ // Group hint alone must not be strong enough to override the rest
+ na.setGroupHint("hint0");
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[3], key);
+ })));
+
+ // Still closest to key 3, though confidence is lower
+ na.setGroupHint("hint1");
+ na.setDnsAddresses(null);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[3], key);
+ })));
+
+ // But changing the MTU makes this closer to key 4
+ na.setMtu(240);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertEquals(FAKE_KEYS[4], key);
+ })));
+
+ // MTU alone not strong enough to make this group-close
+ na.setGroupHint(null);
+ na.setDnsAddresses(null);
+ na.setAssignedV4Address(null);
+ doLatched("Did not finish finding L2Key", latch ->
+ mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> {
+ assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+ status.isSuccess());
+ assertNull(key);
+ })));
}
private void assertNetworksSameness(final String key1, final String key2, final int sameness) {
@@ -349,7 +456,7 @@
@Test
public void testIsSameNetwork() throws UnknownHostException {
final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
- na.setAssignedV4Address((Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4}));
+ na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
na.setGroupHint("hint1");
na.setMtu(219);
na.setDnsAddresses(Arrays.asList(Inet6Address.getByName("0A1C:2E40:480A::1CA6")));