diff options
author | Jason Monk <jmonk@google.com> | 2017-10-19 09:30:56 -0400 |
---|---|---|
committer | Jason Monk <jmonk@google.com> | 2017-10-19 09:30:56 -0400 |
commit | d439404c9988df6001e4ff8bce31537e2692660e (patch) | |
tree | b1462a7177b8a2791140964761eb49d173cdc878 /android/arch/persistence | |
parent | 93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6 (diff) | |
download | android-28-d439404c9988df6001e4ff8bce31537e2692660e.tar.gz |
Import Android SDK Platform P [4402356]
/google/data/ro/projects/android/fetch_artifact \
--bid 4386628 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4402356.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ie49e24e1f4ae9dc96306111e953d3db1e1495b53
Diffstat (limited to 'android/arch/persistence')
16 files changed, 621 insertions, 163 deletions
diff --git a/android/arch/persistence/room/InvalidationTracker.java b/android/arch/persistence/room/InvalidationTracker.java index 45ec0289..b31dc13a 100644 --- a/android/arch/persistence/room/InvalidationTracker.java +++ b/android/arch/persistence/room/InvalidationTracker.java @@ -219,7 +219,7 @@ public class InvalidationTracker { * * @param observer The observer which listens the database for changes. */ - public void addObserver(Observer observer) { + public void addObserver(@NonNull Observer observer) { final String[] tableNames = observer.mTables; int[] tableIds = new int[tableNames.length]; final int size = tableNames.length; @@ -265,7 +265,7 @@ public class InvalidationTracker { * @param observer The observer to remove. */ @SuppressWarnings("WeakerAccess") - public void removeObserver(final Observer observer) { + public void removeObserver(@NonNull final Observer observer) { ObserverWrapper wrapper; synchronized (mObserverMap) { wrapper = mObserverMap.remove(observer); diff --git a/android/arch/persistence/room/Relation.java b/android/arch/persistence/room/Relation.java index 72066992..d55bbfe8 100644 --- a/android/arch/persistence/room/Relation.java +++ b/android/arch/persistence/room/Relation.java @@ -28,6 +28,8 @@ import java.lang.annotation.Target; * <pre> * {@literal @}Entity * public class Pet { + * {@literal @} PrimaryKey + * int id; * int userId; * String name; * // other fields @@ -41,8 +43,8 @@ import java.lang.annotation.Target; * * {@literal @}Dao * public interface UserPetDao { - * {@literal @}Query("SELECT id, name from User WHERE age > :minAge") - * public List<UserNameAndAllPets> loadUserAndPets(int minAge); + * {@literal @}Query("SELECT id, name from User") + * public List<UserNameAndAllPets> loadUserAndPets(); * } * </pre> * <p> @@ -63,16 +65,16 @@ import java.lang.annotation.Target; * {@literal @}Embedded * public User user; * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class) - * public List<PetNameAndId> pets; + * public List<PetNameAndId> pets; * } * {@literal @}Dao * public interface UserPetDao { - * {@literal @}Query("SELECT * from User WHERE age > :minAge") - * public List<UserAllPets> loadUserAndPets(int minAge); + * {@literal @}Query("SELECT * from User") + * public List<UserAllPets> loadUserAndPets(); * } * </pre> * <p> - * In the example above, {@code PetNameAndId} is a regular but all of fields are fetched + * In the example above, {@code PetNameAndId} is a regular Pojo but all of fields are fetched * from the {@code entity} defined in the {@code @Relation} annotation (<i>Pet</i>). * {@code PetNameAndId} could also define its own relations all of which would also be fetched * automatically. @@ -85,7 +87,7 @@ import java.lang.annotation.Target; * public User user; * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class, * projection = {"name"}) - * public List<String> petNames; + * public List<String> petNames; * } * </pre> * <p> @@ -93,7 +95,7 @@ import java.lang.annotation.Target; * cannot have relations. This is a design decision to avoid common pitfalls in {@link Entity} * setups. You can read more about it in the main Room documentation. When loading data, you can * simply work around this limitation by creating Pojo classes that extend the {@link Entity}. - * + * <p> * Note that the {@code @Relation} annotated field cannot be a constructor parameter, it must be * public or have a public setter. */ diff --git a/android/arch/persistence/room/Room.java b/android/arch/persistence/room/Room.java index 8ce4be0c..2850b55e 100644 --- a/android/arch/persistence/room/Room.java +++ b/android/arch/persistence/room/Room.java @@ -43,6 +43,7 @@ public class Room { * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database. */ @SuppressWarnings("WeakerAccess") + @NonNull public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder( @NonNull Context context, @NonNull Class<T> klass, @NonNull String name) { //noinspection ConstantConditions @@ -65,6 +66,7 @@ public class Room { * @param <T> The type of the database class. * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database. */ + @NonNull public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder( @NonNull Context context, @NonNull Class<T> klass) { return new RoomDatabase.Builder<>(context, klass, null); diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java index cdad868d..8c940246 100644 --- a/android/arch/persistence/room/RoomDatabase.java +++ b/android/arch/persistence/room/RoomDatabase.java @@ -49,7 +49,7 @@ import java.util.concurrent.locks.ReentrantLock; * * @see Database */ -@SuppressWarnings({"unused", "WeakerAccess"}) +//@SuppressWarnings({"unused", "WeakerAccess"}) public abstract class RoomDatabase { private static final String DB_IMPL_SUFFIX = "_Impl"; // set by the generated open helper. @@ -153,7 +153,9 @@ public abstract class RoomDatabase { * * @hide */ + @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + // used in generated code public void assertNotMainThread() { if (mAllowMainThreadQueries) { return; @@ -298,6 +300,7 @@ public abstract class RoomDatabase { * @return True if there is an active transaction in current thread, false otherwise. * @see SupportSQLiteDatabase#inTransaction() */ + @SuppressWarnings("WeakerAccess") public boolean inTransaction() { return mOpenHelper.getWritableDatabase().inTransaction(); } @@ -307,7 +310,6 @@ public abstract class RoomDatabase { * * @param <T> The type of the abstract database class. */ - @SuppressWarnings("unused") public static class Builder<T extends RoomDatabase> { private final Class<T> mDatabaseClass; private final String mName; @@ -337,7 +339,8 @@ public abstract class RoomDatabase { * @param factory The factory to use to access the database. * @return this */ - public Builder<T> openHelperFactory(SupportSQLiteOpenHelper.Factory factory) { + @NonNull + public Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) { mFactory = factory; return this; } @@ -361,6 +364,7 @@ public abstract class RoomDatabase { * changes. * @return this */ + @NonNull public Builder<T> addMigrations(Migration... migrations) { mMigrationContainer.addMigrations(migrations); return this; @@ -378,6 +382,7 @@ public abstract class RoomDatabase { * * @return this */ + @NonNull public Builder<T> allowMainThreadQueries() { mAllowMainThreadQueries = true; return this; @@ -400,6 +405,7 @@ public abstract class RoomDatabase { * * @return this */ + @NonNull public Builder<T> fallbackToDestructiveMigration() { mRequireMigration = false; return this; @@ -411,6 +417,7 @@ public abstract class RoomDatabase { * @param callback The callback. * @return this */ + @NonNull public Builder<T> addCallback(@NonNull Callback callback) { if (mCallbacks == null) { mCallbacks = new ArrayList<>(); @@ -427,6 +434,7 @@ public abstract class RoomDatabase { * * @return A new database instance. */ + @NonNull public T build() { //noinspection ConstantConditions if (mContext == null) { @@ -493,6 +501,7 @@ public abstract class RoomDatabase { * @return An ordered list of {@link Migration} objects that should be run to migrate * between the given versions. If a migration path cannot be found, returns {@code null}. */ + @SuppressWarnings("WeakerAccess") @Nullable public List<Migration> findMigrationPath(int start, int end) { if (start == end) { diff --git a/android/arch/persistence/room/RoomWarnings.java b/android/arch/persistence/room/RoomWarnings.java index c64be967..f05e6be2 100644 --- a/android/arch/persistence/room/RoomWarnings.java +++ b/android/arch/persistence/room/RoomWarnings.java @@ -125,4 +125,12 @@ public class RoomWarnings { * annotation. */ public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR"; + + /** + * Reported when a @Query method returns a Pojo that has relations but the method is not + * annotated with @Transaction. Relations are run as separate queries and if the query is not + * run inside a transaction, it might return inconsistent results from the database. + */ + public static final String RELATION_QUERY_WITHOUT_TRANSACTION = + "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"; } diff --git a/android/arch/persistence/room/Transaction.java b/android/arch/persistence/room/Transaction.java index 914e4f41..3b6ede9c 100644 --- a/android/arch/persistence/room/Transaction.java +++ b/android/arch/persistence/room/Transaction.java @@ -22,9 +22,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Marks a method in an abstract {@link Dao} class as a transaction method. + * Marks a method in a {@link Dao} class as a transaction method. * <p> - * The derived implementation of the method will execute the super method in a database transaction. + * When used on a non-abstract method of an abstract {@link Dao} class, + * the derived implementation of the method will execute the super method in a database transaction. * All the parameters and return types are preserved. The transaction will be marked as successful * unless an exception is thrown in the method body. * <p> @@ -44,6 +45,38 @@ import java.lang.annotation.Target; * } * } * </pre> + * <p> + * When used on a {@link Query} method that has a {@code Select} statement, the generated code for + * the Query will be run in a transaction. There are 2 main cases where you may want to do that: + * <ol> + * <li>If the result of the query is fairly big, it is better to run it inside a transaction + * to receive a consistent result. Otherwise, if the query result does not fit into a single + * {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to + * changes in the database in between cursor window swaps. + * <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are + * queried separately. To receive consistent results between these queries, you probably want + * to run them in a single transaction. + * </ol> + * Example: + * <pre> + * class ProductWithReviews extends Product { + * {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class) + * public List<Review> reviews; + * } + * {@literal @}Dao + * public interface ProductDao { + * {@literal @}Transaction {@literal @}Query("SELECT * from products") + * public List<ProductWithReviews> loadAll(); + * } + * </pre> + * If the query is an async query (e.g. returns a {@link android.arch.lifecycle.LiveData LiveData} + * or RxJava Flowable, the transaction is properly handled when the query is run, not when the + * method is called. + * <p> + * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no + * impact because they are always run inside a transaction. Similarly, if it is annotated with + * {@link Query} but runs an update or delete statement, it is automatically wrapped in a + * transaction. */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.CLASS) diff --git a/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java b/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java index 818c46b4..cdd464e4 100644 --- a/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java +++ b/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java @@ -86,6 +86,7 @@ public class RoomPagedListActivity extends AppCompatActivity { @Override protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); PagedList<Customer> list = mAdapter.getCurrentList(); if (list == null) { // Can't find anything to restore diff --git a/android/arch/persistence/room/integration/testapp/database/CustomerDao.java b/android/arch/persistence/room/integration/testapp/database/CustomerDao.java index 9d402370..b5df914a 100644 --- a/android/arch/persistence/room/integration/testapp/database/CustomerDao.java +++ b/android/arch/persistence/room/integration/testapp/database/CustomerDao.java @@ -59,7 +59,7 @@ public interface CustomerDao { // Keyed - @Query("SELECT * from customer ORDER BY mLastName ASC LIMIT :limit") + @Query("SELECT * from customer ORDER BY mLastName DESC LIMIT :limit") List<Customer> customerNameInitial(int limit); @Query("SELECT * from customer WHERE mLastName < :key ORDER BY mLastName DESC LIMIT :limit") diff --git a/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java b/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java deleted file mode 100644 index 3cbffc8b..00000000 --- a/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.persistence.room.integration.testapp.db; - -import android.arch.persistence.db.SupportSQLiteDatabase; -import android.arch.persistence.db.SupportSQLiteOpenHelper; - -public class JDBCOpenHelper implements SupportSQLiteOpenHelper { - @Override - public String getDatabaseName() { - return null; - } - - @Override - public void setWriteAheadLoggingEnabled(boolean enabled) { - - } - - @Override - public SupportSQLiteDatabase getWritableDatabase() { - return null; - } - - @Override - public SupportSQLiteDatabase getReadableDatabase() { - return null; - } - - @Override - public void close() { - - } -} diff --git a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java index 84f20ec5..33f40183 100644 --- a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java +++ b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java @@ -17,20 +17,17 @@ package android.arch.persistence.room.integration.testapp.test; import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import android.arch.core.executor.ArchTaskExecutor; -import android.arch.core.executor.TaskExecutor; +import android.arch.core.executor.testing.CountingTaskExecutorRule; import android.arch.persistence.room.InvalidationTracker; import android.arch.persistence.room.Room; import android.arch.persistence.room.integration.testapp.TestDatabase; import android.arch.persistence.room.integration.testapp.dao.UserDao; import android.arch.persistence.room.integration.testapp.vo.User; import android.content.Context; -import android.os.Handler; -import android.os.Looper; import android.support.annotation.NonNull; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -38,17 +35,13 @@ import android.support.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Tests invalidation tracking. @@ -56,138 +49,97 @@ import java.util.concurrent.TimeUnit; @SmallTest @RunWith(AndroidJUnit4.class) public class InvalidationTest { + @Rule + public CountingTaskExecutorRule executorRule = new CountingTaskExecutorRule(); private UserDao mUserDao; private TestDatabase mDb; @Before - public void createDb() { + public void createDb() throws TimeoutException, InterruptedException { Context context = InstrumentationRegistry.getTargetContext(); mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build(); mUserDao = mDb.getUserDao(); - } - - @Before - public void setSingleThreadedIO() { - ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { - ExecutorService mIOExecutor = Executors.newSingleThreadExecutor(); - Handler mHandler = new Handler(Looper.getMainLooper()); - - @Override - public void executeOnDiskIO(Runnable runnable) { - mIOExecutor.execute(runnable); - } - - @Override - public void postToMainThread(Runnable runnable) { - mHandler.post(runnable); - } - - @Override - public boolean isMainThread() { - return Thread.currentThread() == Looper.getMainLooper().getThread(); - } - }); + drain(); } @After - public void clearExecutor() { - ArchTaskExecutor.getInstance().setDelegate(null); + public void closeDb() throws TimeoutException, InterruptedException { + mDb.close(); + drain(); } - private void waitUntilIOThreadIsIdle() { - FutureTask<Void> future = new FutureTask<>(new Callable<Void>() { - @Override - public Void call() throws Exception { - return null; - } - }); - ArchTaskExecutor.getInstance().executeOnDiskIO(future); - //noinspection TryWithIdenticalCatches - try { - future.get(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } + private void drain() throws TimeoutException, InterruptedException { + executorRule.drainTasks(1, TimeUnit.MINUTES); } @Test - public void testInvalidationOnUpdate() throws InterruptedException { + public void testInvalidationOnUpdate() throws InterruptedException, TimeoutException { User user = TestUtil.createUser(3); mUserDao.insert(user); - LatchObserver observer = new LatchObserver(1, "User"); + LoggingObserver observer = new LoggingObserver("User"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.updateById(3, "foo2"); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } @Test - public void testInvalidationOnDelete() throws InterruptedException { + public void testInvalidationOnDelete() throws InterruptedException, TimeoutException { User user = TestUtil.createUser(3); mUserDao.insert(user); - LatchObserver observer = new LatchObserver(1, "User"); + LoggingObserver observer = new LoggingObserver("User"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.delete(user); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } @Test - public void testInvalidationOnInsert() throws InterruptedException { - LatchObserver observer = new LatchObserver(1, "User"); + public void testInvalidationOnInsert() throws InterruptedException, TimeoutException { + LoggingObserver observer = new LoggingObserver("User"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.insert(TestUtil.createUser(3)); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } @Test - public void testDontInvalidateOnLateInsert() throws InterruptedException { - LatchObserver observer = new LatchObserver(1, "User"); + public void testDontInvalidateOnLateInsert() throws InterruptedException, TimeoutException { + LoggingObserver observer = new LoggingObserver("User"); mUserDao.insert(TestUtil.createUser(3)); - waitUntilIOThreadIsIdle(); + drain(); mDb.getInvalidationTracker().addObserver(observer); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(false)); + drain(); + assertThat(observer.getInvalidatedTables(), nullValue()); } @Test - public void testMultipleTables() throws InterruptedException { - LatchObserver observer = new LatchObserver(1, "User", "Pet"); + public void testMultipleTables() throws InterruptedException, TimeoutException { + LoggingObserver observer = new LoggingObserver("User", "Pet"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.insert(TestUtil.createUser(3)); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } - private static class LatchObserver extends InvalidationTracker.Observer { - CountDownLatch mLatch; - + private static class LoggingObserver extends InvalidationTracker.Observer { private Set<String> mInvalidatedTables; - LatchObserver(int permits, String... tables) { + LoggingObserver(String... tables) { super(tables); - mLatch = new CountDownLatch(permits); - } - - boolean await() throws InterruptedException { - return mLatch.await(5, TimeUnit.SECONDS); } @Override public void onInvalidated(@NonNull Set<String> tables) { mInvalidatedTables = tables; - mLatch.countDown(); } Set<String> getInvalidatedTables() { diff --git a/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java b/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java index e11117e4..2735c05a 100644 --- a/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java +++ b/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java @@ -166,17 +166,13 @@ public class QueryDataSourceTest extends TestDatabaseTest { p = dataSource.loadBefore(15, list.get(0), 10); assertNotNull(p); - for (User u : p) { - list.add(0, u); - } + list.addAll(0, p); assertArrayEquals(Arrays.copyOfRange(expected, 5, 35), list.toArray()); p = dataSource.loadBefore(5, list.get(0), 10); assertNotNull(p); - for (User u : p) { - list.add(0, u); - } + list.addAll(0, p); assertArrayEquals(Arrays.copyOfRange(expected, 0, 35), list.toArray()); } diff --git a/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java new file mode 100644 index 00000000..854c8627 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.persistence.room.integration.testapp.test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.arch.core.executor.ArchTaskExecutor; +import android.arch.core.executor.testing.CountingTaskExecutorRule; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Observer; +import android.arch.paging.LivePagedListProvider; +import android.arch.paging.PagedList; +import android.arch.paging.TiledDataSource; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Database; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.PrimaryKey; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Relation; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.RoomDatabase; +import android.arch.persistence.room.RoomWarnings; +import android.arch.persistence.room.Transaction; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.observers.TestObserver; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subscribers.TestSubscriber; + +@SmallTest +@RunWith(Parameterized.class) +public class QueryTransactionTest { + @Rule + public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule(); + private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0); + private TransactionDb mDb; + private final boolean mUseTransactionDao; + private Entity1Dao mDao; + private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest + .TestLifecycleOwner(); + + @NonNull + @Parameterized.Parameters(name = "useTransaction_{0}") + public static Boolean[] getParams() { + return new Boolean[]{false, true}; + } + + public QueryTransactionTest(boolean useTransactionDao) { + mUseTransactionDao = useTransactionDao; + } + + @Before + public void initDb() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START); + } + }); + + resetTransactionCount(); + mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), + TransactionDb.class).build(); + mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao(); + drain(); + } + + @After + public void closeDb() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY); + } + }); + drain(); + mDb.close(); + } + + @Test + public void readList() { + mDao.insert(new Entity1(1, "foo")); + resetTransactionCount(); + + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + List<Entity1> allEntities = mDao.allEntities(); + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void liveData() { + LiveData<List<Entity1>> listLiveData = mDao.liveData(); + observeForever(listLiveData); + drain(); + assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList())); + + resetTransactionCount(); + mDao.insert(new Entity1(1, "foo")); + drain(); + + //noinspection ConstantConditions + assertThat(listLiveData.getValue().size(), is(1)); + int expectedTransactionCount = mUseTransactionDao ? 2 : 1; + assertTransactionCount(listLiveData.getValue(), expectedTransactionCount); + } + + @Test + public void flowable() { + Flowable<List<Entity1>> flowable = mDao.flowable(); + TestSubscriber<List<Entity1>> subscriber = observe(flowable); + drain(); + assertThat(subscriber.values().size(), is(1)); + + resetTransactionCount(); + mDao.insert(new Entity1(1, "foo")); + drain(); + + List<Entity1> allEntities = subscriber.values().get(1); + assertThat(allEntities.size(), is(1)); + int expectedTransactionCount = mUseTransactionDao ? 2 : 1; + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void maybe() { + mDao.insert(new Entity1(1, "foo")); + resetTransactionCount(); + + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + Maybe<List<Entity1>> listMaybe = mDao.maybe(); + TestObserver<List<Entity1>> observer = observe(listMaybe); + drain(); + List<Entity1> allEntities = observer.values().get(0); + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void single() { + mDao.insert(new Entity1(1, "foo")); + resetTransactionCount(); + + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + Single<List<Entity1>> listMaybe = mDao.single(); + TestObserver<List<Entity1>> observer = observe(listMaybe); + drain(); + List<Entity1> allEntities = observer.values().get(0); + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void relation() { + mDao.insert(new Entity1(1, "foo")); + mDao.insert(new Child(1, 1)); + mDao.insert(new Child(2, 1)); + resetTransactionCount(); + + List<Entity1WithChildren> result = mDao.withRelation(); + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + assertTransactionCountWithChildren(result, expectedTransactionCount); + } + + @Test + public void pagedList() { + LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10); + observeForever(pagedList); + drain(); + assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0)); + + mDao.insert(new Entity1(1, "foo")); + drain(); + //noinspection ConstantConditions + assertThat(pagedList.getValue().size(), is(1)); + assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 2 : 1); + + mDao.insert(new Entity1(2, "bar")); + drain(); + assertThat(pagedList.getValue().size(), is(2)); + assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 4 : 2); + } + + @Test + public void dataSource() { + mDao.insert(new Entity1(2, "bar")); + drain(); + resetTransactionCount(); + TiledDataSource<Entity1> dataSource = mDao.dataSource(); + dataSource.loadRange(0, 10); + assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0)); + } + + private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) { + assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount)); + assertThat(allEntities.isEmpty(), is(false)); + for (Entity1 entity1 : allEntities) { + assertThat(entity1.transactionId, is(expectedTransactionCount)); + } + } + + private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities, + int expectedTransactionCount) { + assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount)); + assertThat(allEntities.isEmpty(), is(false)); + for (Entity1WithChildren entity1 : allEntities) { + assertThat(entity1.transactionId, is(expectedTransactionCount)); + assertThat(entity1.children, notNullValue()); + assertThat(entity1.children.isEmpty(), is(false)); + for (Child child : entity1.children) { + assertThat(child.transactionId, is(expectedTransactionCount)); + } + } + } + + private void resetTransactionCount() { + sStartedTransactionCount.set(0); + } + + private void drain() { + try { + countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new AssertionError("interrupted", e); + } catch (TimeoutException e) { + throw new AssertionError("drain timed out", e); + } + } + + private <T> TestSubscriber<T> observe(final Flowable<T> flowable) { + TestSubscriber<T> subscriber = new TestSubscriber<>(); + flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor())) + .subscribeWith(subscriber); + return subscriber; + } + + private <T> TestObserver<T> observe(final Maybe<T> maybe) { + TestObserver<T> observer = new TestObserver<>(); + maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor())) + .subscribeWith(observer); + return observer; + } + + private <T> TestObserver<T> observe(final Single<T> single) { + TestObserver<T> observer = new TestObserver<>(); + single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor())) + .subscribeWith(observer); + return observer; + } + + private <T> void observeForever(final LiveData<T> liveData) { + FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() { + @Override + public Void call() throws Exception { + liveData.observe(mLifecycleOwner, new Observer<T>() { + @Override + public void onChanged(@Nullable T t) { + + } + }); + return null; + } + }); + ArchTaskExecutor.getMainThreadExecutor().execute(futureTask); + try { + futureTask.get(); + } catch (InterruptedException e) { + throw new AssertionError("interrupted", e); + } catch (ExecutionException e) { + throw new AssertionError("execution error", e); + } + } + + @SuppressWarnings("WeakerAccess") + static class Entity1WithChildren extends Entity1 { + @Relation(entity = Child.class, parentColumn = "id", + entityColumn = "entity1Id") + public List<Child> children; + + Entity1WithChildren(int id, String value) { + super(id, value); + } + } + + @SuppressWarnings("WeakerAccess") + @Entity + static class Child { + @PrimaryKey(autoGenerate = true) + public int id; + public int entity1Id; + @Ignore + public final int transactionId = sStartedTransactionCount.get(); + + Child(int id, int entity1Id) { + this.id = id; + this.entity1Id = entity1Id; + } + } + + @SuppressWarnings("WeakerAccess") + @Entity + static class Entity1 { + @PrimaryKey(autoGenerate = true) + public int id; + public String value; + @Ignore + public final int transactionId = sStartedTransactionCount.get(); + + Entity1(int id, String value) { + this.id = id; + this.value = value; + } + } + + // we don't support dao inheritance for queries so for now, go with this + interface Entity1Dao { + String SELECT_ALL = "select * from Entity1"; + + List<Entity1> allEntities(); + + Flowable<List<Entity1>> flowable(); + + Maybe<List<Entity1>> maybe(); + + Single<List<Entity1>> single(); + + LiveData<List<Entity1>> liveData(); + + List<Entity1WithChildren> withRelation(); + + LivePagedListProvider<Integer, Entity1> pagedList(); + + TiledDataSource<Entity1> dataSource(); + + @Insert + void insert(Entity1 entity1); + + @Insert + void insert(Child entity1); + } + + @Dao + interface EntityDao extends Entity1Dao { + @Override + @Query(SELECT_ALL) + List<Entity1> allEntities(); + + @Override + @Query(SELECT_ALL) + Flowable<List<Entity1>> flowable(); + + @Override + @Query(SELECT_ALL) + LiveData<List<Entity1>> liveData(); + + @Override + @Query(SELECT_ALL) + Maybe<List<Entity1>> maybe(); + + @Override + @Query(SELECT_ALL) + Single<List<Entity1>> single(); + + @Override + @Query(SELECT_ALL) + @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION) + List<Entity1WithChildren> withRelation(); + + @Override + @Query(SELECT_ALL) + LivePagedListProvider<Integer, Entity1> pagedList(); + + @Override + @Query(SELECT_ALL) + TiledDataSource<Entity1> dataSource(); + } + + @Dao + interface TransactionDao extends Entity1Dao { + @Override + @Transaction + @Query(SELECT_ALL) + List<Entity1> allEntities(); + + @Override + @Transaction + @Query(SELECT_ALL) + Flowable<List<Entity1>> flowable(); + + @Override + @Transaction + @Query(SELECT_ALL) + LiveData<List<Entity1>> liveData(); + + @Override + @Transaction + @Query(SELECT_ALL) + Maybe<List<Entity1>> maybe(); + + @Override + @Transaction + @Query(SELECT_ALL) + Single<List<Entity1>> single(); + + @Override + @Transaction + @Query(SELECT_ALL) + List<Entity1WithChildren> withRelation(); + + @Override + @Transaction + @Query(SELECT_ALL) + LivePagedListProvider<Integer, Entity1> pagedList(); + + @Override + @Transaction + @Query(SELECT_ALL) + TiledDataSource<Entity1> dataSource(); + } + + @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false) + abstract static class TransactionDb extends RoomDatabase { + abstract EntityDao dao(); + + abstract TransactionDao transactionDao(); + + @Override + public void beginTransaction() { + super.beginTransaction(); + sStartedTransactionCount.incrementAndGet(); + } + } +} diff --git a/android/arch/persistence/room/migration/Migration.java b/android/arch/persistence/room/migration/Migration.java index 907e624b..d69ea0dc 100644 --- a/android/arch/persistence/room/migration/Migration.java +++ b/android/arch/persistence/room/migration/Migration.java @@ -17,6 +17,7 @@ package android.arch.persistence.room.migration; import android.arch.persistence.db.SupportSQLiteDatabase; +import android.support.annotation.NonNull; /** * Base class for a database migration. @@ -58,5 +59,5 @@ public abstract class Migration { * * @param database The database instance */ - public abstract void migrate(SupportSQLiteDatabase database); + public abstract void migrate(@NonNull SupportSQLiteDatabase database); } diff --git a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java index 1467a4f0..d72cf8cb 100644 --- a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java +++ b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java @@ -16,13 +16,18 @@ package android.arch.persistence.room.migration.bundle; +import android.support.annotation.RestrictTo; + import com.google.gson.annotations.SerializedName; import java.util.List; /** * Holds the information about a foreign key reference. + * + * @hide */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ForeignKeyBundle { @SerializedName("table") private String mTable; diff --git a/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/android/arch/persistence/room/paging/LimitOffsetDataSource.java index 800514cc..2f9a8882 100644 --- a/android/arch/persistence/room/paging/LimitOffsetDataSource.java +++ b/android/arch/persistence/room/paging/LimitOffsetDataSource.java @@ -49,10 +49,13 @@ public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> { private final RoomDatabase mDb; @SuppressWarnings("FieldCanBeLocal") private final InvalidationTracker.Observer mObserver; + private final boolean mInTransaction; - protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, String... tables) { + protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, + boolean inTransaction, String... tables) { mDb = db; mSourceQuery = query; + mInTransaction = inTransaction; mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )"; mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?"; mObserver = new InvalidationTracker.Observer(tables) { @@ -98,13 +101,30 @@ public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> { sqLiteQuery.copyArgumentsFrom(mSourceQuery); sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount); sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition); - Cursor cursor = mDb.query(sqLiteQuery); - - try { - return convertRows(cursor); - } finally { - cursor.close(); - sqLiteQuery.release(); + if (mInTransaction) { + mDb.beginTransaction(); + Cursor cursor = null; + try { + cursor = mDb.query(sqLiteQuery); + List<T> rows = convertRows(cursor); + mDb.setTransactionSuccessful(); + return rows; + } finally { + if (cursor != null) { + cursor.close(); + } + mDb.endTransaction(); + sqLiteQuery.release(); + } + } else { + Cursor cursor = mDb.query(sqLiteQuery); + //noinspection TryFinallyCanBeTryWithResources + try { + return convertRows(cursor); + } finally { + cursor.close(); + sqLiteQuery.release(); + } } } } diff --git a/android/arch/persistence/room/util/StringUtil.java b/android/arch/persistence/room/util/StringUtil.java index bee05ddd..d01e3c53 100644 --- a/android/arch/persistence/room/util/StringUtil.java +++ b/android/arch/persistence/room/util/StringUtil.java @@ -17,6 +17,7 @@ package android.arch.persistence.room.util; import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; import android.util.Log; import java.util.ArrayList; @@ -24,10 +25,14 @@ import java.util.List; import java.util.StringTokenizer; /** + * @hide + * * String utilities for Room */ -@SuppressWarnings("WeakerAccess") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class StringUtil { + + @SuppressWarnings("unused") public static final String[] EMPTY_STRING_ARRAY = new String[0]; /** * Returns a new StringBuilder to be used while producing SQL queries. |