diff options
author | 2019-01-21 09:34:41 +0000 | |
---|---|---|
committer | 2019-01-21 09:34:41 +0000 | |
commit | ca046e898b500c48b5e5412d49bf4f51aa637d98 (patch) | |
tree | 4fc6107978266607978840822f62f489cad1ce4b | |
parent | 7bf90a1f97080ac56db44374035a97ebd2737366 (diff) | |
parent | bf73e66d4db7e2cd235017333260ee2fdceea5ee (diff) |
Merge changes I2ddfef0c,I49bee0c9
* changes:
[MS08] Read back attributes and blobs.
[MS07] Implement storeNetworkAttributes and storeBlob.
6 files changed, 653 insertions, 30 deletions
diff --git a/core/java/android/net/ipmemorystore/Status.java b/core/java/android/net/ipmemorystore/Status.java index 95e504224ac7..cacd42d713c2 100644 --- a/core/java/android/net/ipmemorystore/Status.java +++ b/core/java/android/net/ipmemorystore/Status.java @@ -18,6 +18,8 @@ package android.net.ipmemorystore; import android.annotation.NonNull; +import com.android.internal.annotations.VisibleForTesting; + /** * A parcelable status representing the result of an operation. * Parcels as StatusParceled. @@ -26,7 +28,10 @@ import android.annotation.NonNull; public class Status { public static final int SUCCESS = 0; - public static final int ERROR_DATABASE_CANNOT_BE_OPENED = -1; + public static final int ERROR_GENERIC = -1; + public static final int ERROR_ILLEGAL_ARGUMENT = -2; + public static final int ERROR_DATABASE_CANNOT_BE_OPENED = -3; + public static final int ERROR_STORAGE = -4; public final int resultCode; @@ -34,7 +39,8 @@ public class Status { this.resultCode = resultCode; } - Status(@NonNull final StatusParcelable parcelable) { + @VisibleForTesting + public Status(@NonNull final StatusParcelable parcelable) { this(parcelable.resultCode); } @@ -55,7 +61,12 @@ public class Status { public String toString() { switch (resultCode) { case SUCCESS: return "SUCCESS"; + case ERROR_GENERIC: return "GENERIC ERROR"; + case ERROR_ILLEGAL_ARGUMENT: return "ILLEGAL ARGUMENT"; case ERROR_DATABASE_CANNOT_BE_OPENED: return "DATABASE CANNOT BE OPENED"; + // "DB storage error" is not very helpful but SQLite does not provide specific error + // codes upon store failure. Thus this indicates SQLite returned some error upon store + case ERROR_STORAGE: return "DATABASE STORAGE ERROR"; default: return "Unknown value ?!"; } } diff --git a/core/java/android/net/ipmemorystore/Utils.java b/core/java/android/net/ipmemorystore/Utils.java index 73d8c83acdd9..b361aca5a6f7 100644 --- a/core/java/android/net/ipmemorystore/Utils.java +++ b/core/java/android/net/ipmemorystore/Utils.java @@ -17,18 +17,25 @@ package android.net.ipmemorystore; import android.annotation.NonNull; +import android.annotation.Nullable; /** {@hide} */ public class Utils { /** Pretty print */ - public static String blobToString(final Blob blob) { - final StringBuilder sb = new StringBuilder("Blob : ["); - if (blob.data.length <= 24) { - appendByteArray(sb, blob.data, 0, blob.data.length); + public static String blobToString(@Nullable final Blob blob) { + return "Blob : " + byteArrayToString(null == blob ? null : blob.data); + } + + /** Pretty print */ + public static String byteArrayToString(@Nullable final byte[] data) { + if (null == data) return "null"; + final StringBuilder sb = new StringBuilder("["); + if (data.length <= 24) { + appendByteArray(sb, data, 0, data.length); } else { - appendByteArray(sb, blob.data, 0, 16); + appendByteArray(sb, data, 0, 16); sb.append("..."); - appendByteArray(sb, blob.data, blob.data.length - 8, blob.data.length); + appendByteArray(sb, data, data.length - 8, data.length); } sb.append("]"); return sb.toString(); 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 eaab6507e8d4..238f07715ce3 100644 --- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java +++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java @@ -17,9 +17,24 @@ package com.android.server.net.ipmemorystore; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; +import android.net.NetworkUtils; +import android.net.ipmemorystore.NetworkAttributes; +import android.net.ipmemorystore.Status; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; /** * Encapsulating class for using the SQLite database backing the memory store. @@ -30,6 +45,8 @@ import android.database.sqlite.SQLiteOpenHelper; * @hide */ public class IpMemoryStoreDatabase { + private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName(); + /** * Contract class for the Network Attributes table. */ @@ -57,7 +74,7 @@ public class IpMemoryStoreDatabase { public static final String COLTYPE_DNSADDRESSES = "BLOB"; public static final String COLNAME_MTU = "mtu"; - public static final String COLTYPE_MTU = "INTEGER"; + public static final String COLTYPE_MTU = "INTEGER DEFAULT -1"; public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLENAME + " (" @@ -108,7 +125,7 @@ public class IpMemoryStoreDatabase { /** The SQLite DB helper */ public static class DbHelper extends SQLiteOpenHelper { // Update this whenever changing the schema. - private static final int SCHEMA_VERSION = 1; + private static final int SCHEMA_VERSION = 2; private static final String DATABASE_FILENAME = "IpMemoryStore.db"; public DbHelper(@NonNull final Context context) { @@ -140,4 +157,216 @@ public class IpMemoryStoreDatabase { onCreate(db); } } + + @NonNull + private static byte[] encodeAddressList(@NonNull final List<InetAddress> addresses) { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (final InetAddress address : addresses) { + final byte[] b = address.getAddress(); + os.write(b.length); + os.write(b, 0, b.length); + } + return os.toByteArray(); + } + + @NonNull + private static ArrayList<InetAddress> decodeAddressList(@NonNull final byte[] encoded) { + final ByteArrayInputStream is = new ByteArrayInputStream(encoded); + final ArrayList<InetAddress> addresses = new ArrayList<>(); + int d = -1; + while ((d = is.read()) != -1) { + final byte[] bytes = new byte[d]; + is.read(bytes, 0, d); + try { + addresses.add(InetAddress.getByAddress(bytes)); + } catch (UnknownHostException e) { /* Hopefully impossible */ } + } + return addresses; + } + + // 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(); + 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; + } + + // Convert a byte array into content values to store it in a table compliant with the + // contract defined in PrivateDataContract. + @NonNull + private static ContentValues toContentValues(@NonNull final String key, + @NonNull final String clientId, @NonNull final String name, + @NonNull final byte[] data) { + final ContentValues values = new ContentValues(); + values.put(PrivateDataContract.COLNAME_L2KEY, key); + values.put(PrivateDataContract.COLNAME_CLIENT, clientId); + values.put(PrivateDataContract.COLNAME_DATANAME, name); + values.put(PrivateDataContract.COLNAME_DATA, data); + return values; + } + + private static final String[] EXPIRY_COLUMN = new String[] { + NetworkAttributesContract.COLNAME_EXPIRYDATE + }; + static final int EXPIRY_ERROR = -1; // Legal values for expiry are positive + + static final String SELECT_L2KEY = NetworkAttributesContract.COLNAME_L2KEY + " = ?"; + + // Returns the expiry date of the specified row, or one of the error codes above if the + // row is not found or some other error + static long getExpiry(@NonNull final SQLiteDatabase db, @NonNull final String key) { + final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME, + EXPIRY_COLUMN, // columns + SELECT_L2KEY, // selection + new String[] { key }, // selectionArgs + null, // groupBy + null, // having + null // orderBy + ); + // L2KEY is the primary key ; it should not be possible to get more than one + // result here. 0 results means the key was not found. + if (cursor.getCount() != 1) return EXPIRY_ERROR; + cursor.moveToFirst(); + return cursor.getLong(0); // index in the EXPIRY_COLUMN array + } + + static final int RELEVANCE_ERROR = -1; // Legal values for relevance are positive + + // Returns the relevance of the specified row, or one of the error codes above if the + // row is not found or some other error + static int getRelevance(@NonNull final SQLiteDatabase db, @NonNull final String key) { + final long expiry = getExpiry(db, key); + return expiry < 0 ? (int) expiry : RelevanceUtils.computeRelevanceForNow(expiry); + } + + // If the attributes are null, this will only write the expiry. + // Returns an int out of Status.{SUCCESS,ERROR_*} + static int storeNetworkAttributes(@NonNull final SQLiteDatabase db, @NonNull final String key, + final long expiry, @Nullable final NetworkAttributes attributes) { + final ContentValues cv = toContentValues(key, attributes, expiry); + db.beginTransaction(); + try { + // Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are + // to either insert with on conflict ignore then update (like done here), or to + // construct a custom SQL INSERT statement with nested select. + final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME, + null, cv, SQLiteDatabase.CONFLICT_IGNORE); + if (resultId < 0) { + db.update(NetworkAttributesContract.TABLENAME, cv, SELECT_L2KEY, new String[]{key}); + } + db.setTransactionSuccessful(); + return Status.SUCCESS; + } catch (SQLiteException e) { + // No space left on disk or something + Log.e(TAG, "Could not write to the memory store", e); + } finally { + db.endTransaction(); + } + return Status.ERROR_STORAGE; + } + + // Returns an int out of Status.{SUCCESS,ERROR_*} + static int storeBlob(@NonNull final SQLiteDatabase db, @NonNull final String key, + @NonNull final String clientId, @NonNull final String name, + @NonNull final byte[] data) { + final long res = db.insertWithOnConflict(PrivateDataContract.TABLENAME, null, + toContentValues(key, clientId, name, data), SQLiteDatabase.CONFLICT_REPLACE); + return (res == -1) ? Status.ERROR_STORAGE : Status.SUCCESS; + } + + @Nullable + static NetworkAttributes retrieveNetworkAttributes(@NonNull final SQLiteDatabase db, + @NonNull final String key) { + final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME, + null, // columns, null means everything + NetworkAttributesContract.COLNAME_L2KEY + " = ?", // selection + new String[] { key }, // selectionArgs + null, // groupBy + null, // having + null); // orderBy + // L2KEY is the primary key ; it should not be possible to get more than one + // 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); + 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[] { + PrivateDataContract.COLNAME_DATA + }; + @Nullable + static byte[] retrieveBlob(@NonNull final SQLiteDatabase db, @NonNull final String key, + @NonNull final String clientId, @NonNull final String name) { + final Cursor cursor = db.query(PrivateDataContract.TABLENAME, + DATA_COLUMN, // columns + PrivateDataContract.COLNAME_L2KEY + " = ? AND " // selection + + PrivateDataContract.COLNAME_CLIENT + " = ? AND " + + PrivateDataContract.COLNAME_DATANAME + " = ?", + new String[] { key, clientId, name }, // selectionArgs + null, // groupBy + null, // having + null); // orderBy + // The query above is querying by (composite) primary key, so it should not be possible to + // get more than one result here. 0 results means the key was not found. + if (cursor.getCount() != 1) return null; + cursor.moveToFirst(); + return cursor.getBlob(0); // index in the DATA_COLUMN array + } + + // Helper methods + 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) { + 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) { + final int columnIndex = cursor.getColumnIndex(columnName); + return (columnIndex >= 0) ? cursor.getInt(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 55a72190eff4..444b299d49e6 100644 --- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java +++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java @@ -16,6 +16,13 @@ package com.android.server.net.ipmemorystore; +import static android.net.ipmemorystore.Status.ERROR_DATABASE_CANNOT_BE_OPENED; +import static android.net.ipmemorystore.Status.ERROR_GENERIC; +import static android.net.ipmemorystore.Status.ERROR_ILLEGAL_ARGUMENT; +import static android.net.ipmemorystore.Status.SUCCESS; + +import static com.android.server.net.ipmemorystore.IpMemoryStoreDatabase.EXPIRY_ERROR; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -28,7 +35,12 @@ import android.net.ipmemorystore.IOnL2KeyResponseListener; import android.net.ipmemorystore.IOnNetworkAttributesRetrieved; import android.net.ipmemorystore.IOnSameNetworkResponseListener; import android.net.ipmemorystore.IOnStatusListener; +import android.net.ipmemorystore.NetworkAttributes; import android.net.ipmemorystore.NetworkAttributesParcelable; +import android.net.ipmemorystore.Status; +import android.net.ipmemorystore.StatusParcelable; +import android.net.ipmemorystore.Utils; +import android.os.RemoteException; import android.util.Log; import java.util.concurrent.ExecutorService; @@ -45,6 +57,7 @@ import java.util.concurrent.Executors; public class IpMemoryStoreService extends IIpMemoryStore.Stub { private static final String TAG = IpMemoryStoreService.class.getSimpleName(); private static final int MAX_CONCURRENT_THREADS = 4; + private static final boolean DBG = true; @NonNull final Context mContext; @@ -114,6 +127,11 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { if (mDb != null) mDb.close(); } + /** Helper function to make a status object */ + private StatusParcelable makeStatus(final int code) { + return new Status(code).toParcelable(); + } + /** * Store network attributes for a given L2 key. * @@ -128,11 +146,27 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { * Through the listener, returns the L2 key. This is useful if the L2 key was not specified. * If the call failed, the L2 key will be null. */ + // Note that while l2Key and attributes are non-null in spirit, they are received from + // another process. If the remote process decides to ignore everything and send null, this + // process should still not crash. @Override - public void storeNetworkAttributes(@NonNull final String l2Key, - @NonNull final NetworkAttributesParcelable attributes, + public void storeNetworkAttributes(@Nullable final String l2Key, + @Nullable final NetworkAttributesParcelable attributes, @Nullable final IOnStatusListener listener) { - // TODO : implement this. + // Because the parcelable is 100% mutable, the thread may not see its members initialized. + // Therefore either an immutable object is created on this same thread before it's passed + // to the executor, or there need to be a write barrier here and a read barrier in the + // remote thread. + final NetworkAttributes na = null == attributes ? null : new NetworkAttributes(attributes); + mExecutor.execute(() -> { + try { + final int code = storeNetworkAttributesAndBlobSync(l2Key, na, + null /* clientId */, null /* name */, null /* data */); + if (null != listener) listener.onComplete(makeStatus(code)); + } catch (final RemoteException e) { + // Client at the other end died + } + }); } /** @@ -141,16 +175,63 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { * @param l2Key The L2 key for this network. * @param clientId The ID of the client. * @param name The name of this data. - * @param data The data to store. + * @param blob The data to store. * @param listener The listener that will be invoked to return the answer, or null if the * is not interested in learning about success/failure. * Through the listener, returns a status to indicate success or failure. */ @Override - public void storeBlob(@NonNull final String l2Key, @NonNull final String clientId, - @NonNull final String name, @NonNull final Blob data, + public void storeBlob(@Nullable final String l2Key, @Nullable final String clientId, + @Nullable final String name, @Nullable final Blob blob, @Nullable final IOnStatusListener listener) { - // TODO : implement this. + final byte[] data = null == blob ? null : blob.data; + mExecutor.execute(() -> { + try { + final int code = storeNetworkAttributesAndBlobSync(l2Key, + null /* NetworkAttributes */, clientId, name, data); + if (null != listener) listener.onComplete(makeStatus(code)); + } catch (final RemoteException e) { + // Client at the other end died + } + }); + } + + /** + * Helper method for storeNetworkAttributes and storeBlob. + * + * Either attributes or none of clientId, name and data may be null. This will write the + * passed data if non-null, and will write attributes if non-null, but in any case it will + * bump the relevance up. + * Returns a success code from Status. + */ + private int storeNetworkAttributesAndBlobSync(@Nullable final String l2Key, + @Nullable final NetworkAttributes attributes, + @Nullable final String clientId, + @Nullable final String name, @Nullable final byte[] data) { + if (null == l2Key) return ERROR_ILLEGAL_ARGUMENT; + if (null == attributes && null == data) return ERROR_ILLEGAL_ARGUMENT; + if (null != data && (null == clientId || null == name)) return ERROR_ILLEGAL_ARGUMENT; + if (null == mDb) return ERROR_DATABASE_CANNOT_BE_OPENED; + try { + final long oldExpiry = IpMemoryStoreDatabase.getExpiry(mDb, l2Key); + final long newExpiry = RelevanceUtils.bumpExpiryDate( + oldExpiry == EXPIRY_ERROR ? System.currentTimeMillis() : oldExpiry); + final int errorCode = + IpMemoryStoreDatabase.storeNetworkAttributes(mDb, l2Key, newExpiry, attributes); + // If no blob to store, the client is interested in the result of storing the attributes + if (null == data) return errorCode; + // Otherwise it's interested in the result of storing the blob + return IpMemoryStoreDatabase.storeBlob(mDb, l2Key, clientId, name, data); + } catch (Exception e) { + if (DBG) { + Log.e(TAG, "Exception while storing for key {" + l2Key + + "} ; NetworkAttributes {" + (null == attributes ? "null" : attributes) + + "} ; clientId {" + (null == clientId ? "null" : clientId) + + "} ; name {" + (null == name ? "null" : name) + + "} ; data {" + Utils.byteArrayToString(data) + "}", e); + } + } + return ERROR_GENERIC; } /** @@ -198,9 +279,32 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { * the query. */ @Override - public void retrieveNetworkAttributes(@NonNull final String l2Key, - @NonNull final IOnNetworkAttributesRetrieved listener) { - // TODO : implement this. + public void retrieveNetworkAttributes(@Nullable final String l2Key, + @Nullable final IOnNetworkAttributesRetrieved listener) { + if (null == listener) return; + mExecutor.execute(() -> { + try { + if (null == l2Key) { + listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), l2Key, null); + return; + } + if (null == mDb) { + listener.onL2KeyResponse(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), l2Key, + null); + return; + } + try { + final NetworkAttributes attributes = + IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key); + listener.onL2KeyResponse(makeStatus(SUCCESS), l2Key, + null == attributes ? null : attributes.toParcelable()); + } catch (final Exception e) { + listener.onL2KeyResponse(makeStatus(ERROR_GENERIC), l2Key, null); + } + } catch (final RemoteException e) { + // Client at the other end died + } + }); } /** @@ -217,6 +321,28 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { @Override public void retrieveBlob(@NonNull final String l2Key, @NonNull final String clientId, @NonNull final String name, @NonNull final IOnBlobRetrievedListener listener) { - // TODO : implement this. + if (null == listener) return; + mExecutor.execute(() -> { + try { + if (null == l2Key) { + listener.onBlobRetrieved(makeStatus(ERROR_ILLEGAL_ARGUMENT), l2Key, name, null); + return; + } + if (null == mDb) { + listener.onBlobRetrieved(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), l2Key, + name, null); + return; + } + try { + final Blob b = new Blob(); + b.data = IpMemoryStoreDatabase.retrieveBlob(mDb, l2Key, clientId, name); + listener.onBlobRetrieved(makeStatus(SUCCESS), l2Key, name, b); + } catch (final Exception e) { + listener.onBlobRetrieved(makeStatus(ERROR_GENERIC), l2Key, name, null); + } + } catch (final RemoteException e) { + // Client at the other end died + } + }); } } diff --git a/tests/net/java/android/net/ipmemorystore/ParcelableTests.java b/tests/net/java/android/net/ipmemorystore/ParcelableTests.java index a9f9758bb1f8..1fc67a8212ae 100644 --- a/tests/net/java/android/net/ipmemorystore/ParcelableTests.java +++ b/tests/net/java/android/net/ipmemorystore/ParcelableTests.java @@ -27,6 +27,7 @@ import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import java.lang.reflect.Modifier; import java.net.Inet4Address; import java.net.InetAddress; import java.util.Arrays; @@ -60,6 +61,12 @@ public class ParcelableTests { builder.setMtu(null); in = builder.build(); assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + // Verify that this test does not miss any new field added later. + // If any field is added to NetworkAttributes it must be tested here for parceling + // roundtrip. + assertEquals(4, Arrays.stream(NetworkAttributes.class.getDeclaredFields()) + .filter(f -> !Modifier.isStatic(f.getModifiers())).count()); } @Test 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 e63c3b02d1c3..94bcd28bf009 100644 --- a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java +++ b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java @@ -16,13 +16,30 @@ package com.android.server.net.ipmemorystore; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import android.content.Context; +import android.net.ipmemorystore.Blob; +import android.net.ipmemorystore.IOnBlobRetrievedListener; +import android.net.ipmemorystore.IOnNetworkAttributesRetrieved; +import android.net.ipmemorystore.IOnStatusListener; +import android.net.ipmemorystore.NetworkAttributes; +import android.net.ipmemorystore.NetworkAttributesParcelable; +import android.net.ipmemorystore.Status; +import android.net.ipmemorystore.StatusParcelable; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,41 +47,267 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.File; +import java.lang.reflect.Modifier; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; -/** Unit tests for {@link IpMemoryStoreServiceTest}. */ +/** Unit tests for {@link IpMemoryStoreService}. */ @SmallTest @RunWith(AndroidJUnit4.class) public class IpMemoryStoreServiceTest { + private static final String TEST_CLIENT_ID = "testClientId"; + private static final String TEST_DATA_NAME = "testData"; + @Mock - Context mMockContext; + private Context mMockContext; + private File mDbFile; + + private IpMemoryStoreService mService; @Before public void setUp() { MockitoAnnotations.initMocks(this); - doReturn(new File("/tmp/test.db")).when(mMockContext).getDatabasePath(anyString()); + final Context context = InstrumentationRegistry.getContext(); + final File dir = context.getFilesDir(); + mDbFile = new File(dir, "test.db"); + doReturn(mDbFile).when(mMockContext).getDatabasePath(anyString()); + mService = new IpMemoryStoreService(mMockContext); + } + + @After + public void tearDown() { + mService.shutdown(); + mDbFile.delete(); + } + + /** Helper method to make a vanilla IOnStatusListener */ + private IOnStatusListener onStatus(Consumer<Status> functor) { + return new IOnStatusListener() { + @Override + public void onComplete(final StatusParcelable statusParcelable) throws RemoteException { + functor.accept(new Status(statusParcelable)); + } + + @Override + public IBinder asBinder() { + return null; + } + }; + } + + /** Helper method to make an IOnBlobRetrievedListener */ + private interface OnBlobRetrievedListener { + void onBlobRetrieved(Status status, String l2Key, String name, byte[] data); + } + private IOnBlobRetrievedListener onBlobRetrieved(final OnBlobRetrievedListener functor) { + return new IOnBlobRetrievedListener() { + @Override + public void onBlobRetrieved(final StatusParcelable statusParcelable, + final String l2Key, final String name, final Blob blob) throws RemoteException { + functor.onBlobRetrieved(new Status(statusParcelable), l2Key, name, + null == blob ? null : blob.data); + } + + @Override + public IBinder asBinder() { + return null; + } + }; + } + + /** Helper method to make an IOnNetworkAttributesRetrievedListener */ + private interface OnNetworkAttributesRetrievedListener { + void onNetworkAttributesRetrieved(Status status, String l2Key, NetworkAttributes attr); + } + private IOnNetworkAttributesRetrieved onNetworkAttributesRetrieved( + final OnNetworkAttributesRetrievedListener functor) { + return new IOnNetworkAttributesRetrieved() { + @Override + public void onL2KeyResponse(final StatusParcelable status, final String l2Key, + final NetworkAttributesParcelable attributes) + throws RemoteException { + functor.onNetworkAttributesRetrieved(new Status(status), l2Key, + null == attributes ? null : new NetworkAttributes(attributes)); + } + + @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); + functor.accept(latch); + try { + latch.await(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + fail(timeoutMessage); + } } @Test public void testNetworkAttributes() { - final IpMemoryStoreService service = new IpMemoryStoreService(mMockContext); - // TODO : implement this + 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.setGroupHint("hint1"); + na.setMtu(219); + final String l2Key = UUID.randomUUID().toString(); + NetworkAttributes attributes = na.build(); + doLatched("Did not complete storing attributes", latch -> + mService.storeNetworkAttributes(l2Key, attributes.toParcelable(), + onStatus(status -> { + assertTrue("Store status not successful : " + status.resultCode, + status.isSuccess()); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving attributes", latch -> + mService.retrieveNetworkAttributes(l2Key, onNetworkAttributesRetrieved( + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(attributes, attr); + latch.countDown(); + }))); + + 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 */ } + final NetworkAttributes attributes2 = na2.build(); + doLatched("Did not complete storing attributes 2", latch -> + mService.storeNetworkAttributes(l2Key, attributes2.toParcelable(), + onStatus(status -> latch.countDown()))); + + doLatched("Did not complete retrieving attributes 2", latch -> + mService.retrieveNetworkAttributes(l2Key, onNetworkAttributesRetrieved( + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(attributes.assignedV4Address, attr.assignedV4Address); + assertEquals(attributes.groupHint, attr.groupHint); + assertEquals(attributes.mtu, attr.mtu); + assertEquals(attributes2.dnsAddresses, attr.dnsAddresses); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving attributes 3", latch -> + mService.retrieveNetworkAttributes(l2Key + "nonexistent", + onNetworkAttributesRetrieved( + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key + "nonexistent", key); + assertNull("Retrieved data not stored", attr); + latch.countDown(); + } + ))); + + // Verify that this test does not miss any new field added later. + // If any field is added to NetworkAttributes it must be tested here for storing + // and retrieving. + assertEquals(4, Arrays.stream(NetworkAttributes.class.getDeclaredFields()) + .filter(f -> !Modifier.isStatic(f.getModifiers())).count()); + } + + @Test + public void testInvalidAttributes() { + doLatched("Did not complete storing bad attributes", latch -> + mService.storeNetworkAttributes("key", null, onStatus(status -> { + assertFalse("Success storing on a null key", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + latch.countDown(); + }))); + + final NetworkAttributes na = new NetworkAttributes.Builder().setMtu(2).build(); + doLatched("Did not complete storing bad attributes", latch -> + mService.storeNetworkAttributes(null, na.toParcelable(), onStatus(status -> { + assertFalse("Success storing null attributes on a null key", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + latch.countDown(); + }))); + + doLatched("Did not complete storing bad attributes", latch -> + mService.storeNetworkAttributes(null, null, onStatus(status -> { + assertFalse("Success storing null attributes on a null key", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving bad attributes", latch -> + mService.retrieveNetworkAttributes(null, onNetworkAttributesRetrieved( + (status, key, attr) -> { + assertFalse("Success retrieving attributes for a null key", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + assertNull(key); + assertNull(attr); + }))); } @Test public void testPrivateData() { - final IpMemoryStoreService service = new IpMemoryStoreService(mMockContext); - // TODO : implement this + final Blob b = new Blob(); + b.data = new byte[] { -3, 6, 8, -9, 12, -128, 0, 89, 112, 91, -34 }; + final String l2Key = UUID.randomUUID().toString(); + doLatched("Did not complete storing private data", latch -> + mService.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + onStatus(status -> { + assertTrue("Store status not successful : " + status.resultCode, + status.isSuccess()); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving private data", latch -> + mService.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, onBlobRetrieved( + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + Arrays.equals(b.data, data); + latch.countDown(); + }))); + + // Most puzzling error message ever + doLatched("Did not complete retrieving nothing", latch -> + mService.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME + "2", onBlobRetrieved( + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME + "2"); + assertNull(data); + latch.countDown(); + }))); } @Test public void testFindL2Key() { - final IpMemoryStoreService service = new IpMemoryStoreService(mMockContext); // TODO : implement this } @Test public void testIsSameNetwork() { - final IpMemoryStoreService service = new IpMemoryStoreService(mMockContext); // TODO : implement this } } |