From 5a5d3ee1a03529bcdf3f37f4be1624d3ca291953 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 15 Dec 2021 21:33:14 +0100 Subject: [PATCH] Updated AndroidX --- CHANGELOG.md | 1 + app/build.gradle | 10 +- app/src/main/assets/CHANGELOG.md | 1 + .../room/AutoClosingRoomOpenHelper.java | 16 +- .../androidx/room/DatabaseConfiguration.java | 186 ++++++++++++++++-- .../androidx/room/ExperimentalRoomApi.java | 2 +- .../androidx/room/InvalidationTracker.java | 27 ++- .../room/MultiInstanceInvalidationClient.java | 7 +- .../MultiInstanceInvalidationService.java | 11 +- app/src/main/java/androidx/room/Room.java | 5 +- .../main/java/androidx/room/RoomDatabase.java | 171 +++++++++++++++- .../androidx/room/SQLiteCopyOpenHelper.java | 20 +- ...onCallback.java => AutoMigrationSpec.java} | 18 +- .../main/java/androidx/room/package-info.java | 2 +- .../main/java/androidx/room/util/DBUtil.java | 70 ++++++- .../java/androidx/room/util/TableInfo.java | 96 ++++++++- .../java/androidx/room/util/UUIDUtil.java | 64 ++++++ metadata/en-US/changelogs/1789.txt | 1 + 18 files changed, 613 insertions(+), 95 deletions(-) rename app/src/main/java/androidx/room/migration/{AutoMigrationCallback.java => AutoMigrationSpec.java} (63%) create mode 100644 app/src/main/java/androidx/room/util/UUIDUtil.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ccb19b94d..25a2b8d8e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Next version * Small improvements and minor bug fixes +* Updated AndroidX ### 1.1789 - 2021-12-14 diff --git a/app/build.gradle b/app/build.gradle index d6976c0a81..acb28ba11c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -297,16 +297,16 @@ dependencies { //implementation fileTree(dir: 'libs', include: ['*.jar']) def startup_version = "1.1.0" - def annotation_version_experimental = "1.1.0" // 1.2.0-rc01 + def annotation_version_experimental = "1.2.0" def core_version = "1.6.0" // 1.7.0 def shortcuts_version = "1.0.0" def appcompat_version = "1.3.1" - def emoji_version = "1.0.0" + def emoji_version = "1.0.1" def activity_version = "1.4.0" def fragment_version = "1.4.0" def webkit_version = "1.4.0" def recyclerview_version = "1.2.1" - def coordinatorlayout_version = "1.1.0" // 1.2.0-beta01 + def coordinatorlayout_version = "1.1.0" // 1.2.0-rc01 def constraintlayout_version = "2.1.2" def material_version = "1.4.0" def browser_version = "1.4.0" @@ -315,8 +315,8 @@ dependencies { def documentfile_version = "1.1.0-alpha01" def lifecycle_version = "2.4.0" def lifecycle_extensions_version = "2.2.0" - def room_version = "2.3.0" // 2.4.0-rc01 - def sqlite_version = "2.1.0" // 2.2.0-rc01 + def room_version = "2.4.0" + def sqlite_version = "2.2.0" def requery_version = "3.36.0" def paging_version = "2.1.2" // 3.1.0 def preference_version = "1.1.1" // 1.2.0-alpha02 diff --git a/app/src/main/assets/CHANGELOG.md b/app/src/main/assets/CHANGELOG.md index 7ccb19b94d..25a2b8d8e2 100644 --- a/app/src/main/assets/CHANGELOG.md +++ b/app/src/main/assets/CHANGELOG.md @@ -7,6 +7,7 @@ ### Next version * Small improvements and minor bug fixes +* Updated AndroidX ### 1.1789 - 2021-12-14 diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java index b3a3853f3a..03e68d4ec4 100644 --- a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java @@ -16,7 +16,6 @@ package androidx.room; -import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.ContentValues; import android.database.CharArrayBuffer; @@ -36,6 +35,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.arch.core.util.Function; import androidx.room.util.SneakyThrow; +import androidx.sqlite.db.SupportSQLiteCompat; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; import androidx.sqlite.db.SupportSQLiteQuery; @@ -440,7 +440,6 @@ final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, Delega }); } - @SuppressLint("UnsafeNewApiCall") @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) @Override public void setForeignKeyConstraintsEnabled(boolean enable) { @@ -464,7 +463,6 @@ final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, Delega + "OpenHelper instead of on the database directly."); } - @SuppressLint("UnsafeNewApiCall") @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) @Override public boolean isWriteAheadLoggingEnabled() { @@ -698,27 +696,24 @@ final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, Delega mDelegate.setNotificationUri(cr, uri); } - @SuppressLint("UnsafeNewApiCall") @RequiresApi(api = Build.VERSION_CODES.Q) @Override public void setNotificationUris(@NonNull ContentResolver cr, @NonNull List uris) { - mDelegate.setNotificationUris(cr, uris); + SupportSQLiteCompat.Api29Impl.setNotificationUris(mDelegate, cr, uris); } - @SuppressLint("UnsafeNewApiCall") @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Override public Uri getNotificationUri() { - return mDelegate.getNotificationUri(); + return SupportSQLiteCompat.Api19Impl.getNotificationUri(mDelegate); } - @SuppressLint("UnsafeNewApiCall") @RequiresApi(api = Build.VERSION_CODES.Q) @Nullable @Override public List getNotificationUris() { - return mDelegate.getNotificationUris(); + return SupportSQLiteCompat.Api29Impl.getNotificationUris(mDelegate); } @Override @@ -726,11 +721,10 @@ final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, Delega return mDelegate.getWantsAllOnMoveCalls(); } - @SuppressLint("UnsafeNewApiCall") @RequiresApi(api = Build.VERSION_CODES.M) @Override public void setExtras(Bundle extras) { - mDelegate.setExtras(extras); + SupportSQLiteCompat.Api23Impl.setExtras(mDelegate, extras); } @Override diff --git a/app/src/main/java/androidx/room/DatabaseConfiguration.java b/app/src/main/java/androidx/room/DatabaseConfiguration.java index d19c0cbeb2..01e96bc152 100644 --- a/app/src/main/java/androidx/room/DatabaseConfiguration.java +++ b/app/src/main/java/androidx/room/DatabaseConfiguration.java @@ -18,10 +18,12 @@ package androidx.room; import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import androidx.room.migration.AutoMigrationSpec; import androidx.sqlite.db.SupportSQLiteOpenHelper; import java.io.File; @@ -69,6 +71,9 @@ public class DatabaseConfiguration { @NonNull public final List typeConverters; + @NonNull + public final List autoMigrationSpecs; + /** * Whether Room should throw an exception for queries run on the main thread. */ @@ -98,6 +103,15 @@ public class DatabaseConfiguration { */ public final boolean multiInstanceInvalidation; + /** + * Intent that should be bound to acquire the invalidation service or {@code null} if not used. + * + * @see {@link #multiInstanceInvalidation} + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public final Intent multiInstanceInvalidationServiceIntent; + /** * If true, Room should crash if a migration is missing. */ @@ -133,14 +147,13 @@ public class DatabaseConfiguration { @Nullable public final Callable copyFromInputStream; - /** * Creates a database configuration with the given values. * * @deprecated Use {@link #DatabaseConfiguration(Context, String, * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, - * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, - * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -170,7 +183,8 @@ public class DatabaseConfiguration { @Nullable Set migrationNotRequiredFrom) { this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false, - requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null); + requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null, + null); } /** @@ -178,8 +192,8 @@ public class DatabaseConfiguration { * * @deprecated Use {@link #DatabaseConfiguration(Context, String, * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, - * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, - * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -216,7 +230,7 @@ public class DatabaseConfiguration { this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, - migrationNotRequiredFrom, null, null, null, null, null); + migrationNotRequiredFrom, null, null, null, null, null, null); } /** @@ -224,8 +238,8 @@ public class DatabaseConfiguration { * * @deprecated Use {@link #DatabaseConfiguration(Context, String, * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, - * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, - * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -266,7 +280,7 @@ public class DatabaseConfiguration { this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, - migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, null, null, null); + migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, null, null, null, null); } /** @@ -274,8 +288,8 @@ public class DatabaseConfiguration { * * @deprecated Use {@link #DatabaseConfiguration(Context, String, * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, - * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, - * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -320,16 +334,16 @@ public class DatabaseConfiguration { allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream, - null, null); + null, null, null); } - /** - * Creates a database configuration with the given values. - * - * @deprecated Use {@link #DatabaseConfiguration(Context, String, + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, - * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, - * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -377,12 +391,17 @@ public class DatabaseConfiguration { allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream, - prepackagedDatabaseCallback, null); + prepackagedDatabaseCallback, null, null); } /** * Creates a database configuration with the given values. * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * * @param context The application context. * @param name Name of the database, can be null if it is in memory. * @param sqliteOpenHelperFactory The open helper factory to use. @@ -407,6 +426,7 @@ public class DatabaseConfiguration { * * @hide */ + @Deprecated @SuppressLint("LambdaLast") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(@NonNull Context context, @Nullable String name, @@ -426,6 +446,126 @@ public class DatabaseConfiguration { @Nullable Callable copyFromInputStream, @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback, @Nullable List typeConverters) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, + migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream, + prepackagedDatabaseCallback, typeConverters, null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * @param prepackagedDatabaseCallback The pre-packaged callback. + * @param typeConverters The type converters. + * @param autoMigrationSpecs The auto migration specs. + * + * @hide + */ + @Deprecated + @SuppressLint("LambdaLast") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback, + @Nullable List typeConverters, + @Nullable List autoMigrationSpecs) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation ? new Intent(context, + MultiInstanceInvalidationService.class) : null, + requireMigration, allowDestructiveMigrationOnDowngrade, migrationNotRequiredFrom, + copyFromAssetPath, copyFromFile, copyFromInputStream, prepackagedDatabaseCallback, + typeConverters, autoMigrationSpecs); + } + + /** + * Creates a database configuration with the given values. + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidationServiceIntent The intent to use to bind to the + * invalidation service or {@code null} if not + * used. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * @param prepackagedDatabaseCallback The pre-packaged callback. + * @param typeConverters The type converters. + * @param autoMigrationSpecs The auto migration specs. + * + * @hide + */ + @SuppressLint("LambdaLast") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + @Nullable Intent multiInstanceInvalidationServiceIntent, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback, + @Nullable List typeConverters, + @Nullable List autoMigrationSpecs) { this.sqliteOpenHelperFactory = sqliteOpenHelperFactory; this.context = context; this.name = name; @@ -435,7 +575,9 @@ public class DatabaseConfiguration { this.journalMode = journalMode; this.queryExecutor = queryExecutor; this.transactionExecutor = transactionExecutor; - this.multiInstanceInvalidation = multiInstanceInvalidation; + this.multiInstanceInvalidationServiceIntent = + multiInstanceInvalidationServiceIntent; + this.multiInstanceInvalidation = multiInstanceInvalidationServiceIntent != null; this.requireMigration = requireMigration; this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade; this.mMigrationNotRequiredFrom = migrationNotRequiredFrom; @@ -444,6 +586,8 @@ public class DatabaseConfiguration { this.copyFromInputStream = copyFromInputStream; this.prepackagedDatabaseCallback = prepackagedDatabaseCallback; this.typeConverters = typeConverters == null ? Collections.emptyList() : typeConverters; + this.autoMigrationSpecs = autoMigrationSpecs == null + ? Collections.emptyList() : autoMigrationSpecs; } /** diff --git a/app/src/main/java/androidx/room/ExperimentalRoomApi.java b/app/src/main/java/androidx/room/ExperimentalRoomApi.java index 729a4a9bf6..14896bfadb 100644 --- a/app/src/main/java/androidx/room/ExperimentalRoomApi.java +++ b/app/src/main/java/androidx/room/ExperimentalRoomApi.java @@ -24,6 +24,6 @@ import java.lang.annotation.Target; /** * APIs marked with ExperimentalRoomApi are experimental and may change. */ -@Target({ElementType.METHOD}) +@Target({ElementType.TYPE, ElementType.METHOD}) @RequiresOptIn() public @interface ExperimentalRoomApi {} diff --git a/app/src/main/java/androidx/room/InvalidationTracker.java b/app/src/main/java/androidx/room/InvalidationTracker.java index 7dda1da913..fe0ae9a9ae 100644 --- a/app/src/main/java/androidx/room/InvalidationTracker.java +++ b/app/src/main/java/androidx/room/InvalidationTracker.java @@ -18,6 +18,7 @@ package androidx.room; import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.os.Build; @@ -210,9 +211,9 @@ public class InvalidationTracker { } } - void startMultiInstanceInvalidation(Context context, String name) { - mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this, - mDatabase.getQueryExecutor()); + void startMultiInstanceInvalidation(Context context, String name, Intent serviceIntent) { + mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, + serviceIntent, this, mDatabase.getQueryExecutor()); } void stopMultiInstanceInvalidation() { @@ -422,19 +423,15 @@ public class InvalidationTracker { return; } - if (mDatabase.mWriteAheadLoggingEnabled) { - // This transaction has to be on the underlying DB rather than the RoomDatabase - // in order to avoid a recursive loop after endTransaction. - SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase(); - db.beginTransactionNonExclusive(); - try { - invalidatedTableIds = checkUpdatedTable(); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } else { + // This transaction has to be on the underlying DB rather than the RoomDatabase + // in order to avoid a recursive loop after endTransaction. + SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { invalidatedTableIds = checkUpdatedTable(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); } } catch (IllegalStateException | SQLiteException exception) { // may happen if db is closed. just log. diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java index c8a7b5dc9d..cd847acc89 100644 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java @@ -142,10 +142,12 @@ class MultiInstanceInvalidationClient { * @param context The Context to be used for binding * {@link IMultiInstanceInvalidationService}. * @param name The name of the database file. + * @param serviceIntent The {@link Intent} used for binding + * {@link IMultiInstanceInvalidationService}. * @param invalidationTracker The {@link InvalidationTracker} * @param executor The background executor. */ - MultiInstanceInvalidationClient(Context context, String name, + MultiInstanceInvalidationClient(Context context, String name, Intent serviceIntent, InvalidationTracker invalidationTracker, Executor executor) { mAppContext = context.getApplicationContext(); mName = name; @@ -174,8 +176,7 @@ class MultiInstanceInvalidationClient { return true; } }; - Intent intent = new Intent(mAppContext, MultiInstanceInvalidationService.class); - mAppContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); + mAppContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); } void stop() { diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java index 2e98f120ff..0c82f93fa0 100644 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java @@ -23,8 +23,8 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; import java.util.HashMap; @@ -33,9 +33,12 @@ import java.util.HashMap; * This service runs in the main app process. All the instances of {@link InvalidationTracker} * (potentially in other processes) has to connect to this service. * - * @hide + *

