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

Commit 91549b6d authored by Chalard Jean's avatar Chalard Jean
Browse files

[MS07] Implement storeNetworkAttributes and storeBlob.

Test: New tests in IpMemoryStore
Bug: 113554482

Change-Id: I49bee0c903247e77ab93517efbe44548313cf1a4
parent db4ce870
Loading
Loading
Loading
Loading
+13 −2
Original line number Diff line number Diff line
@@ -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 ?!";
        }
    }
+13 −6
Original line number Diff line number Diff line
@@ -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();
+135 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+87 −7
Original line number Diff line number Diff line
@@ -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;
    }

    /**
+86 −9
Original line number Diff line number Diff line
@@ -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
    }
}