Loading core/java/android/net/ipmemorystore/Status.java +13 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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); } Loading @@ -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 ?!"; } } Loading core/java/android/net/ipmemorystore/Utils.java +13 −6 Original line number Diff line number Diff line Loading @@ -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(); Loading services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java +231 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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. */ Loading Loading @@ -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 + " (" Loading Loading @@ -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) { Loading Loading @@ -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; } } services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java +137 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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. * Loading @@ -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 } }); } /** Loading @@ -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; } /** Loading Loading @@ -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 } }); } /** Loading @@ -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 } }); } } tests/net/java/android/net/ipmemorystore/ParcelableTests.java +7 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading
core/java/android/net/ipmemorystore/Status.java +13 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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); } Loading @@ -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 ?!"; } } Loading
core/java/android/net/ipmemorystore/Utils.java +13 −6 Original line number Diff line number Diff line Loading @@ -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(); Loading
services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java +231 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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. */ Loading Loading @@ -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 + " (" Loading Loading @@ -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) { Loading Loading @@ -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; } }
services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java +137 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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. * Loading @@ -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 } }); } /** Loading @@ -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; } /** Loading Loading @@ -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 } }); } /** Loading @@ -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 } }); } }
tests/net/java/android/net/ipmemorystore/ParcelableTests.java +7 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading