Home | History | Annotate | Download | only in room
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.room;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.app.ActivityManager;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.os.Build;
     24 import android.util.Log;
     25 
     26 import androidx.annotation.CallSuper;
     27 import androidx.annotation.NonNull;
     28 import androidx.annotation.Nullable;
     29 import androidx.annotation.RequiresApi;
     30 import androidx.annotation.RestrictTo;
     31 import androidx.annotation.WorkerThread;
     32 import androidx.collection.SparseArrayCompat;
     33 import androidx.core.app.ActivityManagerCompat;
     34 import androidx.arch.core.executor.ArchTaskExecutor;
     35 import androidx.room.migration.Migration;
     36 import androidx.sqlite.db.SimpleSQLiteQuery;
     37 import androidx.sqlite.db.SupportSQLiteDatabase;
     38 import androidx.sqlite.db.SupportSQLiteOpenHelper;
     39 import androidx.sqlite.db.SupportSQLiteQuery;
     40 import androidx.sqlite.db.SupportSQLiteStatement;
     41 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
     42 
     43 import java.util.ArrayList;
     44 import java.util.Collections;
     45 import java.util.HashSet;
     46 import java.util.List;
     47 import java.util.Set;
     48 import java.util.concurrent.Callable;
     49 import java.util.concurrent.locks.Lock;
     50 import java.util.concurrent.locks.ReentrantLock;
     51 
     52 /**
     53  * Base class for all Room databases. All classes that are annotated with {@link Database} must
     54  * extend this class.
     55  * <p>
     56  * RoomDatabase provides direct access to the underlying database implementation but you should
     57  * prefer using {@link Dao} classes.
     58  *
     59  * @see Database
     60  */
     61 //@SuppressWarnings({"unused", "WeakerAccess"})
     62 public abstract class RoomDatabase {
     63     private static final String DB_IMPL_SUFFIX = "_Impl";
     64     /**
     65      * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
     66      *
     67      * @hide
     68      */
     69     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     70     public static final int MAX_BIND_PARAMETER_CNT = 999;
     71     // set by the generated open helper.
     72     protected volatile SupportSQLiteDatabase mDatabase;
     73     private SupportSQLiteOpenHelper mOpenHelper;
     74     private final InvalidationTracker mInvalidationTracker;
     75     private boolean mAllowMainThreadQueries;
     76     boolean mWriteAheadLoggingEnabled;
     77 
     78     @Nullable
     79     protected List<Callback> mCallbacks;
     80 
     81     private final ReentrantLock mCloseLock = new ReentrantLock();
     82 
     83     /**
     84      * {@link InvalidationTracker} uses this lock to prevent the database from closing while it is
     85      * querying database updates.
     86      *
     87      * @return The lock for {@link #close()}.
     88      */
     89     Lock getCloseLock() {
     90         return mCloseLock;
     91     }
     92 
     93     /**
     94      * Creates a RoomDatabase.
     95      * <p>
     96      * You cannot create an instance of a database, instead, you should acquire it via
     97      * {@link Room#databaseBuilder(Context, Class, String)} or
     98      * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
     99      */
    100     public RoomDatabase() {
    101         mInvalidationTracker = createInvalidationTracker();
    102     }
    103 
    104     /**
    105      * Called by {@link Room} when it is initialized.
    106      *
    107      * @param configuration The database configuration.
    108      */
    109     @CallSuper
    110     public void init(@NonNull DatabaseConfiguration configuration) {
    111         mOpenHelper = createOpenHelper(configuration);
    112         boolean wal = false;
    113         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    114             wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING;
    115             mOpenHelper.setWriteAheadLoggingEnabled(wal);
    116         }
    117         mCallbacks = configuration.callbacks;
    118         mAllowMainThreadQueries = configuration.allowMainThreadQueries;
    119         mWriteAheadLoggingEnabled = wal;
    120     }
    121 
    122     /**
    123      * Returns the SQLite open helper used by this database.
    124      *
    125      * @return The SQLite open helper used by this database.
    126      */
    127     @NonNull
    128     public SupportSQLiteOpenHelper getOpenHelper() {
    129         return mOpenHelper;
    130     }
    131 
    132     /**
    133      * Creates the open helper to access the database. Generated class already implements this
    134      * method.
    135      * Note that this method is called when the RoomDatabase is initialized.
    136      *
    137      * @param config The configuration of the Room database.
    138      * @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
    139      */
    140     @NonNull
    141     protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
    142 
    143     /**
    144      * Called when the RoomDatabase is created.
    145      * <p>
    146      * This is already implemented by the generated code.
    147      *
    148      * @return Creates a new InvalidationTracker.
    149      */
    150     @NonNull
    151     protected abstract InvalidationTracker createInvalidationTracker();
    152 
    153     /**
    154      * Deletes all rows from all the tables that are registered to this database as
    155      * {@link Database#entities()}.
    156      * <p>
    157      * This does NOT reset the auto-increment value generated by {@link PrimaryKey#autoGenerate()}.
    158      * <p>
    159      * After deleting the rows, Room will set a WAL checkpoint and run VACUUM. This means that the
    160      * data is completely erased. The space will be reclaimed by the system if the amount surpasses
    161      * the threshold of database file size.
    162      *
    163      * @see <a href="https://www.sqlite.org/fileformat.html">Database File Format</a>
    164      */
    165     @WorkerThread
    166     public abstract void clearAllTables();
    167 
    168     /**
    169      * Returns true if database connection is open and initialized.
    170      *
    171      * @return true if the database connection is open, false otherwise.
    172      */
    173     public boolean isOpen() {
    174         final SupportSQLiteDatabase db = mDatabase;
    175         return db != null && db.isOpen();
    176     }
    177 
    178     /**
    179      * Closes the database if it is already open.
    180      */
    181     public void close() {
    182         if (isOpen()) {
    183             try {
    184                 mCloseLock.lock();
    185                 mOpenHelper.close();
    186             } finally {
    187                 mCloseLock.unlock();
    188             }
    189         }
    190     }
    191 
    192     /**
    193      * Asserts that we are not on the main thread.
    194      *
    195      * @hide
    196      */
    197     @SuppressWarnings("WeakerAccess")
    198     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    199     // used in generated code
    200     public void assertNotMainThread() {
    201         if (mAllowMainThreadQueries) {
    202             return;
    203         }
    204         if (ArchTaskExecutor.getInstance().isMainThread()) {
    205             throw new IllegalStateException("Cannot access database on the main thread since"
    206                     + " it may potentially lock the UI for a long period of time.");
    207         }
    208     }
    209 
    210     // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
    211     // methods we are using and also helps unit tests to mock this class without mocking
    212     // all SQLite database methods.
    213 
    214     /**
    215      * Convenience method to query the database with arguments.
    216      *
    217      * @param query The sql query
    218      * @param args The bind arguments for the placeholders in the query
    219      *
    220      * @return A Cursor obtained by running the given query in the Room database.
    221      */
    222     public Cursor query(String query, @Nullable Object[] args) {
    223         return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
    224     }
    225 
    226     /**
    227      * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
    228      *
    229      * @param query The Query which includes the SQL and a bind callback for bind arguments.
    230      * @return Result of the query.
    231      */
    232     public Cursor query(SupportSQLiteQuery query) {
    233         assertNotMainThread();
    234         return mOpenHelper.getWritableDatabase().query(query);
    235     }
    236 
    237     /**
    238      * Wrapper for {@link SupportSQLiteDatabase#compileStatement(String)}.
    239      *
    240      * @param sql The query to compile.
    241      * @return The compiled query.
    242      */
    243     public SupportSQLiteStatement compileStatement(@NonNull String sql) {
    244         assertNotMainThread();
    245         return mOpenHelper.getWritableDatabase().compileStatement(sql);
    246     }
    247 
    248     /**
    249      * Wrapper for {@link SupportSQLiteDatabase#beginTransaction()}.
    250      */
    251     public void beginTransaction() {
    252         assertNotMainThread();
    253         SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
    254         mInvalidationTracker.syncTriggers(database);
    255         database.beginTransaction();
    256     }
    257 
    258     /**
    259      * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}.
    260      */
    261     public void endTransaction() {
    262         mOpenHelper.getWritableDatabase().endTransaction();
    263         if (!inTransaction()) {
    264             // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last
    265             // endTransaction call to do it.
    266             mInvalidationTracker.refreshVersionsAsync();
    267         }
    268     }
    269 
    270     /**
    271      * Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
    272      */
    273     public void setTransactionSuccessful() {
    274         mOpenHelper.getWritableDatabase().setTransactionSuccessful();
    275     }
    276 
    277     /**
    278      * Executes the specified {@link Runnable} in a database transaction. The transaction will be
    279      * marked as successful unless an exception is thrown in the {@link Runnable}.
    280      *
    281      * @param body The piece of code to execute.
    282      */
    283     public void runInTransaction(@NonNull Runnable body) {
    284         beginTransaction();
    285         try {
    286             body.run();
    287             setTransactionSuccessful();
    288         } finally {
    289             endTransaction();
    290         }
    291     }
    292 
    293     /**
    294      * Executes the specified {@link Callable} in a database transaction. The transaction will be
    295      * marked as successful unless an exception is thrown in the {@link Callable}.
    296      *
    297      * @param body The piece of code to execute.
    298      * @param <V>  The type of the return value.
    299      * @return The value returned from the {@link Callable}.
    300      */
    301     public <V> V runInTransaction(@NonNull Callable<V> body) {
    302         beginTransaction();
    303         try {
    304             V result = body.call();
    305             setTransactionSuccessful();
    306             return result;
    307         } catch (RuntimeException e) {
    308             throw e;
    309         } catch (Exception e) {
    310             throw new RuntimeException("Exception in transaction", e);
    311         } finally {
    312             endTransaction();
    313         }
    314     }
    315 
    316     /**
    317      * Called by the generated code when database is open.
    318      * <p>
    319      * You should never call this method manually.
    320      *
    321      * @param db The database instance.
    322      */
    323     protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
    324         mInvalidationTracker.internalInit(db);
    325     }
    326 
    327     /**
    328      * Returns the invalidation tracker for this database.
    329      * <p>
    330      * You can use the invalidation tracker to get notified when certain tables in the database
    331      * are modified.
    332      *
    333      * @return The invalidation tracker for the database.
    334      */
    335     @NonNull
    336     public InvalidationTracker getInvalidationTracker() {
    337         return mInvalidationTracker;
    338     }
    339 
    340     /**
    341      * Returns true if current thread is in a transaction.
    342      *
    343      * @return True if there is an active transaction in current thread, false otherwise.
    344      * @see SupportSQLiteDatabase#inTransaction()
    345      */
    346     @SuppressWarnings("WeakerAccess")
    347     public boolean inTransaction() {
    348         return mOpenHelper.getWritableDatabase().inTransaction();
    349     }
    350 
    351     /**
    352      * Journal modes for SQLite database.
    353      *
    354      * @see RoomDatabase.Builder#setJournalMode(JournalMode)
    355      */
    356     public enum JournalMode {
    357 
    358         /**
    359          * Let Room choose the journal mode. This is the default value when no explicit value is
    360          * specified.
    361          * <p>
    362          * The actual value will be {@link #TRUNCATE} when the device runs API Level lower than 16
    363          * or it is a low-RAM device. Otherwise, {@link #WRITE_AHEAD_LOGGING} will be used.
    364          */
    365         AUTOMATIC,
    366 
    367         /**
    368          * Truncate journal mode.
    369          */
    370         TRUNCATE,
    371 
    372         /**
    373          * Write-Ahead Logging mode.
    374          */
    375         @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
    376         WRITE_AHEAD_LOGGING;
    377 
    378         /**
    379          * Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or
    380          * {@link #WRITE_AHEAD_LOGGING}.
    381          */
    382         @SuppressLint("NewApi")
    383         JournalMode resolve(Context context) {
    384             if (this != AUTOMATIC) {
    385                 return this;
    386             }
    387             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    388                 ActivityManager manager = (ActivityManager)
    389                         context.getSystemService(Context.ACTIVITY_SERVICE);
    390                 if (manager != null && !ActivityManagerCompat.isLowRamDevice(manager)) {
    391                     return WRITE_AHEAD_LOGGING;
    392                 }
    393             }
    394             return TRUNCATE;
    395         }
    396     }
    397 
    398     /**
    399      * Builder for RoomDatabase.
    400      *
    401      * @param <T> The type of the abstract database class.
    402      */
    403     public static class Builder<T extends RoomDatabase> {
    404         private final Class<T> mDatabaseClass;
    405         private final String mName;
    406         private final Context mContext;
    407         private ArrayList<Callback> mCallbacks;
    408 
    409         private SupportSQLiteOpenHelper.Factory mFactory;
    410         private boolean mAllowMainThreadQueries;
    411         private JournalMode mJournalMode;
    412         private boolean mRequireMigration;
    413         /**
    414          * Migrations, mapped by from-to pairs.
    415          */
    416         private final MigrationContainer mMigrationContainer;
    417         private Set<Integer> mMigrationsNotRequiredFrom;
    418         /**
    419          * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
    420          * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
    421          * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(int...)}.
    422          */
    423         private Set<Integer> mMigrationStartAndEndVersions;
    424 
    425         Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
    426             mContext = context;
    427             mDatabaseClass = klass;
    428             mName = name;
    429             mJournalMode = JournalMode.AUTOMATIC;
    430             mRequireMigration = true;
    431             mMigrationContainer = new MigrationContainer();
    432         }
    433 
    434         /**
    435          * Sets the database factory. If not set, it defaults to
    436          * {@link FrameworkSQLiteOpenHelperFactory}.
    437          *
    438          * @param factory The factory to use to access the database.
    439          * @return this
    440          */
    441         @NonNull
    442         public Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) {
    443             mFactory = factory;
    444             return this;
    445         }
    446 
    447         /**
    448          * Adds a migration to the builder.
    449          * <p>
    450          * Each Migration has a start and end versions and Room runs these migrations to bring the
    451          * database to the latest version.
    452          * <p>
    453          * If a migration item is missing between current version and the latest version, Room
    454          * will clear the database and recreate so even if you have no changes between 2 versions,
    455          * you should still provide a Migration object to the builder.
    456          * <p>
    457          * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
    458          * going version 3 to 5 without going to version 4). If Room opens a database at version
    459          * 3 and latest version is &gt;= 5, Room will use the migration object that can migrate from
    460          * 3 to 5 instead of 3 to 4 and 4 to 5.
    461          *
    462          * @param migrations The migration object that can modify the database and to the necessary
    463          *                   changes.
    464          * @return this
    465          */
    466         @NonNull
    467         public Builder<T> addMigrations(@NonNull  Migration... migrations) {
    468             if (mMigrationStartAndEndVersions == null) {
    469                 mMigrationStartAndEndVersions = new HashSet<>();
    470             }
    471             for (Migration migration: migrations) {
    472                 mMigrationStartAndEndVersions.add(migration.startVersion);
    473                 mMigrationStartAndEndVersions.add(migration.endVersion);
    474             }
    475 
    476             mMigrationContainer.addMigrations(migrations);
    477             return this;
    478         }
    479 
    480         /**
    481          * Disables the main thread query check for Room.
    482          * <p>
    483          * Room ensures that Database is never accessed on the main thread because it may lock the
    484          * main thread and trigger an ANR. If you need to access the database from the main thread,
    485          * you should always use async alternatives or manually move the call to a background
    486          * thread.
    487          * <p>
    488          * You may want to turn this check off for testing.
    489          *
    490          * @return this
    491          */
    492         @NonNull
    493         public Builder<T> allowMainThreadQueries() {
    494             mAllowMainThreadQueries = true;
    495             return this;
    496         }
    497 
    498         /**
    499          * Sets the journal mode for this database.
    500          *
    501          * <p>
    502          * This value is ignored if the builder is initialized with
    503          * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
    504          * <p>
    505          * The journal mode should be consistent across multiple instances of
    506          * {@link RoomDatabase} for a single SQLite database file.
    507          * <p>
    508          * The default value is {@link JournalMode#AUTOMATIC}.
    509          *
    510          * @param journalMode The journal mode.
    511          * @return this
    512          */
    513         @NonNull
    514         public Builder<T> setJournalMode(@NonNull JournalMode journalMode) {
    515             mJournalMode = journalMode;
    516             return this;
    517         }
    518 
    519         /**
    520          * Allows Room to destructively recreate database tables if {@link Migration}s that would
    521          * migrate old database schemas to the latest schema version are not found.
    522          * <p>
    523          * When the database version on the device does not match the latest schema version, Room
    524          * runs necessary {@link Migration}s on the database.
    525          * <p>
    526          * If it cannot find the set of {@link Migration}s that will bring the database to the
    527          * current version, it will throw an {@link IllegalStateException}.
    528          * <p>
    529          * You can call this method to change this behavior to re-create the database instead of
    530          * crashing.
    531          * <p>
    532          * Note that this will delete all of the data in the database tables managed by Room.
    533          *
    534          * @return this
    535          */
    536         @NonNull
    537         public Builder<T> fallbackToDestructiveMigration() {
    538             mRequireMigration = false;
    539             return this;
    540         }
    541 
    542         /**
    543          * Informs Room that it is allowed to destructively recreate database tables from specific
    544          * starting schema versions.
    545          * <p>
    546          * This functionality is the same as that provided by
    547          * {@link #fallbackToDestructiveMigration()}, except that this method allows the
    548          * specification of a set of schema versions for which destructive recreation is allowed.
    549          * <p>
    550          * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
    551          * to allow destructive migrations from some schema versions while still taking advantage
    552          * of exceptions being thrown due to unintentionally missing migrations.
    553          * <p>
    554          * Note: No versions passed to this method may also exist as either starting or ending
    555          * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
    556          * version passed to this method is found as a starting or ending version in a Migration, an
    557          * exception will be thrown.
    558          *
    559          * @param startVersions The set of schema versions from which Room should use a destructive
    560          *                      migration.
    561          * @return this
    562          */
    563         @NonNull
    564         public Builder<T> fallbackToDestructiveMigrationFrom(int... startVersions) {
    565             if (mMigrationsNotRequiredFrom == null) {
    566                 mMigrationsNotRequiredFrom = new HashSet<>(startVersions.length);
    567             }
    568             for (int startVersion : startVersions) {
    569                 mMigrationsNotRequiredFrom.add(startVersion);
    570             }
    571             return this;
    572         }
    573 
    574         /**
    575          * Adds a {@link Callback} to this database.
    576          *
    577          * @param callback The callback.
    578          * @return this
    579          */
    580         @NonNull
    581         public Builder<T> addCallback(@NonNull Callback callback) {
    582             if (mCallbacks == null) {
    583                 mCallbacks = new ArrayList<>();
    584             }
    585             mCallbacks.add(callback);
    586             return this;
    587         }
    588 
    589         /**
    590          * Creates the databases and initializes it.
    591          * <p>
    592          * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
    593          * triggers.
    594          *
    595          * @return A new database instance.
    596          */
    597         @NonNull
    598         public T build() {
    599             //noinspection ConstantConditions
    600             if (mContext == null) {
    601                 throw new IllegalArgumentException("Cannot provide null context for the database.");
    602             }
    603             //noinspection ConstantConditions
    604             if (mDatabaseClass == null) {
    605                 throw new IllegalArgumentException("Must provide an abstract class that"
    606                         + " extends RoomDatabase");
    607             }
    608 
    609             if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
    610                 for (Integer version : mMigrationStartAndEndVersions) {
    611                     if (mMigrationsNotRequiredFrom.contains(version)) {
    612                         throw new IllegalArgumentException(
    613                                 "Inconsistency detected. A Migration was supplied to "
    614                                         + "addMigration(Migration... migrations) that has a start "
    615                                         + "or end version equal to a start version supplied to "
    616                                         + "fallbackToDestructiveMigrationFrom(int... "
    617                                         + "startVersions). Start version: "
    618                                         + version);
    619                     }
    620                 }
    621             }
    622 
    623             if (mFactory == null) {
    624                 mFactory = new FrameworkSQLiteOpenHelperFactory();
    625             }
    626             DatabaseConfiguration configuration =
    627                     new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
    628                             mCallbacks, mAllowMainThreadQueries,
    629                             mJournalMode.resolve(mContext),
    630                             mRequireMigration, mMigrationsNotRequiredFrom);
    631             T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
    632             db.init(configuration);
    633             return db;
    634         }
    635     }
    636 
    637     /**
    638      * A container to hold migrations. It also allows querying its contents to find migrations
    639      * between two versions.
    640      */
    641     public static class MigrationContainer {
    642         private SparseArrayCompat<SparseArrayCompat<Migration>> mMigrations =
    643                 new SparseArrayCompat<>();
    644 
    645         /**
    646          * Adds the given migrations to the list of available migrations. If 2 migrations have the
    647          * same start-end versions, the latter migration overrides the previous one.
    648          *
    649          * @param migrations List of available migrations.
    650          */
    651         public void addMigrations(@NonNull Migration... migrations) {
    652             for (Migration migration : migrations) {
    653                 addMigration(migration);
    654             }
    655         }
    656 
    657         private void addMigration(Migration migration) {
    658             final int start = migration.startVersion;
    659             final int end = migration.endVersion;
    660             SparseArrayCompat<Migration> targetMap = mMigrations.get(start);
    661             if (targetMap == null) {
    662                 targetMap = new SparseArrayCompat<>();
    663                 mMigrations.put(start, targetMap);
    664             }
    665             Migration existing = targetMap.get(end);
    666             if (existing != null) {
    667                 Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
    668             }
    669             targetMap.append(end, migration);
    670         }
    671 
    672         /**
    673          * Finds the list of migrations that should be run to move from {@code start} version to
    674          * {@code end} version.
    675          *
    676          * @param start The current database version
    677          * @param end   The target database version
    678          * @return An ordered list of {@link Migration} objects that should be run to migrate
    679          * between the given versions. If a migration path cannot be found, returns {@code null}.
    680          */
    681         @SuppressWarnings("WeakerAccess")
    682         @Nullable
    683         public List<Migration> findMigrationPath(int start, int end) {
    684             if (start == end) {
    685                 return Collections.emptyList();
    686             }
    687             boolean migrateUp = end > start;
    688             List<Migration> result = new ArrayList<>();
    689             return findUpMigrationPath(result, migrateUp, start, end);
    690         }
    691 
    692         private List<Migration> findUpMigrationPath(List<Migration> result, boolean upgrade,
    693                 int start, int end) {
    694             final int searchDirection = upgrade ? -1 : 1;
    695             while (upgrade ? start < end : start > end) {
    696                 SparseArrayCompat<Migration> targetNodes = mMigrations.get(start);
    697                 if (targetNodes == null) {
    698                     return null;
    699                 }
    700                 // keys are ordered so we can start searching from one end of them.
    701                 final int size = targetNodes.size();
    702                 final int firstIndex;
    703                 final int lastIndex;
    704 
    705                 if (upgrade) {
    706                     firstIndex = size - 1;
    707                     lastIndex = -1;
    708                 } else {
    709                     firstIndex = 0;
    710                     lastIndex = size;
    711                 }
    712                 boolean found = false;
    713                 for (int i = firstIndex; i != lastIndex; i += searchDirection) {
    714                     final int targetVersion = targetNodes.keyAt(i);
    715                     final boolean shouldAddToPath;
    716                     if (upgrade) {
    717                         shouldAddToPath = targetVersion <= end && targetVersion > start;
    718                     } else {
    719                         shouldAddToPath = targetVersion >= end && targetVersion < start;
    720                     }
    721                     if (shouldAddToPath) {
    722                         result.add(targetNodes.valueAt(i));
    723                         start = targetVersion;
    724                         found = true;
    725                         break;
    726                     }
    727                 }
    728                 if (!found) {
    729                     return null;
    730                 }
    731             }
    732             return result;
    733         }
    734     }
    735 
    736     /**
    737      * Callback for {@link RoomDatabase}.
    738      */
    739     public abstract static class Callback {
    740 
    741         /**
    742          * Called when the database is created for the first time. This is called after all the
    743          * tables are created.
    744          *
    745          * @param db The database.
    746          */
    747         public void onCreate(@NonNull SupportSQLiteDatabase db) {
    748         }
    749 
    750         /**
    751          * Called when the database has been opened.
    752          *
    753          * @param db The database.
    754          */
    755         public void onOpen(@NonNull SupportSQLiteDatabase db) {
    756         }
    757     }
    758 }
    759