diff options
4 files changed, 323 insertions, 61 deletions
diff --git a/core/java/android/net/ipmemorystore/NetworkAttributes.java b/core/java/android/net/ipmemorystore/NetworkAttributes.java index 5397b57a4568..6a9eae00e3ff 100644 --- a/core/java/android/net/ipmemorystore/NetworkAttributes.java +++ b/core/java/android/net/ipmemorystore/NetworkAttributes.java @@ -252,6 +252,12 @@ public class NetworkAttributes { } } + /** @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 32513c1332bc..e99dd4f1cbae 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.annotation.Nullable; 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.InetAddress; 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 @@ import java.util.List; */ 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 @@ public class IpMemoryStoreDatabase { 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 @@ public class IpMemoryStoreDatabase { 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 @@ public class IpMemoryStoreDatabase { // 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 @@ public class IpMemoryStoreDatabase { 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 8b521f415925..d43dc6a24260 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 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { * 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 c748d0f5f743..f2ecef95b599 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 static org.mockito.Mockito.doReturn; 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 @@ public class IpMemoryStoreServiceTest { 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 @@ public class IpMemoryStoreServiceTest { }; } + /** 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 @@ public class IpMemoryStoreServiceTest { } @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 @@ public class IpMemoryStoreServiceTest { }))); 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 @@ public class IpMemoryStoreServiceTest { } @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 @@ public class IpMemoryStoreServiceTest { @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"))); |