summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Chalard Jean <jchalard@google.com> 2018-12-18 22:05:19 +0900
committer Chalard Jean <jchalard@google.com> 2019-01-21 15:21:09 +0900
commit91549b6d1be1e0d8d0deb9da45050eb76165a39d (patch)
tree26b587a60864fa43024d6516aa779ddc73fe5cd4
parentdb4ce8705911e36a36f0a2f36b8bf9e04d78a2d3 (diff)
[MS07] Implement storeNetworkAttributes and storeBlob.
Test: New tests in IpMemoryStore Bug: 113554482 Change-Id: I49bee0c903247e77ab93517efbe44548313cf1a4
-rw-r--r--core/java/android/net/ipmemorystore/Status.java15
-rw-r--r--core/java/android/net/ipmemorystore/Utils.java19
-rw-r--r--services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java135
-rw-r--r--services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java94
-rw-r--r--tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java95
5 files changed, 334 insertions, 24 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..25522763df09 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,21 @@
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.ByteArrayOutputStream;
+import java.net.InetAddress;
+import java.util.List;
/**
* Encapsulating class for using the SQLite database backing the memory store.
@@ -30,6 +42,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.
*/
@@ -140,4 +154,125 @@ 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();
+ }
+
+ // 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;
+ }
}
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..f002da851287 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,12 @@
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 com.android.server.net.ipmemorystore.IpMemoryStoreDatabase.EXPIRY_ERROR;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -28,7 +34,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 +56,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 +126,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 +145,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 +174,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;
}
/**
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..00137f83f52e 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,24 @@
package com.android.server.net.ipmemorystore;
+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.IOnStatusListener;
+import android.net.ipmemorystore.NetworkAttributes;
+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 +41,107 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+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;
+ }
+ };
}
@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();
+ final CountDownLatch latch = new CountDownLatch(1);
+ mService.storeNetworkAttributes(l2Key, na.build().toParcelable(),
+ onStatus(status -> {
+ assertTrue("Store status not successful : " + status.resultCode,
+ status.isSuccess());
+ latch.countDown();
+ }));
+ try {
+ latch.await(5000, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ fail("Did not complete storing attributes");
+ }
}
@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();
+ final CountDownLatch latch = new CountDownLatch(1);
+ mService.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
+ onStatus(status -> {
+ assertTrue("Store status not successful : " + status.resultCode,
+ status.isSuccess());
+ latch.countDown();
+ }));
+ try {
+ latch.await(5000, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ fail("Did not complete storing private data");
+ }
}
@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
}
}