The intent to launch it can be specified by + * {@link RoomDatabase.Builder#setMultiInstanceInvalidationServiceIntent}, although the service is + * defined in the manifest by default so there should be no need to override it in a normal + * situation. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +@ExperimentalRoomApi public class MultiInstanceInvalidationService extends Service { // synthetic access @@ -128,7 +131,7 @@ public class MultiInstanceInvalidationService extends Service { @Nullable @Override - public IBinder onBind(Intent intent) { + public IBinder onBind(@NonNull Intent intent) { return mBinder; } } diff --git a/app/src/main/java/androidx/room/Room.java b/app/src/main/java/androidx/room/Room.java index 6a570c4dc0..d2520998d1 100644 --- a/app/src/main/java/androidx/room/Room.java +++ b/app/src/main/java/androidx/room/Room.java @@ -19,6 +19,7 @@ package androidx.room; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; /** * Utility class for Room. @@ -75,7 +76,9 @@ public class Room { @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"}) @NonNull - static T getGeneratedImplementation(Class klass, String suffix) { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static T getGeneratedImplementation(@NonNull Class klass, + @NonNull String suffix) { final String fullPackage = klass.getPackage().getName(); String name = klass.getCanonicalName(); final String postPackageName = fullPackage.isEmpty() diff --git a/app/src/main/java/androidx/room/RoomDatabase.java b/app/src/main/java/androidx/room/RoomDatabase.java index 9b652bf528..54427d6eea 100644 --- a/app/src/main/java/androidx/room/RoomDatabase.java +++ b/app/src/main/java/androidx/room/RoomDatabase.java @@ -19,6 +19,7 @@ package androidx.room; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.os.Build; import android.os.CancellationSignal; @@ -33,9 +34,11 @@ import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.WorkerThread; import androidx.arch.core.executor.ArchTaskExecutor; +import androidx.room.migration.AutoMigrationSpec; import androidx.room.migration.Migration; import androidx.room.util.SneakyThrow; import androidx.sqlite.db.SimpleSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteCompat; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; import androidx.sqlite.db.SupportSQLiteQuery; @@ -99,6 +102,15 @@ public abstract class RoomDatabase { @Deprecated protected List mCallbacks; + /** + * A map of auto migration spec classes to their provided instance. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @NonNull + protected Map, AutoMigrationSpec> mAutoMigrationSpecs; + private final ReentrantReadWriteLock mCloseLock = new ReentrantReadWriteLock(); @Nullable @@ -150,7 +162,6 @@ public abstract class RoomDatabase { // Updated later to an unmodifiable map when init is called. private final Map, Object> mTypeConverters; - /** * Gets the instance of the given Type Converter. * @@ -174,6 +185,7 @@ public abstract class RoomDatabase { public RoomDatabase() { mInvalidationTracker = createInvalidationTracker(); mTypeConverters = new HashMap<>(); + mAutoMigrationSpecs = new HashMap<>(); } /** @@ -184,6 +196,47 @@ public abstract class RoomDatabase { @CallSuper public void init(@NonNull DatabaseConfiguration configuration) { mOpenHelper = createOpenHelper(configuration); + Set> requiredAutoMigrationSpecs = + getRequiredAutoMigrationSpecs(); + BitSet usedSpecs = new BitSet(); + for (Class spec : requiredAutoMigrationSpecs) { + int foundIndex = -1; + for (int providedIndex = configuration.autoMigrationSpecs.size() - 1; + providedIndex >= 0; providedIndex-- + ) { + Object provided = configuration.autoMigrationSpecs.get(providedIndex); + if (spec.isAssignableFrom(provided.getClass())) { + foundIndex = providedIndex; + usedSpecs.set(foundIndex); + break; + } + } + if (foundIndex < 0) { + throw new IllegalArgumentException( + "A required auto migration spec (" + spec.getCanonicalName() + + ") is missing in the database configuration."); + } + mAutoMigrationSpecs.put(spec, configuration.autoMigrationSpecs.get(foundIndex)); + } + + for (int providedIndex = configuration.autoMigrationSpecs.size() - 1; + providedIndex >= 0; providedIndex--) { + if (!usedSpecs.get(providedIndex)) { + throw new IllegalArgumentException("Unexpected auto migration specs found. " + + "Annotate AutoMigrationSpec implementation with " + + "@ProvidedAutoMigrationSpec annotation or remove this spec from the " + + "builder."); + } + } + + List autoMigrations = getAutoMigrations(mAutoMigrationSpecs); + for (Migration autoMigration : autoMigrations) { + boolean migrationExists = configuration.migrationContainer.getMigrations() + .containsKey(autoMigration.startVersion); + if (!migrationExists) { + configuration.migrationContainer.addMigrations(autoMigration); + } + } // Configure SqliteCopyOpenHelper if it is available: SQLiteCopyOpenHelper copyOpenHelper = unwrapOpenHelper(SQLiteCopyOpenHelper.class, @@ -211,9 +264,9 @@ public abstract class RoomDatabase { mTransactionExecutor = new TransactionExecutor(configuration.transactionExecutor); mAllowMainThreadQueries = configuration.allowMainThreadQueries; mWriteAheadLoggingEnabled = wal; - if (configuration.multiInstanceInvalidation) { + if (configuration.multiInstanceInvalidationServiceIntent != null) { mInvalidationTracker.startMultiInstanceInvalidation(configuration.context, - configuration.name); + configuration.name, configuration.multiInstanceInvalidationServiceIntent); } Map, List>> requiredFactories = getRequiredTypeConverters(); @@ -256,6 +309,22 @@ public abstract class RoomDatabase { } } + /** + * Returns a list of {@link Migration} of a database that have been automatically generated. + * + * @return A list of migration instances each of which is a generated autoMigration + * @param autoMigrationSpecs + * + * @hide + */ + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public List getAutoMigrations( + @NonNull Map, AutoMigrationSpec> autoMigrationSpecs + ) { + return Collections.emptyList(); + } + /** * Unwraps (delegating) open helpers until it finds clazz, otherwise returns null. * @@ -322,6 +391,21 @@ public abstract class RoomDatabase { return Collections.emptyMap(); } + /** + * Returns a Set of required AutoMigrationSpec classes. + *

+ * This is implemented by the generated code. + * + * @return Creates a set that will include all required auto migration specs for this database. + * + * @hide + */ + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public Set> getRequiredAutoMigrationSpecs() { + return Collections.emptySet(); + } + /** * Deletes all rows from all the tables that are registered to this database as * {@link Database#entities()}. @@ -625,7 +709,7 @@ public abstract class RoomDatabase { /** * Journal modes for SQLite database. * - * @see RoomDatabase.Builder#setJournalMode(JournalMode) + * @see Builder#setJournalMode(JournalMode) */ public enum JournalMode { @@ -653,7 +737,6 @@ public abstract class RoomDatabase { * Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or * {@link #WRITE_AHEAD_LOGGING}. */ - @SuppressLint("NewApi") JournalMode resolve(Context context) { if (this != AUTOMATIC) { return this; @@ -670,7 +753,7 @@ public abstract class RoomDatabase { private static boolean isLowRamDevice(@NonNull ActivityManager activityManager) { if (Build.VERSION.SDK_INT >= 19) { - return activityManager.isLowRamDevice(); + return SupportSQLiteCompat.Api19Impl.isLowRamDevice(activityManager); } return false; } @@ -690,6 +773,7 @@ public abstract class RoomDatabase { private QueryCallback mQueryCallback; private Executor mQueryCallbackExecutor; private List mTypeConverters; + private List mAutoMigrationSpecs; /** The Executor used to run database queries. This should be background-threaded. */ private Executor mQueryExecutor; @@ -698,7 +782,7 @@ public abstract class RoomDatabase { private SupportSQLiteOpenHelper.Factory mFactory; private boolean mAllowMainThreadQueries; private JournalMode mJournalMode; - private boolean mMultiInstanceInvalidation; + private Intent mMultiInstanceInvalidationIntent; private boolean mRequireMigration; private boolean mAllowDestructiveMigrationOnDowngrade; @@ -961,6 +1045,23 @@ public abstract class RoomDatabase { return this; } + /** + * Adds an auto migration spec to the builder. + * + * @param autoMigrationSpec The auto migration object that is annotated with + * {@link AutoMigrationSpec} and is declared in an {@link AutoMigration} annotation. + * @return This {@link Builder} instance. + */ + @NonNull + @SuppressWarnings("MissingGetterMatchingBuilder") + public Builder addAutoMigrationSpec(@NonNull AutoMigrationSpec autoMigrationSpec) { + if (mAutoMigrationSpecs == null) { + mAutoMigrationSpecs = new ArrayList<>(); + } + mAutoMigrationSpecs.add(autoMigrationSpec); + return this; + } + /** * Disables the main thread query check for Room. *

@@ -1067,7 +1168,33 @@ public abstract class RoomDatabase { */ @NonNull public Builder enableMultiInstanceInvalidation() { - mMultiInstanceInvalidation = mName != null; + mMultiInstanceInvalidationIntent = mName != null ? new Intent(mContext, + MultiInstanceInvalidationService.class) : null; + return this; + } + + /** + * Sets whether table invalidation in this instance of {@link RoomDatabase} should be + * broadcast and synchronized with other instances of the same {@link RoomDatabase}, + * including those in a separate process. In order to enable multi-instance invalidation, + * this has to be turned on both ends and need to point to the same + * {@link MultiInstanceInvalidationService}. + *

+ * This is not enabled by default. + *

+ * This does not work for in-memory databases. This does not work between database instances + * targeting different database files. + * + * @return This {@link Builder} instance. + * @param invalidationServiceIntent Intent to bind to the + * {@link MultiInstanceInvalidationService}. + */ + @SuppressWarnings("MissingGetterMatchingBuilder") + @NonNull + @ExperimentalRoomApi + public Builder setMultiInstanceInvalidationServiceIntent( + @NonNull Intent invalidationServiceIntent) { + mMultiInstanceInvalidationIntent = mName != null ? invalidationServiceIntent : null; return this; } @@ -1346,7 +1473,7 @@ public abstract class RoomDatabase { mJournalMode.resolve(mContext), mQueryExecutor, mTransactionExecutor, - mMultiInstanceInvalidation, + mMultiInstanceInvalidationIntent, mRequireMigration, mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom, @@ -1354,7 +1481,8 @@ public abstract class RoomDatabase { mCopyFromFile, mCopyFromInputStream, mPrepackagedDatabaseCallback, - mTypeConverters); + mTypeConverters, + mAutoMigrationSpecs); T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); db.init(configuration); return db; @@ -1380,6 +1508,18 @@ public abstract class RoomDatabase { } } + /** + * Adds the given migrations to the list of available migrations. If 2 migrations have the + * same start-end versions, the latter migration overrides the previous one. + * + * @param migrations List of available migrations. + */ + public void addMigrations(@NonNull List migrations) { + for (Migration migration : migrations) { + addMigration(migration); + } + } + private void addMigration(Migration migration) { final int start = migration.startVersion; final int end = migration.endVersion; @@ -1395,6 +1535,17 @@ public abstract class RoomDatabase { targetMap.put(end, migration); } + /** + * Returns the map of available migrations where the key is the start version of the + * migration, and the value is a map of (end version -> Migration). + * + * @return Map of migrations keyed by the start version + */ + @NonNull + public Map> getMigrations() { + return Collections.unmodifiableMap(mMigrations); + } + /** * Finds the list of migrations that should be run to move from {@code start} version to * {@code end} version. diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java index b7d244ebb0..f7b856873d 100644 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java @@ -248,18 +248,16 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHel } private SupportSQLiteOpenHelper createFrameworkOpenHelper(File databaseFile) { - String databaseName = databaseFile.getName(); - int version; + final int version; try { version = DBUtil.readVersion(databaseFile); } catch (IOException e) { throw new RuntimeException("Malformed database file, unable to read version.", e); } - FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory(); Configuration configuration = Configuration.builder(mContext) - .name(databaseName) - .callback(new Callback(version) { + .name(databaseFile.getAbsolutePath()) + .callback(new Callback(Math.max(version, 1)) { @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { } @@ -268,6 +266,18 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHel public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion, int newVersion) { } + + @Override + public void onOpen(@NonNull SupportSQLiteDatabase db) { + // If pre-packaged database has a version < 1 we will open it as if it was + // version 1 because the framework open helper does not allow version < 1. + // The database will be considered as newly created and onCreate() will be + // invoked, but we do nothing and reset the version back so Room later runs + // migrations as usual. + if (version < 1) { + db.setVersion(version); + } + } }) .build(); return factory.create(configuration); diff --git a/app/src/main/java/androidx/room/migration/AutoMigrationCallback.java b/app/src/main/java/androidx/room/migration/AutoMigrationSpec.java similarity index 63% rename from app/src/main/java/androidx/room/migration/AutoMigrationCallback.java rename to app/src/main/java/androidx/room/migration/AutoMigrationSpec.java index f1a04f921f..1eea1897a8 100644 --- a/app/src/main/java/androidx/room/migration/AutoMigrationCallback.java +++ b/app/src/main/java/androidx/room/migration/AutoMigrationSpec.java @@ -16,21 +16,23 @@ package androidx.room.migration; import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; +import androidx.room.AutoMigration; import androidx.sqlite.db.SupportSQLiteDatabase; /** - * Interface for defining automatic migration strategy for Room databases. + * Interface for defining an automatic migration specification for Room databases. + *

+ * The methods defined in this interface will be called on a background thread from the executor + * set in Room's builder. It is important to note that the methods are all in a transaction when + * it is called. * - * @hide + * @see AutoMigration */ -// TODO: (b/181655460) Complete code usage documentation for this class and the AutoMigration -// annotation. -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public interface AutoMigrationCallback { +public interface AutoMigrationSpec { /** - * Handles any changes the user may want to implement after migration is completed. + * Invoked after the migration is completed. + * @param db The SQLite database. */ default void onPostMigrate(@NonNull SupportSQLiteDatabase db) {} } diff --git a/app/src/main/java/androidx/room/package-info.java b/app/src/main/java/androidx/room/package-info.java index 4507a93e2d..b67bdcc0aa 100644 --- a/app/src/main/java/androidx/room/package-info.java +++ b/app/src/main/java/androidx/room/package-info.java @@ -80,7 +80,7 @@ * void delete(Song song); * } * // File: MusicDatabase.java - * {@literal @}Database(entities = {Song.java}) + * {@literal @}Database(entities = {Song.class}) * public abstract class MusicDatabase extends RoomDatabase { * public abstract SongDao songDao(); * } diff --git a/app/src/main/java/androidx/room/util/DBUtil.java b/app/src/main/java/androidx/room/util/DBUtil.java index 1b9b8660b2..8f876ae8ee 100644 --- a/app/src/main/java/androidx/room/util/DBUtil.java +++ b/app/src/main/java/androidx/room/util/DBUtil.java @@ -25,6 +25,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteCompat; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteQuery; @@ -34,7 +35,9 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Database utilities for Room @@ -125,6 +128,22 @@ public class DBUtil { } } + /** + * Checks for foreign key violations by executing a PRAGMA foreign_key_check. + */ + public static void foreignKeyCheck(@NonNull SupportSQLiteDatabase db, + @NonNull String tableName) { + Cursor cursor = db.query("PRAGMA foreign_key_check(`" + tableName + "`)"); + try { + if (cursor.getCount() > 0) { + String errorMsg = processForeignKeyCheckFailure(cursor); + throw new IllegalStateException(errorMsg); + } + } finally { + cursor.close(); + } + } + /** * Reads the user version number out of the database header from the given file. * @@ -165,11 +184,60 @@ public class DBUtil { @Nullable public static CancellationSignal createCancellationSignal() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return new CancellationSignal(); + return SupportSQLiteCompat.Api16Impl.createCancellationSignal(); } return null; } + + /** + * Converts the {@link Cursor} returned in case of a foreign key violation into a detailed + * error message for debugging. + *

+ * The foreign_key_check pragma returns one row output for each foreign key violation. + *

+ * The cursor received has four columns for each row output. The first column is the name of + * the child table. The second column is the rowId of the row that contains the foreign key + * violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the + * name of the parent table. The fourth column is the index of the specific foreign key + * constraint that failed. + * + * @param cursor Cursor containing information regarding the FK violation + * @return Error message generated containing debugging information + */ + private static String processForeignKeyCheckFailure(Cursor cursor) { + int rowCount = cursor.getCount(); + String childTableName = null; + Map fkParentTables = new HashMap<>(); + + while (cursor.moveToNext()) { + if (childTableName == null) { + childTableName = cursor.getString(0); + } + String constraintIndex = cursor.getString(3); + if (!fkParentTables.containsKey(constraintIndex)) { + fkParentTables.put(constraintIndex, cursor.getString(2)); + } + } + + StringBuilder sb = new StringBuilder(); + sb.append("Foreign key violation(s) detected in '") + .append(childTableName).append("'.\n"); + sb.append("Number of different violations discovered: ") + .append(fkParentTables.keySet().size()).append("\n"); + sb.append("Number of rows in violation: ") + .append(rowCount).append("\n"); + sb.append("Violation(s) detected in the following constraint(s):\n"); + + for (Map.Entry entry : fkParentTables.entrySet()) { + sb.append("\tParent Table = ") + .append(entry.getValue()); + sb.append(", Foreign Key Constraint Index = ") + .append(entry.getKey()).append("\n"); + } + return sb.toString(); + } + private DBUtil() { } } diff --git a/app/src/main/java/androidx/room/util/TableInfo.java b/app/src/main/java/androidx/room/util/TableInfo.java index f884f3f1b9..2d3c8be329 100644 --- a/app/src/main/java/androidx/room/util/TableInfo.java +++ b/app/src/main/java/androidx/room/util/TableInfo.java @@ -237,6 +237,7 @@ public final class TableInfo { private static Map readColumns(SupportSQLiteDatabase database, String tableName) { + Cursor cursor = database .query("PRAGMA table_info(`" + tableName + "`)"); //noinspection TryFinallyCanBeTryWithResources @@ -312,11 +313,14 @@ public final class TableInfo { final int seqnoColumnIndex = cursor.getColumnIndex("seqno"); final int cidColumnIndex = cursor.getColumnIndex("cid"); final int nameColumnIndex = cursor.getColumnIndex("name"); - if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) { + final int descColumnIndex = cursor.getColumnIndex("desc"); + if (seqnoColumnIndex == -1 || cidColumnIndex == -1 + || nameColumnIndex == -1 || descColumnIndex == -1) { // we cannot read them so better not validate any index. return null; } - final TreeMap results = new TreeMap<>(); + final TreeMap columnsMap = new TreeMap<>(); + final TreeMap ordersMap = new TreeMap<>(); while (cursor.moveToNext()) { int cid = cursor.getInt(cidColumnIndex); @@ -326,11 +330,16 @@ public final class TableInfo { } int seq = cursor.getInt(seqnoColumnIndex); String columnName = cursor.getString(nameColumnIndex); - results.put(seq, columnName); + String order = cursor.getInt(descColumnIndex) > 0 ? "DESC" : "ASC"; + + columnsMap.put(seq, columnName); + ordersMap.put(seq, order); } - final List columns = new ArrayList<>(results.size()); - columns.addAll(results.values()); - return new Index(name, unique, columns); + final List columns = new ArrayList<>(columnsMap.size()); + columns.addAll(columnsMap.values()); + final List orders = new ArrayList<>(ordersMap.size()); + orders.addAll(ordersMap.values()); + return new Index(name, unique, columns, orders); } finally { cursor.close(); } @@ -456,15 +465,18 @@ public final class TableInfo { // from the compiler itself has it. b/136019383 if (mCreatedFrom == CREATED_FROM_ENTITY && column.mCreatedFrom == CREATED_FROM_DATABASE - && (defaultValue != null && !defaultValue.equals(column.defaultValue))) { + && (defaultValue != null && !defaultValueEquals(defaultValue, + column.defaultValue))) { return false; } else if (mCreatedFrom == CREATED_FROM_DATABASE && column.mCreatedFrom == CREATED_FROM_ENTITY - && (column.defaultValue != null && !column.defaultValue.equals(defaultValue))) { + && (column.defaultValue != null && !defaultValueEquals( + column.defaultValue, defaultValue))) { return false; } else if (mCreatedFrom != CREATED_FROM_UNKNOWN && mCreatedFrom == column.mCreatedFrom - && (defaultValue != null ? !defaultValue.equals(column.defaultValue) + && (defaultValue != null ? !defaultValueEquals(defaultValue, + column.defaultValue) : column.defaultValue != null)) { return false; } @@ -472,6 +484,56 @@ public final class TableInfo { return affinity == column.affinity; } + /** + * Checks if the default values provided match. Handles the special case in which the + * default value is surrounded by parenthesis (e.g. encountered in b/182284899). + * + * Surrounding parenthesis are removed by SQLite when reading from the database, hence + * this function will check if they are present in the actual value, if so, it will + * compare the two values by ignoring the surrounding parenthesis. + * + */ + public static boolean defaultValueEquals(@NonNull String actual, @Nullable String other) { + if (other == null) { + return false; + } + + if (actual.equals(other)) { + return true; + } else if (containsSurroundingParenthesis(actual)) { + return actual.substring(1, actual.length() - 1).trim().equals(other); + } + return false; + } + + /** + * Checks for potential surrounding parenthesis, if found, removes them and checks if + * remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant, + * and returns true. + */ + private static boolean containsSurroundingParenthesis(@NonNull String actual) { + if (actual.length() == 0) { + return false; + } + int surroundingParenthesis = 0; + for (int i = 0; i < actual.length(); i++) { + char c = actual.charAt(i); + if (i == 0 && c != '(') { + return false; + } + + if (c == '(') { + surroundingParenthesis++; + } else if (c == ')') { + surroundingParenthesis--; + if (surroundingParenthesis == 0 && i != actual.length() - 1) { + return false; + } + } + } + return surroundingParenthesis == 0; + } + /** * Returns whether this column is part of the primary key or not. * @@ -614,11 +676,22 @@ public final class TableInfo { public final String name; public final boolean unique; public final List columns; + public final List orders; + /** + * @deprecated Use {@link #Index(String, boolean, List, List)} + */ public Index(String name, boolean unique, List columns) { + this(name, unique, columns, null); + } + + public Index(String name, boolean unique, List columns, List orders) { this.name = name; this.unique = unique; this.columns = columns; + this.orders = orders == null || orders.size() == 0 + ? Collections.nCopies(columns.size(), androidx.room.Index.Order.ASC.name()) + : orders; } @Override @@ -633,6 +706,9 @@ public final class TableInfo { if (!columns.equals(index.columns)) { return false; } + if (!orders.equals(index.orders)) { + return false; + } if (name.startsWith(Index.DEFAULT_PREFIX)) { return index.name.startsWith(Index.DEFAULT_PREFIX); } else { @@ -650,6 +726,7 @@ public final class TableInfo { } result = 31 * result + (unique ? 1 : 0); result = 31 * result + columns.hashCode(); + result = 31 * result + orders.hashCode(); return result; } @@ -659,6 +736,7 @@ public final class TableInfo { + "name='" + name + '\'' + ", unique=" + unique + ", columns=" + columns + + ", orders=" + orders + '}'; } } diff --git a/app/src/main/java/androidx/room/util/UUIDUtil.java b/app/src/main/java/androidx/room/util/UUIDUtil.java new file mode 100644 index 0000000000..275a95ca76 --- /dev/null +++ b/app/src/main/java/androidx/room/util/UUIDUtil.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 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 androidx.room.util; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * UUID / byte[] two-way conversion utility for Room + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public final class UUIDUtil { + + // private constructor to prevent instantiation + private UUIDUtil() {} + + /** + * Converts a 16-bytes array BLOB into a UUID pojo + * + * @param bytes byte array stored in database as BLOB + * @return a UUID object created based on the provided byte array + */ + @NonNull + public static UUID convertByteToUUID(@NonNull byte[] bytes) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + long firstLong = buffer.getLong(); + long secondLong = buffer.getLong(); + return new UUID(firstLong, secondLong); + } + + /** + * Converts a UUID pojo into a 16-bytes array to store into database as BLOB + * + * @param uuid the UUID pojo + * @return a byte array to store into database + */ + @NonNull + public static byte[] convertUUIDToByte(@NonNull UUID uuid) { + byte[] bytes = new byte[16]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + return buffer.array(); + } +} diff --git a/metadata/en-US/changelogs/1789.txt b/metadata/en-US/changelogs/1789.txt index 7ccb19b94d..25a2b8d8e2 100644 --- a/metadata/en-US/changelogs/1789.txt +++ b/metadata/en-US/changelogs/1789.txt @@ -7,6 +7,7 @@ ### Next version * Small improvements and minor bug fixes +* Updated AndroidX ### 1.1789 - 2021-12-14