diff options
| author | 2018-12-18 22:05:19 +0900 | |
|---|---|---|
| committer | 2019-01-21 15:21:09 +0900 | |
| commit | 91549b6d1be1e0d8d0deb9da45050eb76165a39d (patch) | |
| tree | 26b587a60864fa43024d6516aa779ddc73fe5cd4 | |
| parent | db4ce8705911e36a36f0a2f36b8bf9e04d78a2d3 (diff) | |
[MS07] Implement storeNetworkAttributes and storeBlob.
Test: New tests in IpMemoryStore
Bug: 113554482
Change-Id: I49bee0c903247e77ab93517efbe44548313cf1a4
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 } } |