Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 21c82571 authored by Chalard Jean's avatar Chalard Jean Committed by android-build-merger
Browse files

Merge "[MS11] Implement findL2Key"

am: 773f3f5b

Change-Id: If429108eddc802f0c13f7b10048b1fe87bd51b22
parents a8e16262 773f3f5b
Loading
Loading
Loading
Loading
+6 −0
Original line number Original line Diff line number Diff line
@@ -252,6 +252,12 @@ public class NetworkAttributes {
        }
        }
    }
    }


    /** @hide */
    public boolean isEmpty() {
        return (null == assignedV4Address) && (null == groupHint)
                && (null == dnsAddresses) && (null == mtu);
    }

    @Override
    @Override
    public boolean equals(@Nullable final Object o) {
    public boolean equals(@Nullable final Object o) {
        if (!(o instanceof NetworkAttributes)) return false;
        if (!(o instanceof NetworkAttributes)) return false;
+177 −45
Original line number Original line Diff line number Diff line
@@ -21,9 +21,12 @@ import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.ContentValues;
import android.content.Context;
import android.content.Context;
import android.database.Cursor;
import android.database.Cursor;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQuery;
import android.net.NetworkUtils;
import android.net.NetworkUtils;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.Status;
import android.net.ipmemorystore.Status;
@@ -35,6 +38,7 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.List;
import java.util.StringJoiner;


/**
/**
 * Encapsulating class for using the SQLite database backing the memory store.
 * Encapsulating class for using the SQLite database backing the memory store.
@@ -46,6 +50,9 @@ import java.util.List;
 */
 */
public class IpMemoryStoreDatabase {
public class IpMemoryStoreDatabase {
    private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName();
    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.
     * Contract class for the Network Attributes table.
@@ -187,15 +194,10 @@ public class IpMemoryStoreDatabase {
        return addresses;
        return addresses;
    }
    }


    // Convert a NetworkAttributes object to content values to store them in a table compliant
    // with the contract defined in NetworkAttributesContract.
    @NonNull
    @NonNull
    private static ContentValues toContentValues(@NonNull final String key,
    private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) {
            @Nullable final NetworkAttributes attributes, final long expiry) {
        final ContentValues values = new ContentValues();
        final ContentValues values = new ContentValues();
        values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
        if (null == attributes) return values;
        values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
        if (null != attributes) {
        if (null != attributes.assignedV4Address) {
        if (null != attributes.assignedV4Address) {
            values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
            values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS,
                    NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address));
                    NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address));
@@ -210,7 +212,17 @@ public class IpMemoryStoreDatabase {
        if (null != attributes.mtu) {
        if (null != attributes.mtu) {
            values.put(NetworkAttributesContract.COLNAME_MTU, 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 = toContentValues(attributes);
        values.put(NetworkAttributesContract.COLNAME_L2KEY, key);
        values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry);
        return values;
        return values;
    }
    }


@@ -228,6 +240,32 @@ public class IpMemoryStoreDatabase {
        return values;
        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[] {
    private static final String[] EXPIRY_COLUMN = new String[] {
        NetworkAttributesContract.COLNAME_EXPIRYDATE
        NetworkAttributesContract.COLNAME_EXPIRYDATE
    };
    };
@@ -313,32 +351,9 @@ public class IpMemoryStoreDatabase {
        // result here. 0 results means the key was not found.
        // result here. 0 results means the key was not found.
        if (cursor.getCount() != 1) return null;
        if (cursor.getCount() != 1) return null;
        cursor.moveToFirst();
        cursor.moveToFirst();

        final NetworkAttributes attributes = readNetworkAttributesLine(cursor);
        // 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);
        cursor.close();
        cursor.close();

        return attributes;
        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[] DATA_COLUMN = new String[] {
    private static final String[] DATA_COLUMN = new String[] {
@@ -365,17 +380,134 @@ public class IpMemoryStoreDatabase {
        return result;
        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
    // 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);
        final int columnIndex = cursor.getColumnIndex(columnName);
        return (columnIndex >= 0) ? cursor.getString(columnIndex) : null;
        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);
        final int columnIndex = cursor.getColumnIndex(columnName);
        return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null;
        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);
        final int columnIndex = cursor.getColumnIndex(columnName);
        return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue;
        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;
    }
}
}
+20 −3
Original line number Original line Diff line number Diff line
@@ -250,9 +250,26 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub {
     * Through the listener, returns the L2 key if one matched, or null.
     * Through the listener, returns the L2 key if one matched, or null.
     */
     */
    @Override
    @Override
    public void findL2Key(@NonNull final NetworkAttributesParcelable attributes,
    public void findL2Key(@Nullable final NetworkAttributesParcelable attributes,
            @NonNull final IOnL2KeyResponseListener listener) {
            @Nullable final IOnL2KeyResponseListener listener) {
        // TODO : implement this.
        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
            }
        });
    }
    }


    /**
    /**
+120 −13
Original line number Original line Diff line number Diff line
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.doReturn;
import android.content.Context;
import android.content.Context;
import android.net.ipmemorystore.Blob;
import android.net.ipmemorystore.Blob;
import android.net.ipmemorystore.IOnBlobRetrievedListener;
import android.net.ipmemorystore.IOnBlobRetrievedListener;
import android.net.ipmemorystore.IOnL2KeyResponseListener;
import android.net.ipmemorystore.IOnNetworkAttributesRetrieved;
import android.net.ipmemorystore.IOnNetworkAttributesRetrieved;
import android.net.ipmemorystore.IOnSameNetworkResponseListener;
import android.net.ipmemorystore.IOnSameNetworkResponseListener;
import android.net.ipmemorystore.IOnStatusListener;
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_CLIENT_ID = "testClientId";
    private static final String TEST_DATA_NAME = "testData";
    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
    @Mock
    private Context mMockContext;
    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
    // Helper method to factorize some boilerplate
    private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) {
    private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) {
        final CountDownLatch latch = new CountDownLatch(1);
        final CountDownLatch latch = new CountDownLatch(1);
@@ -195,12 +222,9 @@ public class IpMemoryStoreServiceTest {
    }
    }


    @Test
    @Test
    public void testNetworkAttributes() {
    public void testNetworkAttributes() throws UnknownHostException {
        final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
        final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
        try {
        na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
            na.setAssignedV4Address(
                    (Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4}));
        } catch (UnknownHostException e) { /* Can't happen */ }
        na.setGroupHint("hint1");
        na.setGroupHint("hint1");
        na.setMtu(219);
        na.setMtu(219);
        final String l2Key = FAKE_KEYS[0];
        final String l2Key = FAKE_KEYS[0];
@@ -218,10 +242,8 @@ public class IpMemoryStoreServiceTest {
                        })));
                        })));


        final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder();
        final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder();
        try {
        na.setDnsAddresses(Arrays.asList(
        na.setDnsAddresses(Arrays.asList(
                new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
                new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")}));
        } catch (UnknownHostException e) { /* Still can't happen */ }
        final NetworkAttributes attributes2 = na2.build();
        final NetworkAttributes attributes2 = na2.build();
        storeAttributes("Did not complete storing attributes 2", l2Key, attributes2);
        storeAttributes("Did not complete storing attributes 2", l2Key, attributes2);


@@ -333,8 +355,93 @@ public class IpMemoryStoreServiceTest {
    }
    }


    @Test
    @Test
    public void testFindL2Key() {
    public void testFindL2Key() throws UnknownHostException {
        // TODO : implement this
        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) {
    private void assertNetworksSameness(final String key1, final String key2, final int sameness) {
@@ -349,7 +456,7 @@ public class IpMemoryStoreServiceTest {
    @Test
    @Test
    public void testIsSameNetwork() throws UnknownHostException {
    public void testIsSameNetwork() throws UnknownHostException {
        final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
        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.setGroupHint("hint1");
        na.setMtu(219);
        na.setMtu(219);
        na.setDnsAddresses(Arrays.asList(Inet6Address.getByName("0A1C:2E40:480A::1CA6")));
        na.setDnsAddresses(Arrays.asList(Inet6Address.getByName("0A1C:2E40:480A::1CA6")));