mirror of https://github.com/M66B/FairEmail.git
parent
86c0638fa0
commit
519e617dc7
@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RPC Callbacks for {@link IMultiInstanceInvalidationService}.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
interface IMultiInstanceInvalidationCallback {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when invalidation is detected in another instance of the same database.
|
|
||||||
*
|
|
||||||
* @param tables List of invalidated table names
|
|
||||||
*/
|
|
||||||
oneway void onInvalidation(in String[] tables);
|
|
||||||
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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;
|
|
||||||
|
|
||||||
import androidx.room.IMultiInstanceInvalidationCallback;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RPC Service that controls interaction about multi-instance invalidation.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
interface IMultiInstanceInvalidationService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a new {@link IMultiInstanceInvalidationCallback} as a client of this service.
|
|
||||||
*
|
|
||||||
* @param callback The RPC callback.
|
|
||||||
* @param name The name of the database file as it is passed to {@link RoomDatabase.Builder}.
|
|
||||||
* @return A new client ID. The client needs to hold on to this ID and pass it to the service
|
|
||||||
* for subsequent calls.
|
|
||||||
*/
|
|
||||||
int registerCallback(IMultiInstanceInvalidationCallback callback, String name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters the specified {@link IMultiInstanceInvalidationCallback} from this service.
|
|
||||||
* <p>
|
|
||||||
* Clients might die without explicitly calling this method. In that case, the service should
|
|
||||||
* handle the clean up.
|
|
||||||
*
|
|
||||||
* @param callback The RPC callback.
|
|
||||||
* @param clientId The client ID returned from {@link #registerCallback}.
|
|
||||||
*/
|
|
||||||
void unregisterCallback(IMultiInstanceInvalidationCallback callback, int clientId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts invalidation of database tables to other clients registered to this service.
|
|
||||||
* <p>
|
|
||||||
* The broadcast is delivered to {@link IMultiInstanceInvalidationCallback#onInvalidation} of
|
|
||||||
* the registered clients. The client calling this method will not receive its own broadcast.
|
|
||||||
* Clients that are associated with a different database file will not be notified.
|
|
||||||
*
|
|
||||||
* @param clientId The client ID returned from {@link #registerCallback}.
|
|
||||||
* @param tables The names of invalidated tables.
|
|
||||||
*/
|
|
||||||
oneway void broadcastInvalidation(int clientId, in String[] tables);
|
|
||||||
|
|
||||||
}
|
|
@ -1,294 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 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;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration class for a {@link RoomDatabase}.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public class DatabaseConfiguration {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The factory to use to access the database.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
|
|
||||||
/**
|
|
||||||
* The context to use while connecting to the database.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final Context context;
|
|
||||||
/**
|
|
||||||
* The name of the database file or null if it is an in-memory database.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collection of available migrations.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final RoomDatabase.MigrationContainer migrationContainer;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public final List<RoomDatabase.Callback> callbacks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether Room should throw an exception for queries run on the main thread.
|
|
||||||
*/
|
|
||||||
public final boolean allowMainThreadQueries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The journal mode for this database.
|
|
||||||
*/
|
|
||||||
public final RoomDatabase.JournalMode journalMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Executor used to execute asynchronous queries.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final Executor queryExecutor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Executor used to execute asynchronous transactions.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public final Executor transactionExecutor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
|
|
||||||
* synchronized with other instances of the same {@link RoomDatabase} file, including those
|
|
||||||
* in a separate process.
|
|
||||||
*/
|
|
||||||
public final boolean multiInstanceInvalidation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, Room should crash if a migration is missing.
|
|
||||||
*/
|
|
||||||
public final boolean requireMigration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, Room should perform a destructive migration when downgrading without an available
|
|
||||||
* migration.
|
|
||||||
*/
|
|
||||||
public final boolean allowDestructiveMigrationOnDowngrade;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The collection of schema versions from which migrations aren't required.
|
|
||||||
*/
|
|
||||||
private final Set<Integer> mMigrationNotRequiredFrom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The assets path to a pre-packaged database to copy from.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String copyFromAssetPath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pre-packaged database file to copy from.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final File copyFromFile;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)}
|
|
||||||
*
|
|
||||||
* @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 requireMigration True if Room should require a valid migration if version changes,
|
|
||||||
* instead of recreating the tables.
|
|
||||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
|
||||||
* aren't required.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
|
||||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
|
||||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
|
||||||
@Nullable List<androidx.room.RoomDatabase.Callback> callbacks,
|
|
||||||
boolean allowMainThreadQueries,
|
|
||||||
RoomDatabase.JournalMode journalMode,
|
|
||||||
@NonNull Executor queryExecutor,
|
|
||||||
boolean requireMigration,
|
|
||||||
@Nullable Set<Integer> migrationNotRequiredFrom) {
|
|
||||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
|
||||||
allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
|
|
||||||
requireMigration, false, migrationNotRequiredFrom, 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, boolean, boolean, boolean, Set, String, File)}
|
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
|
||||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
|
||||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
|
||||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
|
||||||
boolean allowMainThreadQueries,
|
|
||||||
RoomDatabase.JournalMode journalMode,
|
|
||||||
@NonNull Executor queryExecutor,
|
|
||||||
@NonNull Executor transactionExecutor,
|
|
||||||
boolean multiInstanceInvalidation,
|
|
||||||
boolean requireMigration,
|
|
||||||
boolean allowDestructiveMigrationOnDowngrade,
|
|
||||||
@Nullable Set<Integer> migrationNotRequiredFrom) {
|
|
||||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
|
||||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
|
||||||
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
|
|
||||||
migrationNotRequiredFrom, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
|
||||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
|
||||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
|
||||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
|
||||||
boolean allowMainThreadQueries,
|
|
||||||
RoomDatabase.JournalMode journalMode,
|
|
||||||
@NonNull Executor queryExecutor,
|
|
||||||
@NonNull Executor transactionExecutor,
|
|
||||||
boolean multiInstanceInvalidation,
|
|
||||||
boolean requireMigration,
|
|
||||||
boolean allowDestructiveMigrationOnDowngrade,
|
|
||||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
|
||||||
@Nullable String copyFromAssetPath,
|
|
||||||
@Nullable File copyFromFile) {
|
|
||||||
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
|
|
||||||
this.context = context;
|
|
||||||
this.name = name;
|
|
||||||
this.migrationContainer = migrationContainer;
|
|
||||||
this.callbacks = callbacks;
|
|
||||||
this.allowMainThreadQueries = allowMainThreadQueries;
|
|
||||||
this.journalMode = journalMode;
|
|
||||||
this.queryExecutor = queryExecutor;
|
|
||||||
this.transactionExecutor = transactionExecutor;
|
|
||||||
this.multiInstanceInvalidation = multiInstanceInvalidation;
|
|
||||||
this.requireMigration = requireMigration;
|
|
||||||
this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
|
|
||||||
this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
|
|
||||||
this.copyFromAssetPath = copyFromAssetPath;
|
|
||||||
this.copyFromFile = copyFromFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a migration is required from the specified version.
|
|
||||||
*
|
|
||||||
* @param version The schema version.
|
|
||||||
* @return True if a valid migration is required, false otherwise.
|
|
||||||
*
|
|
||||||
* @deprecated Use {@link #isMigrationRequired(int, int)} which takes
|
|
||||||
* {@link #allowDestructiveMigrationOnDowngrade} into account.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean isMigrationRequiredFrom(int version) {
|
|
||||||
return isMigrationRequired(version, version + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a migration is required between two versions.
|
|
||||||
*
|
|
||||||
* @param fromVersion The old schema version.
|
|
||||||
* @param toVersion The new schema version.
|
|
||||||
* @return True if a valid migration is required, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isMigrationRequired(int fromVersion, int toVersion) {
|
|
||||||
// Migrations are not required if its a downgrade AND destructive migration during downgrade
|
|
||||||
// has been allowed.
|
|
||||||
final boolean isDowngrade = fromVersion > toVersion;
|
|
||||||
if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrations are required between the two versions if we generally require migrations
|
|
||||||
// AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
|
|
||||||
// exceptions.
|
|
||||||
return requireMigration
|
|
||||||
&& (mMigrationNotRequiredFrom == null
|
|
||||||
|| !mMigrationNotRequiredFrom.contains(fromVersion));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 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;
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementations of this class knows how to delete or update a particular entity.
|
|
||||||
* <p>
|
|
||||||
* This is an internal library class and all of its implementations are auto-generated.
|
|
||||||
*
|
|
||||||
* @param <T> The type parameter of the entity to be deleted
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
|
||||||
public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
|
|
||||||
/**
|
|
||||||
* Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
|
|
||||||
* database.
|
|
||||||
*
|
|
||||||
* @param database The database to delete / update the item in.
|
|
||||||
*/
|
|
||||||
public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
|
|
||||||
super(database);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the deletion or update query
|
|
||||||
*
|
|
||||||
* @return An SQL query that can delete or update instances of T.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected abstract String createQuery();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the entity into the given statement.
|
|
||||||
*
|
|
||||||
* @param statement The SQLite statement that prepared for the query returned from
|
|
||||||
* createQuery.
|
|
||||||
* @param entity The entity of type T.
|
|
||||||
*/
|
|
||||||
protected abstract void bind(SupportSQLiteStatement statement, T entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes or updates the given entities in the database and returns the affected row count.
|
|
||||||
*
|
|
||||||
* @param entity The entity to delete or update
|
|
||||||
* @return The number of affected rows
|
|
||||||
*/
|
|
||||||
public final int handle(T entity) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
bind(stmt, entity);
|
|
||||||
return stmt.executeUpdateDelete();
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes or updates the given entities in the database and returns the affected row count.
|
|
||||||
*
|
|
||||||
* @param entities Entities to delete or update
|
|
||||||
* @return The number of affected rows
|
|
||||||
*/
|
|
||||||
public final int handleMultiple(Iterable<? extends T> entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
int total = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
total += stmt.executeUpdateDelete();
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes or updates the given entities in the database and returns the affected row count.
|
|
||||||
*
|
|
||||||
* @param entities Entities to delete or update
|
|
||||||
* @return The number of affected rows
|
|
||||||
*/
|
|
||||||
public final int handleMultiple(T[] entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
int total = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
total += stmt.executeUpdateDelete();
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,251 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 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;
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementations of this class knows how to insert a particular entity.
|
|
||||||
* <p>
|
|
||||||
* This is an internal library class and all of its implementations are auto-generated.
|
|
||||||
*
|
|
||||||
* @param <T> The type parameter of the entity to be inserted
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
|
|
||||||
/**
|
|
||||||
* Creates an InsertionAdapter that can insert the entity type T into the given database.
|
|
||||||
*
|
|
||||||
* @param database The database to insert into.
|
|
||||||
*/
|
|
||||||
public EntityInsertionAdapter(RoomDatabase database) {
|
|
||||||
super(database);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the entity into the given statement.
|
|
||||||
*
|
|
||||||
* @param statement The SQLite statement that prepared for the query returned from
|
|
||||||
* createInsertQuery.
|
|
||||||
* @param entity The entity of type T.
|
|
||||||
*/
|
|
||||||
protected abstract void bind(SupportSQLiteStatement statement, T entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the entity into the database.
|
|
||||||
*
|
|
||||||
* @param entity The entity to insert
|
|
||||||
*/
|
|
||||||
public final void insert(T entity) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
bind(stmt, entity);
|
|
||||||
stmt.executeInsert();
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
*/
|
|
||||||
public final void insert(T[] entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
stmt.executeInsert();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
*/
|
|
||||||
public final void insert(Iterable<? extends T> entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
stmt.executeInsert();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entity into the database and returns the row id.
|
|
||||||
*
|
|
||||||
* @param entity The entity to insert
|
|
||||||
* @return The SQLite row id or -1 if no row is inserted
|
|
||||||
*/
|
|
||||||
public final long insertAndReturnId(T entity) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
bind(stmt, entity);
|
|
||||||
return stmt.executeInsert();
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database and returns the row ids.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
|
||||||
*/
|
|
||||||
public final long[] insertAndReturnIdsArray(Collection<? extends T> entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
final long[] result = new long[entities.size()];
|
|
||||||
int index = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
result[index] = stmt.executeInsert();
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database and returns the row ids.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
|
||||||
*/
|
|
||||||
public final long[] insertAndReturnIdsArray(T[] entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
final long[] result = new long[entities.length];
|
|
||||||
int index = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
result[index] = stmt.executeInsert();
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database and returns the row ids.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
|
||||||
*/
|
|
||||||
public final Long[] insertAndReturnIdsArrayBox(Collection<? extends T> entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
final Long[] result = new Long[entities.size()];
|
|
||||||
int index = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
result[index] = stmt.executeInsert();
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database and returns the row ids.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
|
||||||
*/
|
|
||||||
public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
final Long[] result = new Long[entities.length];
|
|
||||||
int index = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
result[index] = stmt.executeInsert();
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database and returns the row ids.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
|
||||||
*/
|
|
||||||
public final List<Long> insertAndReturnIdsList(T[] entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
final List<Long> result = new ArrayList<>(entities.length);
|
|
||||||
int index = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
result.add(index, stmt.executeInsert());
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the given entities into the database and returns the row ids.
|
|
||||||
*
|
|
||||||
* @param entities Entities to insert
|
|
||||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
|
||||||
*/
|
|
||||||
public final List<Long> insertAndReturnIdsList(Collection<? extends T> entities) {
|
|
||||||
final SupportSQLiteStatement stmt = acquire();
|
|
||||||
try {
|
|
||||||
final List<Long> result = new ArrayList<>(entities.size());
|
|
||||||
int index = 0;
|
|
||||||
for (T entity : entities) {
|
|
||||||
bind(stmt, entity);
|
|
||||||
result.add(index, stmt.executeInsert());
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
release(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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;
|
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class that maintains {@link RoomTrackingLiveData} instances for an
|
|
||||||
* {@link InvalidationTracker}.
|
|
||||||
* <p>
|
|
||||||
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
|
|
||||||
* developer does not hold onto the returned LiveData.
|
|
||||||
*/
|
|
||||||
class InvalidationLiveDataContainer {
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
final Set<LiveData> mLiveDataSet = Collections.newSetFromMap(
|
|
||||||
new IdentityHashMap<LiveData, Boolean>()
|
|
||||||
);
|
|
||||||
private final RoomDatabase mDatabase;
|
|
||||||
|
|
||||||
InvalidationLiveDataContainer(RoomDatabase database) {
|
|
||||||
mDatabase = database;
|
|
||||||
}
|
|
||||||
|
|
||||||
<T> LiveData<T> create(String[] tableNames, boolean inTransaction,
|
|
||||||
Callable<T> computeFunction) {
|
|
||||||
return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
|
|
||||||
tableNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onActive(LiveData liveData) {
|
|
||||||
mLiveDataSet.add(liveData);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onInactive(LiveData liveData) {
|
|
||||||
mLiveDataSet.remove(liveData);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,853 +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 androidx.room;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.annotation.WorkerThread;
|
|
||||||
import androidx.arch.core.internal.SafeIterableMap;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
|
|
||||||
* these tables.
|
|
||||||
*/
|
|
||||||
// Some details on how the InvalidationTracker works:
|
|
||||||
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
|
|
||||||
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
|
|
||||||
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
|
|
||||||
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
|
|
||||||
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
|
|
||||||
// tables.
|
|
||||||
// * Each update (write operation) on one of the observed tables triggers an update into the
|
|
||||||
// memory table table, flipping the invalidated flag ON.
|
|
||||||
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
|
|
||||||
// It works as an Observer, and notifies other instances of table invalidation.
|
|
||||||
public class InvalidationTracker {
|
|
||||||
|
|
||||||
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
|
|
||||||
|
|
||||||
private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
|
|
||||||
|
|
||||||
private static final String TABLE_ID_COLUMN_NAME = "table_id";
|
|
||||||
|
|
||||||
private static final String INVALIDATED_COLUMN_NAME = "invalidated";
|
|
||||||
|
|
||||||
private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
|
|
||||||
+ "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
|
|
||||||
+ INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
|
|
||||||
+ " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
|
|
||||||
+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@VisibleForTesting
|
|
||||||
final HashMap<String, Integer> mTableIdLookup;
|
|
||||||
final String[] mTableNames;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Map<String, Set<String>> mViewTables;
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
|
||||||
final RoomDatabase mDatabase;
|
|
||||||
|
|
||||||
AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
private volatile boolean mInitialized = false;
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
|
||||||
volatile SupportSQLiteStatement mCleanupStatement;
|
|
||||||
|
|
||||||
private ObservedTableTracker mObservedTableTracker;
|
|
||||||
|
|
||||||
private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;
|
|
||||||
|
|
||||||
// should be accessed with synchronization only.
|
|
||||||
@VisibleForTesting
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
|
|
||||||
|
|
||||||
private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used by the generated code.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public InvalidationTracker(RoomDatabase database, String... tableNames) {
|
|
||||||
this(database, new HashMap<String, String>(), Collections.<String, Set<String>>emptyMap(),
|
|
||||||
tableNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used by the generated code.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
|
|
||||||
Map<String, Set<String>> viewTables, String... tableNames) {
|
|
||||||
mDatabase = database;
|
|
||||||
mObservedTableTracker = new ObservedTableTracker(tableNames.length);
|
|
||||||
mTableIdLookup = new HashMap<>();
|
|
||||||
mViewTables = viewTables;
|
|
||||||
mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
|
|
||||||
final int size = tableNames.length;
|
|
||||||
mTableNames = new String[size];
|
|
||||||
for (int id = 0; id < size; id++) {
|
|
||||||
final String tableName = tableNames[id].toLowerCase(Locale.US);
|
|
||||||
mTableIdLookup.put(tableName, id);
|
|
||||||
String shadowTableName = shadowTablesMap.get(tableNames[id]);
|
|
||||||
if (shadowTableName != null) {
|
|
||||||
mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
|
|
||||||
} else {
|
|
||||||
mTableNames[id] = tableName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Adjust table id lookup for those tables whose shadow table is another already mapped
|
|
||||||
// table (e.g. external content fts tables).
|
|
||||||
for (Map.Entry<String, String> shadowTableEntry : shadowTablesMap.entrySet()) {
|
|
||||||
String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
|
|
||||||
if (mTableIdLookup.containsKey(shadowTableName)) {
|
|
||||||
String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
|
|
||||||
mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal method to initialize table tracking.
|
|
||||||
* <p>
|
|
||||||
* You should never call this method, it is called by the generated code.
|
|
||||||
*/
|
|
||||||
void internalInit(SupportSQLiteDatabase database) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (mInitialized) {
|
|
||||||
Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These actions are not in a transaction because temp_store is not allowed to be
|
|
||||||
// performed on a transaction, and recursive_triggers is not affected by transactions.
|
|
||||||
database.execSQL("PRAGMA temp_store = MEMORY;");
|
|
||||||
database.execSQL("PRAGMA recursive_triggers='ON';");
|
|
||||||
database.execSQL(CREATE_TRACKING_TABLE_SQL);
|
|
||||||
syncTriggers(database);
|
|
||||||
mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
|
|
||||||
mInitialized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startMultiInstanceInvalidation(Context context, String name) {
|
|
||||||
mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this,
|
|
||||||
mDatabase.getQueryExecutor());
|
|
||||||
}
|
|
||||||
|
|
||||||
void stopMultiInstanceInvalidation() {
|
|
||||||
if (mMultiInstanceInvalidationClient != null) {
|
|
||||||
mMultiInstanceInvalidationClient.stop();
|
|
||||||
mMultiInstanceInvalidationClient = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendTriggerName(StringBuilder builder, String tableName,
|
|
||||||
String triggerType) {
|
|
||||||
builder.append("`")
|
|
||||||
.append("room_table_modification_trigger_")
|
|
||||||
.append(tableName)
|
|
||||||
.append("_")
|
|
||||||
.append(triggerType)
|
|
||||||
.append("`");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
|
|
||||||
final String tableName = mTableNames[tableId];
|
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
|
||||||
for (String trigger : TRIGGERS) {
|
|
||||||
stringBuilder.setLength(0);
|
|
||||||
stringBuilder.append("DROP TRIGGER IF EXISTS ");
|
|
||||||
appendTriggerName(stringBuilder, tableName, trigger);
|
|
||||||
writableDb.execSQL(stringBuilder.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
|
|
||||||
writableDb.execSQL(
|
|
||||||
"INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
|
|
||||||
final String tableName = mTableNames[tableId];
|
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
|
||||||
for (String trigger : TRIGGERS) {
|
|
||||||
stringBuilder.setLength(0);
|
|
||||||
stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
|
|
||||||
appendTriggerName(stringBuilder, tableName, trigger);
|
|
||||||
stringBuilder.append(" AFTER ")
|
|
||||||
.append(trigger)
|
|
||||||
.append(" ON `")
|
|
||||||
.append(tableName)
|
|
||||||
.append("` BEGIN UPDATE ")
|
|
||||||
.append(UPDATE_TABLE_NAME)
|
|
||||||
.append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
|
|
||||||
.append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
|
|
||||||
.append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
|
|
||||||
.append("; END");
|
|
||||||
writableDb.execSQL(stringBuilder.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given observer to the observers list and it will be notified if any table it
|
|
||||||
* observes changes.
|
|
||||||
* <p>
|
|
||||||
* Database changes are pulled on another thread so in some race conditions, the observer might
|
|
||||||
* be invoked for changes that were done before it is added.
|
|
||||||
* <p>
|
|
||||||
* If the observer already exists, this is a no-op call.
|
|
||||||
* <p>
|
|
||||||
* If one of the tables in the Observer does not exist in the database, this method throws an
|
|
||||||
* {@link IllegalArgumentException}.
|
|
||||||
*
|
|
||||||
* @param observer The observer which listens the database for changes.
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
@WorkerThread
|
|
||||||
public void addObserver(@NonNull Observer observer) {
|
|
||||||
final String[] tableNames = resolveViews(observer.mTables);
|
|
||||||
int[] tableIds = new int[tableNames.length];
|
|
||||||
final int size = tableNames.length;
|
|
||||||
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
|
|
||||||
if (tableId == null) {
|
|
||||||
throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
|
|
||||||
}
|
|
||||||
tableIds[i] = tableId;
|
|
||||||
}
|
|
||||||
ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
|
|
||||||
ObserverWrapper currentObserver;
|
|
||||||
synchronized (mObserverMap) {
|
|
||||||
currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
|
|
||||||
}
|
|
||||||
if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
|
|
||||||
syncTriggers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] validateAndResolveTableNames(String[] tableNames) {
|
|
||||||
String[] resolved = resolveViews(tableNames);
|
|
||||||
for (String tableName : resolved) {
|
|
||||||
if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
|
|
||||||
throw new IllegalArgumentException("There is no table with name " + tableName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the list of tables and views into a list of unique tables that are underlying them.
|
|
||||||
*
|
|
||||||
* @param names The names of tables or views.
|
|
||||||
* @return The names of the underlying tables.
|
|
||||||
*/
|
|
||||||
private String[] resolveViews(String[] names) {
|
|
||||||
Set<String> tables = new HashSet<>();
|
|
||||||
for (String name : names) {
|
|
||||||
final String lowercase = name.toLowerCase(Locale.US);
|
|
||||||
if (mViewTables.containsKey(lowercase)) {
|
|
||||||
tables.addAll(mViewTables.get(lowercase));
|
|
||||||
} else {
|
|
||||||
tables.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tables.toArray(new String[tables.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an observer but keeps a weak reference back to it.
|
|
||||||
* <p>
|
|
||||||
* Note that you cannot remove this observer once added. It will be automatically removed
|
|
||||||
* when the observer is GC'ed.
|
|
||||||
*
|
|
||||||
* @param observer The observer to which InvalidationTracker will keep a weak reference.
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public void addWeakObserver(Observer observer) {
|
|
||||||
addObserver(new WeakObserver(this, observer));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the observer from the observers list.
|
|
||||||
*
|
|
||||||
* @param observer The observer to remove.
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@WorkerThread
|
|
||||||
public void removeObserver(@NonNull final Observer observer) {
|
|
||||||
ObserverWrapper wrapper;
|
|
||||||
synchronized (mObserverMap) {
|
|
||||||
wrapper = mObserverMap.remove(observer);
|
|
||||||
}
|
|
||||||
if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
|
|
||||||
syncTriggers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
|
||||||
boolean ensureInitialization() {
|
|
||||||
if (!mDatabase.isOpen()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!mInitialized) {
|
|
||||||
// trigger initialization
|
|
||||||
mDatabase.getOpenHelper().getWritableDatabase();
|
|
||||||
}
|
|
||||||
if (!mInitialized) {
|
|
||||||
Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
Runnable mRefreshRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
final Lock closeLock = mDatabase.getCloseLock();
|
|
||||||
Set<Integer> invalidatedTableIds = null;
|
|
||||||
try {
|
|
||||||
closeLock.lock();
|
|
||||||
|
|
||||||
if (!ensureInitialization()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mPendingRefresh.compareAndSet(true, false)) {
|
|
||||||
// no pending refresh
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDatabase.inTransaction()) {
|
|
||||||
// current thread is in a transaction. when it ends, it will invoke
|
|
||||||
// refreshRunnable again. mPendingRefresh is left as false on purpose
|
|
||||||
// so that the last transaction can flip it on again.
|
|
||||||
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.beginTransaction();
|
|
||||||
try {
|
|
||||||
invalidatedTableIds = checkUpdatedTable();
|
|
||||||
db.setTransactionSuccessful();
|
|
||||||
} finally {
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
invalidatedTableIds = checkUpdatedTable();
|
|
||||||
}
|
|
||||||
} catch (IllegalStateException | SQLiteException exception) {
|
|
||||||
// may happen if db is closed. just log.
|
|
||||||
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
|
|
||||||
exception);
|
|
||||||
} finally {
|
|
||||||
closeLock.unlock();
|
|
||||||
}
|
|
||||||
if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
|
|
||||||
synchronized (mObserverMap) {
|
|
||||||
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
|
|
||||||
entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<Integer> checkUpdatedTable() {
|
|
||||||
HashSet<Integer> invalidatedTableIds = new HashSet<>();
|
|
||||||
Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
final int tableId = cursor.getInt(0);
|
|
||||||
invalidatedTableIds.add(tableId);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
if (!invalidatedTableIds.isEmpty()) {
|
|
||||||
mCleanupStatement.executeUpdateDelete();
|
|
||||||
}
|
|
||||||
return invalidatedTableIds;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueues a task to refresh the list of updated tables.
|
|
||||||
* <p>
|
|
||||||
* This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
|
|
||||||
* if you have another connection to the database or directly use {@link
|
|
||||||
* SupportSQLiteDatabase}, you may need to call this manually.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public void refreshVersionsAsync() {
|
|
||||||
// TODO we should consider doing this sync instead of async.
|
|
||||||
if (mPendingRefresh.compareAndSet(false, true)) {
|
|
||||||
mDatabase.getQueryExecutor().execute(mRefreshRunnable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check versions for tables, and run observers synchronously if tables have been updated.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
@WorkerThread
|
|
||||||
public void refreshVersionsSync() {
|
|
||||||
syncTriggers();
|
|
||||||
mRefreshRunnable.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies all the registered {@link Observer}s of table changes.
|
|
||||||
* <p>
|
|
||||||
* This can be used for notifying invalidation that cannot be detected by this
|
|
||||||
* {@link InvalidationTracker}, for example, invalidation from another process.
|
|
||||||
*
|
|
||||||
* @param tables The invalidated tables.
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
|
||||||
public void notifyObserversByTableNames(String... tables) {
|
|
||||||
synchronized (mObserverMap) {
|
|
||||||
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
|
|
||||||
if (!entry.getKey().isRemote()) {
|
|
||||||
entry.getValue().notifyByTableNames(tables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void syncTriggers(SupportSQLiteDatabase database) {
|
|
||||||
if (database.inTransaction()) {
|
|
||||||
// we won't run this inside another transaction.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// This method runs in a while loop because while changes are synced to db, another
|
|
||||||
// runnable may be skipped. If we cause it to skip, we need to do its work.
|
|
||||||
while (true) {
|
|
||||||
Lock closeLock = mDatabase.getCloseLock();
|
|
||||||
closeLock.lock();
|
|
||||||
try {
|
|
||||||
// there is a potential race condition where another mSyncTriggers runnable
|
|
||||||
// can start running right after we get the tables list to sync.
|
|
||||||
final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
|
|
||||||
if (tablesToSync == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final int limit = tablesToSync.length;
|
|
||||||
database.beginTransaction();
|
|
||||||
try {
|
|
||||||
for (int tableId = 0; tableId < limit; tableId++) {
|
|
||||||
switch (tablesToSync[tableId]) {
|
|
||||||
case ObservedTableTracker.ADD:
|
|
||||||
startTrackingTable(database, tableId);
|
|
||||||
break;
|
|
||||||
case ObservedTableTracker.REMOVE:
|
|
||||||
stopTrackingTable(database, tableId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
database.setTransactionSuccessful();
|
|
||||||
} finally {
|
|
||||||
database.endTransaction();
|
|
||||||
}
|
|
||||||
mObservedTableTracker.onSyncCompleted();
|
|
||||||
} finally {
|
|
||||||
closeLock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IllegalStateException | SQLiteException exception) {
|
|
||||||
// may happen if db is closed. just log.
|
|
||||||
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
|
|
||||||
exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by RoomDatabase before each beginTransaction call.
|
|
||||||
* <p>
|
|
||||||
* It is important that pending trigger changes are applied to the database before any query
|
|
||||||
* runs. Otherwise, we may miss some changes.
|
|
||||||
* <p>
|
|
||||||
* This api should eventually be public.
|
|
||||||
*/
|
|
||||||
void syncTriggers() {
|
|
||||||
if (!mDatabase.isOpen()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a LiveData that computes the given function once and for every other invalidation
|
|
||||||
* of the database.
|
|
||||||
* <p>
|
|
||||||
* Holds a strong reference to the created LiveData as long as it is active.
|
|
||||||
*
|
|
||||||
* @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
|
|
||||||
*
|
|
||||||
* @param computeFunction The function that calculates the value
|
|
||||||
* @param tableNames The list of tables to observe
|
|
||||||
* @param <T> The return type
|
|
||||||
* @return A new LiveData that computes the given function when the given list of tables
|
|
||||||
* invalidates.
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
|
|
||||||
return createLiveData(tableNames, false, computeFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a LiveData that computes the given function once and for every other invalidation
|
|
||||||
* of the database.
|
|
||||||
* <p>
|
|
||||||
* Holds a strong reference to the created LiveData as long as it is active.
|
|
||||||
*
|
|
||||||
* @param tableNames The list of tables to observe
|
|
||||||
* @param inTransaction True if the computeFunction will be done in a transaction, false
|
|
||||||
* otherwise.
|
|
||||||
* @param computeFunction The function that calculates the value
|
|
||||||
* @param <T> The return type
|
|
||||||
* @return A new LiveData that computes the given function when the given list of tables
|
|
||||||
* invalidates.
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
|
|
||||||
Callable<T> computeFunction) {
|
|
||||||
return mInvalidationLiveDataContainer.create(
|
|
||||||
validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps an observer and keeps the table information.
|
|
||||||
* <p>
|
|
||||||
* Internally table ids are used which may change from database to database so the table
|
|
||||||
* related information is kept here rather than in the Observer.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
static class ObserverWrapper {
|
|
||||||
final int[] mTableIds;
|
|
||||||
private final String[] mTableNames;
|
|
||||||
final Observer mObserver;
|
|
||||||
private final Set<String> mSingleTableSet;
|
|
||||||
|
|
||||||
ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
|
|
||||||
mObserver = observer;
|
|
||||||
mTableIds = tableIds;
|
|
||||||
mTableNames = tableNames;
|
|
||||||
if (tableIds.length == 1) {
|
|
||||||
HashSet<String> set = new HashSet<>();
|
|
||||||
set.add(mTableNames[0]);
|
|
||||||
mSingleTableSet = Collections.unmodifiableSet(set);
|
|
||||||
} else {
|
|
||||||
mSingleTableSet = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
|
|
||||||
* based on the given invalid status set.
|
|
||||||
*
|
|
||||||
* @param invalidatedTablesIds The table ids of the tables that are invalidated.
|
|
||||||
*/
|
|
||||||
void notifyByTableInvalidStatus(Set<Integer> invalidatedTablesIds) {
|
|
||||||
Set<String> invalidatedTables = null;
|
|
||||||
final int size = mTableIds.length;
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
final int tableId = mTableIds[index];
|
|
||||||
if (invalidatedTablesIds.contains(tableId)) {
|
|
||||||
if (size == 1) {
|
|
||||||
// Optimization for a single-table observer
|
|
||||||
invalidatedTables = mSingleTableSet;
|
|
||||||
} else {
|
|
||||||
if (invalidatedTables == null) {
|
|
||||||
invalidatedTables = new HashSet<>(size);
|
|
||||||
}
|
|
||||||
invalidatedTables.add(mTableNames[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (invalidatedTables != null) {
|
|
||||||
mObserver.onInvalidated(invalidatedTables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the underlying {@link #mObserver} if it observes any of the specified
|
|
||||||
* {@code tables}.
|
|
||||||
*
|
|
||||||
* @param tables The invalidated table names.
|
|
||||||
*/
|
|
||||||
void notifyByTableNames(String[] tables) {
|
|
||||||
Set<String> invalidatedTables = null;
|
|
||||||
if (mTableNames.length == 1) {
|
|
||||||
for (String table : tables) {
|
|
||||||
if (table.equalsIgnoreCase(mTableNames[0])) {
|
|
||||||
// Optimization for a single-table observer
|
|
||||||
invalidatedTables = mSingleTableSet;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HashSet<String> set = new HashSet<>();
|
|
||||||
for (String table : tables) {
|
|
||||||
for (String ourTable : mTableNames) {
|
|
||||||
if (ourTable.equalsIgnoreCase(table)) {
|
|
||||||
set.add(ourTable);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (set.size() > 0) {
|
|
||||||
invalidatedTables = set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (invalidatedTables != null) {
|
|
||||||
mObserver.onInvalidated(invalidatedTables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An observer that can listen for changes in the database.
|
|
||||||
*/
|
|
||||||
public abstract static class Observer {
|
|
||||||
final String[] mTables;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observes the given list of tables and views.
|
|
||||||
*
|
|
||||||
* @param firstTable The name of the table or view.
|
|
||||||
* @param rest More names of tables or views.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
protected Observer(@NonNull String firstTable, String... rest) {
|
|
||||||
mTables = Arrays.copyOf(rest, rest.length + 1);
|
|
||||||
mTables[rest.length] = firstTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observes the given list of tables and views.
|
|
||||||
*
|
|
||||||
* @param tables The list of tables or views to observe for changes.
|
|
||||||
*/
|
|
||||||
public Observer(@NonNull String[] tables) {
|
|
||||||
// copy tables in case user modifies them afterwards
|
|
||||||
mTables = Arrays.copyOf(tables, tables.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when one of the observed tables is invalidated in the database.
|
|
||||||
*
|
|
||||||
* @param tables A set of invalidated tables. This is useful when the observer targets
|
|
||||||
* multiple tables and you want to know which table is invalidated. This will
|
|
||||||
* be names of underlying tables when you are observing views.
|
|
||||||
*/
|
|
||||||
public abstract void onInvalidated(@NonNull Set<String> tables);
|
|
||||||
|
|
||||||
boolean isRemote() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
|
|
||||||
* triggers in the database.
|
|
||||||
* <p>
|
|
||||||
* This class is thread safe
|
|
||||||
*/
|
|
||||||
static class ObservedTableTracker {
|
|
||||||
static final int NO_OP = 0; // don't change trigger state for this table
|
|
||||||
static final int ADD = 1; // add triggers for this table
|
|
||||||
static final int REMOVE = 2; // remove triggers for this table
|
|
||||||
|
|
||||||
// number of observers per table
|
|
||||||
final long[] mTableObservers;
|
|
||||||
// trigger state for each table at last sync
|
|
||||||
// this field is updated when syncAndGet is called.
|
|
||||||
final boolean[] mTriggerStates;
|
|
||||||
// when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
|
|
||||||
final int[] mTriggerStateChanges;
|
|
||||||
|
|
||||||
boolean mNeedsSync;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
|
|
||||||
* returning any non-null value from getTablesToSync.
|
|
||||||
* This allows us to workaround any multi-threaded state syncing issues.
|
|
||||||
*/
|
|
||||||
boolean mPendingSync;
|
|
||||||
|
|
||||||
ObservedTableTracker(int tableCount) {
|
|
||||||
mTableObservers = new long[tableCount];
|
|
||||||
mTriggerStates = new boolean[tableCount];
|
|
||||||
mTriggerStateChanges = new int[tableCount];
|
|
||||||
Arrays.fill(mTableObservers, 0);
|
|
||||||
Arrays.fill(mTriggerStates, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if # of triggers is affected.
|
|
||||||
*/
|
|
||||||
boolean onAdded(int... tableIds) {
|
|
||||||
boolean needTriggerSync = false;
|
|
||||||
synchronized (this) {
|
|
||||||
for (int tableId : tableIds) {
|
|
||||||
final long prevObserverCount = mTableObservers[tableId];
|
|
||||||
mTableObservers[tableId] = prevObserverCount + 1;
|
|
||||||
if (prevObserverCount == 0) {
|
|
||||||
mNeedsSync = true;
|
|
||||||
needTriggerSync = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return needTriggerSync;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if # of triggers is affected.
|
|
||||||
*/
|
|
||||||
boolean onRemoved(int... tableIds) {
|
|
||||||
boolean needTriggerSync = false;
|
|
||||||
synchronized (this) {
|
|
||||||
for (int tableId : tableIds) {
|
|
||||||
final long prevObserverCount = mTableObservers[tableId];
|
|
||||||
mTableObservers[tableId] = prevObserverCount - 1;
|
|
||||||
if (prevObserverCount == 1) {
|
|
||||||
mNeedsSync = true;
|
|
||||||
needTriggerSync = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return needTriggerSync;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this returns non-null, you must call onSyncCompleted.
|
|
||||||
*
|
|
||||||
* @return int[] An int array where the index for each tableId has the action for that
|
|
||||||
* table.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
int[] getTablesToSync() {
|
|
||||||
synchronized (this) {
|
|
||||||
if (!mNeedsSync || mPendingSync) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final int tableCount = mTableObservers.length;
|
|
||||||
for (int i = 0; i < tableCount; i++) {
|
|
||||||
final boolean newState = mTableObservers[i] > 0;
|
|
||||||
if (newState != mTriggerStates[i]) {
|
|
||||||
mTriggerStateChanges[i] = newState ? ADD : REMOVE;
|
|
||||||
} else {
|
|
||||||
mTriggerStateChanges[i] = NO_OP;
|
|
||||||
}
|
|
||||||
mTriggerStates[i] = newState;
|
|
||||||
}
|
|
||||||
mPendingSync = true;
|
|
||||||
mNeedsSync = false;
|
|
||||||
return mTriggerStateChanges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if getTablesToSync returned non-null, the called should call onSyncCompleted once it
|
|
||||||
* is done.
|
|
||||||
*/
|
|
||||||
void onSyncCompleted() {
|
|
||||||
synchronized (this) {
|
|
||||||
mPendingSync = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Observer wrapper that keeps a weak reference to the given object.
|
|
||||||
* <p>
|
|
||||||
* This class will automatically unsubscribe when the wrapped observer goes out of memory.
|
|
||||||
*/
|
|
||||||
static class WeakObserver extends Observer {
|
|
||||||
final InvalidationTracker mTracker;
|
|
||||||
final WeakReference<Observer> mDelegateRef;
|
|
||||||
|
|
||||||
WeakObserver(InvalidationTracker tracker, Observer delegate) {
|
|
||||||
super(delegate.mTables);
|
|
||||||
mTracker = tracker;
|
|
||||||
mDelegateRef = new WeakReference<>(delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInvalidated(@NonNull Set<String> tables) {
|
|
||||||
final Observer observer = mDelegateRef.get();
|
|
||||||
if (observer == null) {
|
|
||||||
mTracker.removeObserver(this);
|
|
||||||
} else {
|
|
||||||
observer.onInvalidated(tables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,200 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
|
|
||||||
* {@link MultiInstanceInvalidationService}.
|
|
||||||
*/
|
|
||||||
class MultiInstanceInvalidationClient {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application context.
|
|
||||||
*/
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Context mAppContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the database file.
|
|
||||||
*/
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final String mName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The client ID assigned by {@link MultiInstanceInvalidationService}.
|
|
||||||
*/
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
int mClientId;
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final InvalidationTracker mInvalidationTracker;
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final InvalidationTracker.Observer mObserver;
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@Nullable
|
|
||||||
IMultiInstanceInvalidationService mService;
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Executor mExecutor;
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final IMultiInstanceInvalidationCallback mCallback =
|
|
||||||
new IMultiInstanceInvalidationCallback.Stub() {
|
|
||||||
@Override
|
|
||||||
public void onInvalidation(final String[] tables) {
|
|
||||||
mExecutor.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mInvalidationTracker.notifyObserversByTableNames(tables);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final AtomicBoolean mStopped = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final ServiceConnection mServiceConnection = new ServiceConnection() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
|
|
||||||
mExecutor.execute(mSetUpRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
mExecutor.execute(mRemoveObserverRunnable);
|
|
||||||
mService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Runnable mSetUpRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
final IMultiInstanceInvalidationService service = mService;
|
|
||||||
if (service != null) {
|
|
||||||
mClientId = service.registerCallback(mCallback, mName);
|
|
||||||
mInvalidationTracker.addObserver(mObserver);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Runnable mRemoveObserverRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mInvalidationTracker.removeObserver(mObserver);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Runnable mTearDownRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mInvalidationTracker.removeObserver(mObserver);
|
|
||||||
try {
|
|
||||||
final IMultiInstanceInvalidationService service = mService;
|
|
||||||
if (service != null) {
|
|
||||||
service.unregisterCallback(mCallback, mClientId);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
|
|
||||||
}
|
|
||||||
mAppContext.unbindService(mServiceConnection);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context The Context to be used for binding
|
|
||||||
* {@link IMultiInstanceInvalidationService}.
|
|
||||||
* @param name The name of the database file.
|
|
||||||
* @param invalidationTracker The {@link InvalidationTracker}
|
|
||||||
* @param executor The background executor.
|
|
||||||
*/
|
|
||||||
MultiInstanceInvalidationClient(Context context, String name,
|
|
||||||
InvalidationTracker invalidationTracker, Executor executor) {
|
|
||||||
mAppContext = context.getApplicationContext();
|
|
||||||
mName = name;
|
|
||||||
mInvalidationTracker = invalidationTracker;
|
|
||||||
mExecutor = executor;
|
|
||||||
mObserver = new InvalidationTracker.Observer(invalidationTracker.mTableNames) {
|
|
||||||
@Override
|
|
||||||
public void onInvalidated(@NonNull Set<String> tables) {
|
|
||||||
if (mStopped.get()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final IMultiInstanceInvalidationService service = mService;
|
|
||||||
if (service != null) {
|
|
||||||
service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean isRemote() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Intent intent = new Intent(mAppContext, MultiInstanceInvalidationService.class);
|
|
||||||
mAppContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
|
||||||
if (mStopped.compareAndSet(false, true)) {
|
|
||||||
mExecutor.execute(mTearDownRunnable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.RemoteCallbackList;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class MultiInstanceInvalidationService extends Service {
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
int mMaxClientId = 0;
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final HashMap<Integer, String> mClientNames = new HashMap<>();
|
|
||||||
|
|
||||||
// synthetic access
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final RemoteCallbackList<IMultiInstanceInvalidationCallback> mCallbackList =
|
|
||||||
new RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
|
|
||||||
@Override
|
|
||||||
public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
|
|
||||||
Object cookie) {
|
|
||||||
mClientNames.remove((int) cookie);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final IMultiInstanceInvalidationService.Stub mBinder =
|
|
||||||
new IMultiInstanceInvalidationService.Stub() {
|
|
||||||
|
|
||||||
// Assigns a client ID to the client.
|
|
||||||
@Override
|
|
||||||
public int registerCallback(IMultiInstanceInvalidationCallback callback,
|
|
||||||
String name) {
|
|
||||||
if (name == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
synchronized (mCallbackList) {
|
|
||||||
int clientId = ++mMaxClientId;
|
|
||||||
// Use the client ID as the RemoteCallbackList cookie.
|
|
||||||
if (mCallbackList.register(callback, clientId)) {
|
|
||||||
mClientNames.put(clientId, name);
|
|
||||||
return clientId;
|
|
||||||
} else {
|
|
||||||
--mMaxClientId;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly removes the client.
|
|
||||||
// The client can die without calling this. In that case, mCallbackList
|
|
||||||
// .onCallbackDied() can take care of removal.
|
|
||||||
@Override
|
|
||||||
public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
|
|
||||||
int clientId) {
|
|
||||||
synchronized (mCallbackList) {
|
|
||||||
mCallbackList.unregister(callback);
|
|
||||||
mClientNames.remove(clientId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcasts table invalidation to other instances of the same database file.
|
|
||||||
// The broadcast is not sent to the caller itself.
|
|
||||||
@Override
|
|
||||||
public void broadcastInvalidation(int clientId, String[] tables) {
|
|
||||||
synchronized (mCallbackList) {
|
|
||||||
String name = mClientNames.get(clientId);
|
|
||||||
if (name == null) {
|
|
||||||
Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int count = mCallbackList.beginBroadcast();
|
|
||||||
try {
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
|
|
||||||
String targetName = mClientNames.get(targetClientId);
|
|
||||||
if (clientId == targetClientId // This is the caller itself.
|
|
||||||
|| !name.equals(targetName)) { // Not the same file.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
IMultiInstanceInvalidationCallback callback =
|
|
||||||
mCallbackList.getBroadcastItem(i);
|
|
||||||
callback.onInvalidation(tables);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
mCallbackList.finishBroadcast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return mBinder;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 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;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for Room.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class Room {
|
|
||||||
static final String LOG_TAG = "ROOM";
|
|
||||||
/**
|
|
||||||
* The master table where room keeps its metadata information.
|
|
||||||
*/
|
|
||||||
public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
|
|
||||||
private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
|
|
||||||
* should keep a reference to it and re-use it.
|
|
||||||
*
|
|
||||||
* @param context The context for the database. This is usually the Application context.
|
|
||||||
* @param klass The abstract class which is annotated with {@link Database} and extends
|
|
||||||
* {@link RoomDatabase}.
|
|
||||||
* @param name The name of the database file.
|
|
||||||
* @param <T> The type of the database class.
|
|
||||||
* @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
|
|
||||||
if (name == null || name.trim().length() == 0) {
|
|
||||||
throw new IllegalArgumentException("Cannot build a database with null or empty name."
|
|
||||||
+ " If you are trying to create an in memory database, use Room"
|
|
||||||
+ ".inMemoryDatabaseBuilder");
|
|
||||||
}
|
|
||||||
return new RoomDatabase.Builder<>(context, klass, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
|
|
||||||
* database disappears when the process is killed.
|
|
||||||
* Once a database is built, you should keep a reference to it and re-use it.
|
|
||||||
*
|
|
||||||
* @param context The context for the database. This is usually the Application context.
|
|
||||||
* @param klass The abstract class which is annotated with {@link Database} and extends
|
|
||||||
* {@link RoomDatabase}.
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
|
|
||||||
@NonNull
|
|
||||||
static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {
|
|
||||||
final String fullPackage = klass.getPackage().getName();
|
|
||||||
String name = klass.getCanonicalName();
|
|
||||||
final String postPackageName = fullPackage.isEmpty()
|
|
||||||
? name
|
|
||||||
: (name.substring(fullPackage.length() + 1));
|
|
||||||
final String implName = postPackageName.replace('.', '_') + suffix;
|
|
||||||
//noinspection TryWithIdenticalCatches
|
|
||||||
try {
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final Class<T> aClass = (Class<T>) Class.forName(
|
|
||||||
fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
|
|
||||||
return aClass.newInstance();
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
throw new RuntimeException("cannot find implementation for "
|
|
||||||
+ klass.getCanonicalName() + ". " + implName + " does not exist");
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
throw new RuntimeException("Cannot access the constructor"
|
|
||||||
+ klass.getCanonicalName());
|
|
||||||
} catch (InstantiationException e) {
|
|
||||||
throw new RuntimeException("Failed to create an instance of "
|
|
||||||
+ klass.getCanonicalName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated This type should not be instantiated as it contains only static methods. */
|
|
||||||
@Deprecated
|
|
||||||
@SuppressWarnings("PrivateConstructorForUtilityClass")
|
|
||||||
public Room() {
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,277 +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 androidx.room;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.room.migration.Migration;
|
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An open helper that holds a reference to the configuration until the database is opened.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
|
|
||||||
@Nullable
|
|
||||||
private DatabaseConfiguration mConfiguration;
|
|
||||||
@NonNull
|
|
||||||
private final Delegate mDelegate;
|
|
||||||
@NonNull
|
|
||||||
private final String mIdentityHash;
|
|
||||||
/**
|
|
||||||
* Room v1 had a bug where the hash was not consistent if fields are reordered.
|
|
||||||
* The new has fixes it but we still need to accept the legacy hash.
|
|
||||||
*/
|
|
||||||
@NonNull // b/64290754
|
|
||||||
private final String mLegacyHash;
|
|
||||||
|
|
||||||
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
|
|
||||||
@NonNull String identityHash, @NonNull String legacyHash) {
|
|
||||||
super(delegate.version);
|
|
||||||
mConfiguration = configuration;
|
|
||||||
mDelegate = delegate;
|
|
||||||
mIdentityHash = identityHash;
|
|
||||||
mLegacyHash = legacyHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
|
|
||||||
@NonNull String legacyHash) {
|
|
||||||
this(configuration, delegate, "", legacyHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigure(SupportSQLiteDatabase db) {
|
|
||||||
super.onConfigure(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(SupportSQLiteDatabase db) {
|
|
||||||
boolean isEmptyDatabase = hasEmptySchema(db);
|
|
||||||
mDelegate.createAllTables(db);
|
|
||||||
if (!isEmptyDatabase) {
|
|
||||||
// A 0 version pre-populated database goes through the create path because the
|
|
||||||
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
|
|
||||||
// find the database not to be empty, then it is a pre-populated, we must validate it to
|
|
||||||
// see if its suitable for usage.
|
|
||||||
ValidationResult result = mDelegate.onValidateSchema(db);
|
|
||||||
if (!result.isValid) {
|
|
||||||
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
|
|
||||||
+ result.expectedFoundMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateIdentity(db);
|
|
||||||
mDelegate.onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
boolean migrated = false;
|
|
||||||
if (mConfiguration != null) {
|
|
||||||
List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
|
|
||||||
oldVersion, newVersion);
|
|
||||||
if (migrations != null) {
|
|
||||||
mDelegate.onPreMigrate(db);
|
|
||||||
for (Migration migration : migrations) {
|
|
||||||
migration.migrate(db);
|
|
||||||
}
|
|
||||||
ValidationResult result = mDelegate.onValidateSchema(db);
|
|
||||||
if (!result.isValid) {
|
|
||||||
throw new IllegalStateException("Migration didn't properly handle: "
|
|
||||||
+ result.expectedFoundMsg);
|
|
||||||
}
|
|
||||||
mDelegate.onPostMigrate(db);
|
|
||||||
updateIdentity(db);
|
|
||||||
migrated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!migrated) {
|
|
||||||
if (mConfiguration != null
|
|
||||||
&& !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
|
|
||||||
mDelegate.dropAllTables(db);
|
|
||||||
mDelegate.createAllTables(db);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("A migration from " + oldVersion + " to "
|
|
||||||
+ newVersion + " was required but not found. Please provide the "
|
|
||||||
+ "necessary Migration path via "
|
|
||||||
+ "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
|
|
||||||
+ "destructive migrations via one of the "
|
|
||||||
+ "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
onUpgrade(db, oldVersion, newVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(SupportSQLiteDatabase db) {
|
|
||||||
super.onOpen(db);
|
|
||||||
checkIdentity(db);
|
|
||||||
mDelegate.onOpen(db);
|
|
||||||
// there might be too many configurations etc, just clear it.
|
|
||||||
mConfiguration = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkIdentity(SupportSQLiteDatabase db) {
|
|
||||||
if (hasRoomMasterTable(db)) {
|
|
||||||
String identityHash = null;
|
|
||||||
Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
identityHash = cursor.getString(0);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
|
|
||||||
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
|
|
||||||
+ " you've changed schema but forgot to update the version number. You can"
|
|
||||||
+ " simply fix this by increasing the version number.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No room_master_table, this might an a pre-populated DB, we must validate to see if
|
|
||||||
// its suitable for usage.
|
|
||||||
ValidationResult result = mDelegate.onValidateSchema(db);
|
|
||||||
if (!result.isValid) {
|
|
||||||
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
|
|
||||||
+ result.expectedFoundMsg);
|
|
||||||
}
|
|
||||||
mDelegate.onPostMigrate(db);
|
|
||||||
updateIdentity(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateIdentity(SupportSQLiteDatabase db) {
|
|
||||||
createMasterTableIfNotExists(db);
|
|
||||||
db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
|
|
||||||
db.execSQL(RoomMasterTable.CREATE_QUERY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
|
|
||||||
Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
|
|
||||||
+ RoomMasterTable.TABLE_NAME + "'");
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
return cursor.moveToFirst() && cursor.getInt(0) != 0;
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasEmptySchema(SupportSQLiteDatabase db) {
|
|
||||||
Cursor cursor = db.query(
|
|
||||||
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'");
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
return cursor.moveToFirst() && cursor.getInt(0) == 0;
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public abstract static class Delegate {
|
|
||||||
public final int version;
|
|
||||||
|
|
||||||
public Delegate(int version) {
|
|
||||||
this.version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void dropAllTables(SupportSQLiteDatabase database);
|
|
||||||
|
|
||||||
protected abstract void createAllTables(SupportSQLiteDatabase database);
|
|
||||||
|
|
||||||
protected abstract void onOpen(SupportSQLiteDatabase database);
|
|
||||||
|
|
||||||
protected abstract void onCreate(SupportSQLiteDatabase database);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a migration run to validate database integrity.
|
|
||||||
*
|
|
||||||
* @param db The SQLite database.
|
|
||||||
*
|
|
||||||
* @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)}
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
protected void validateMigration(SupportSQLiteDatabase db) {
|
|
||||||
throw new UnsupportedOperationException("validateMigration is deprecated");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a migration run or pre-package database copy to validate database integrity.
|
|
||||||
*
|
|
||||||
* @param db The SQLite database.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@NonNull
|
|
||||||
protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) {
|
|
||||||
validateMigration(db);
|
|
||||||
return new ValidationResult(true, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before migrations execute to perform preliminary work.
|
|
||||||
* @param database The SQLite database.
|
|
||||||
*/
|
|
||||||
protected void onPreMigrate(SupportSQLiteDatabase database) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after migrations execute to perform additional work.
|
|
||||||
* @param database The SQLite database.
|
|
||||||
*/
|
|
||||||
protected void onPostMigrate(SupportSQLiteDatabase database) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public static class ValidationResult {
|
|
||||||
|
|
||||||
public final boolean isValid;
|
|
||||||
@Nullable
|
|
||||||
public final String expectedFoundMsg;
|
|
||||||
|
|
||||||
public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) {
|
|
||||||
this.isValid = isValid;
|
|
||||||
this.expectedFoundMsg = expectedFoundMsg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,299 +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 androidx.room;
|
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteProgram;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used as an intermediate place to keep binding arguments so that we can run
|
|
||||||
* Cursor queries with correct types rather than passing everything as a string.
|
|
||||||
* <p>
|
|
||||||
* Because it is relatively a big object, they are pooled and must be released after each use.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
// Maximum number of queries we'll keep cached.
|
|
||||||
static final int POOL_LIMIT = 15;
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
|
|
||||||
// clear the bigger queries (# of arguments).
|
|
||||||
static final int DESIRED_POOL_SIZE = 10;
|
|
||||||
private volatile String mQuery;
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
final long[] mLongBindings;
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
final double[] mDoubleBindings;
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
final String[] mStringBindings;
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
final byte[][] mBlobBindings;
|
|
||||||
|
|
||||||
@Binding
|
|
||||||
private final int[] mBindingTypes;
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
final int mCapacity;
|
|
||||||
// number of arguments in the query
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
int mArgCount;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
@VisibleForTesting
|
|
||||||
static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
|
|
||||||
*
|
|
||||||
* @param supportSQLiteQuery The query to copy from
|
|
||||||
* @return A new query copied from the provided one.
|
|
||||||
*/
|
|
||||||
public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
|
|
||||||
final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
|
|
||||||
supportSQLiteQuery.getSql(),
|
|
||||||
supportSQLiteQuery.getArgCount());
|
|
||||||
supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
|
|
||||||
@Override
|
|
||||||
public void bindNull(int index) {
|
|
||||||
query.bindNull(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindLong(int index, long value) {
|
|
||||||
query.bindLong(index, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindDouble(int index, double value) {
|
|
||||||
query.bindDouble(index, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindString(int index, String value) {
|
|
||||||
query.bindString(index, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindBlob(int index, byte[] value) {
|
|
||||||
query.bindBlob(index, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearBindings() {
|
|
||||||
query.clearBindings();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
// ignored.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
|
|
||||||
* given query.
|
|
||||||
*
|
|
||||||
* @param query The query to prepare
|
|
||||||
* @param argumentCount The number of query arguments
|
|
||||||
* @return A RoomSQLiteQuery that holds the given query and has space for the given number of
|
|
||||||
* arguments.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public static RoomSQLiteQuery acquire(String query, int argumentCount) {
|
|
||||||
synchronized (sQueryPool) {
|
|
||||||
final Map.Entry<Integer, RoomSQLiteQuery> entry =
|
|
||||||
sQueryPool.ceilingEntry(argumentCount);
|
|
||||||
if (entry != null) {
|
|
||||||
sQueryPool.remove(entry.getKey());
|
|
||||||
final RoomSQLiteQuery sqliteQuery = entry.getValue();
|
|
||||||
sqliteQuery.init(query, argumentCount);
|
|
||||||
return sqliteQuery;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
|
|
||||||
sqLiteQuery.init(query, argumentCount);
|
|
||||||
return sqLiteQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RoomSQLiteQuery(int capacity) {
|
|
||||||
mCapacity = capacity;
|
|
||||||
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
|
|
||||||
int limit = capacity + 1;
|
|
||||||
//noinspection WrongConstant
|
|
||||||
mBindingTypes = new int[limit];
|
|
||||||
mLongBindings = new long[limit];
|
|
||||||
mDoubleBindings = new double[limit];
|
|
||||||
mStringBindings = new String[limit];
|
|
||||||
mBlobBindings = new byte[limit][];
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
void init(String query, int argCount) {
|
|
||||||
mQuery = query;
|
|
||||||
mArgCount = argCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the query back to the pool.
|
|
||||||
* <p>
|
|
||||||
* After released, the statement might be returned when {@link #acquire(String, int)} is called
|
|
||||||
* so you should never re-use it after releasing.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public void release() {
|
|
||||||
synchronized (sQueryPool) {
|
|
||||||
sQueryPool.put(mCapacity, this);
|
|
||||||
prunePoolLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void prunePoolLocked() {
|
|
||||||
if (sQueryPool.size() > POOL_LIMIT) {
|
|
||||||
int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
|
|
||||||
final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
|
|
||||||
while (toBeRemoved-- > 0) {
|
|
||||||
iterator.next();
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSql() {
|
|
||||||
return mQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getArgCount() {
|
|
||||||
return mArgCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindTo(SupportSQLiteProgram program) {
|
|
||||||
for (int index = 1; index <= mArgCount; index++) {
|
|
||||||
switch (mBindingTypes[index]) {
|
|
||||||
case NULL:
|
|
||||||
program.bindNull(index);
|
|
||||||
break;
|
|
||||||
case LONG:
|
|
||||||
program.bindLong(index, mLongBindings[index]);
|
|
||||||
break;
|
|
||||||
case DOUBLE:
|
|
||||||
program.bindDouble(index, mDoubleBindings[index]);
|
|
||||||
break;
|
|
||||||
case STRING:
|
|
||||||
program.bindString(index, mStringBindings[index]);
|
|
||||||
break;
|
|
||||||
case BLOB:
|
|
||||||
program.bindBlob(index, mBlobBindings[index]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindNull(int index) {
|
|
||||||
mBindingTypes[index] = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindLong(int index, long value) {
|
|
||||||
mBindingTypes[index] = LONG;
|
|
||||||
mLongBindings[index] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindDouble(int index, double value) {
|
|
||||||
mBindingTypes[index] = DOUBLE;
|
|
||||||
mDoubleBindings[index] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindString(int index, String value) {
|
|
||||||
mBindingTypes[index] = STRING;
|
|
||||||
mStringBindings[index] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindBlob(int index, byte[] value) {
|
|
||||||
mBindingTypes[index] = BLOB;
|
|
||||||
mBlobBindings[index] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
// no-op. not calling release because it is internal API.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies arguments from another RoomSQLiteQuery into this query.
|
|
||||||
*
|
|
||||||
* @param other The other query, which holds the arguments to be copied.
|
|
||||||
*/
|
|
||||||
public void copyArgumentsFrom(RoomSQLiteQuery other) {
|
|
||||||
int argCount = other.getArgCount() + 1; // +1 for the binding offsets
|
|
||||||
System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
|
|
||||||
System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
|
|
||||||
System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
|
|
||||||
System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
|
|
||||||
System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearBindings() {
|
|
||||||
Arrays.fill(mBindingTypes, NULL);
|
|
||||||
Arrays.fill(mStringBindings, null);
|
|
||||||
Arrays.fill(mBlobBindings, null);
|
|
||||||
mQuery = null;
|
|
||||||
// no need to clear others
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int NULL = 1;
|
|
||||||
private static final int LONG = 2;
|
|
||||||
private static final int DOUBLE = 3;
|
|
||||||
private static final int STRING = 4;
|
|
||||||
private static final int BLOB = 5;
|
|
||||||
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
@IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
|
|
||||||
@interface Binding {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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;
|
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
|
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.WorkerThread;
|
|
||||||
import androidx.arch.core.executor.ArchTaskExecutor;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A LiveData implementation that closely works with {@link InvalidationTracker} to implement
|
|
||||||
* database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
|
|
||||||
* as they are active.
|
|
||||||
* <p>
|
|
||||||
* We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
|
|
||||||
* observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
|
|
||||||
* memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
|
|
||||||
* {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
|
|
||||||
* extra query on every UI rotation.
|
|
||||||
* <p>
|
|
||||||
* This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
|
|
||||||
* strongly by the {@link InvalidationTracker} as long as it is active.
|
|
||||||
*/
|
|
||||||
class RoomTrackingLiveData<T> extends LiveData<T> {
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final RoomDatabase mDatabase;
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final boolean mInTransaction;
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Callable<T> mComputeFunction;
|
|
||||||
|
|
||||||
private final InvalidationLiveDataContainer mContainer;
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final InvalidationTracker.Observer mObserver;
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final AtomicBoolean mInvalid = new AtomicBoolean(true);
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final AtomicBoolean mComputing = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Runnable mRefreshRunnable = new Runnable() {
|
|
||||||
@WorkerThread
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (mRegisteredObserver.compareAndSet(false, true)) {
|
|
||||||
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
|
|
||||||
}
|
|
||||||
boolean computed;
|
|
||||||
do {
|
|
||||||
computed = false;
|
|
||||||
// compute can happen only in 1 thread but no reason to lock others.
|
|
||||||
if (mComputing.compareAndSet(false, true)) {
|
|
||||||
// as long as it is invalid, keep computing.
|
|
||||||
try {
|
|
||||||
T value = null;
|
|
||||||
while (mInvalid.compareAndSet(true, false)) {
|
|
||||||
computed = true;
|
|
||||||
try {
|
|
||||||
value = mComputeFunction.call();
|
|
||||||
} catch (Exception e) {
|
|
||||||
eu.faircode.email.Log.w(e);
|
|
||||||
//throw new RuntimeException("Exception while computing database"
|
|
||||||
// + " live data.", e);
|
|
||||||
computed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (computed) {
|
|
||||||
postValue(value);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// release compute lock
|
|
||||||
mComputing.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check invalid after releasing compute lock to avoid the following scenario.
|
|
||||||
// Thread A runs compute()
|
|
||||||
// Thread A checks invalid, it is false
|
|
||||||
// Main thread sets invalid to true
|
|
||||||
// Thread B runs, fails to acquire compute lock and skips
|
|
||||||
// Thread A releases compute lock
|
|
||||||
// We've left invalid in set state. The check below recovers.
|
|
||||||
} while (computed && mInvalid.get());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
final Runnable mInvalidationRunnable = new Runnable() {
|
|
||||||
@MainThread
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean isActive = hasActiveObservers();
|
|
||||||
if (mInvalid.compareAndSet(false, true)) {
|
|
||||||
if (isActive) {
|
|
||||||
getQueryExecutor().execute(mRefreshRunnable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
RoomTrackingLiveData(
|
|
||||||
RoomDatabase database,
|
|
||||||
InvalidationLiveDataContainer container,
|
|
||||||
boolean inTransaction,
|
|
||||||
Callable<T> computeFunction,
|
|
||||||
String[] tableNames) {
|
|
||||||
mDatabase = database;
|
|
||||||
mInTransaction = inTransaction;
|
|
||||||
mComputeFunction = computeFunction;
|
|
||||||
mContainer = container;
|
|
||||||
mObserver = new InvalidationTracker.Observer(tableNames) {
|
|
||||||
@Override
|
|
||||||
public void onInvalidated(@NonNull Set<String> tables) {
|
|
||||||
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActive() {
|
|
||||||
super.onActive();
|
|
||||||
mContainer.onActive(this);
|
|
||||||
getQueryExecutor().execute(mRefreshRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onInactive() {
|
|
||||||
super.onInactive();
|
|
||||||
mContainer.onInactive(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Executor getQueryExecutor() {
|
|
||||||
if (mInTransaction) {
|
|
||||||
return mDatabase.getTransactionExecutor();
|
|
||||||
} else {
|
|
||||||
return mDatabase.getQueryExecutor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,205 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.room.util.CopyLock;
|
|
||||||
import androidx.room.util.DBUtil;
|
|
||||||
import androidx.room.util.FileUtil;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.channels.Channels;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.nio.channels.ReadableByteChannel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
|
|
||||||
* storage.
|
|
||||||
*/
|
|
||||||
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final Context mContext;
|
|
||||||
@Nullable
|
|
||||||
private final String mCopyFromAssetPath;
|
|
||||||
@Nullable
|
|
||||||
private final File mCopyFromFile;
|
|
||||||
private final int mDatabaseVersion;
|
|
||||||
@NonNull
|
|
||||||
private final SupportSQLiteOpenHelper mDelegate;
|
|
||||||
@Nullable
|
|
||||||
private DatabaseConfiguration mDatabaseConfiguration;
|
|
||||||
|
|
||||||
private boolean mVerified;
|
|
||||||
|
|
||||||
SQLiteCopyOpenHelper(
|
|
||||||
@NonNull Context context,
|
|
||||||
@Nullable String copyFromAssetPath,
|
|
||||||
@Nullable File copyFromFile,
|
|
||||||
int databaseVersion,
|
|
||||||
@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
|
|
||||||
mContext = context;
|
|
||||||
mCopyFromAssetPath = copyFromAssetPath;
|
|
||||||
mCopyFromFile = copyFromFile;
|
|
||||||
mDatabaseVersion = databaseVersion;
|
|
||||||
mDelegate = supportSQLiteOpenHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDatabaseName() {
|
|
||||||
return mDelegate.getDatabaseName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
|
||||||
public void setWriteAheadLoggingEnabled(boolean enabled) {
|
|
||||||
mDelegate.setWriteAheadLoggingEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized SupportSQLiteDatabase getWritableDatabase() {
|
|
||||||
if (!mVerified) {
|
|
||||||
verifyDatabaseFile();
|
|
||||||
mVerified = true;
|
|
||||||
}
|
|
||||||
return mDelegate.getWritableDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized SupportSQLiteDatabase getReadableDatabase() {
|
|
||||||
if (!mVerified) {
|
|
||||||
verifyDatabaseFile();
|
|
||||||
mVerified = true;
|
|
||||||
}
|
|
||||||
return mDelegate.getReadableDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void close() {
|
|
||||||
mDelegate.close();
|
|
||||||
mVerified = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can't be constructor param because the factory is needed by the database builder which in
|
|
||||||
// turn is the one that actually builds the configuration.
|
|
||||||
void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) {
|
|
||||||
mDatabaseConfiguration = databaseConfiguration;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyDatabaseFile() {
|
|
||||||
String databaseName = getDatabaseName();
|
|
||||||
File databaseFile = mContext.getDatabasePath(databaseName);
|
|
||||||
boolean processLevelLock = mDatabaseConfiguration == null
|
|
||||||
|| mDatabaseConfiguration.multiInstanceInvalidation;
|
|
||||||
CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock);
|
|
||||||
try {
|
|
||||||
// Acquire a copy lock, this lock works across threads and processes, preventing
|
|
||||||
// concurrent copy attempts from occurring.
|
|
||||||
copyLock.lock();
|
|
||||||
|
|
||||||
if (!databaseFile.exists()) {
|
|
||||||
try {
|
|
||||||
// No database file found, copy and be done.
|
|
||||||
copyDatabaseFile(databaseFile);
|
|
||||||
return;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Unable to copy database file.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDatabaseConfiguration == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A database file is present, check if we need to re-copy it.
|
|
||||||
int currentVersion;
|
|
||||||
try {
|
|
||||||
currentVersion = DBUtil.readVersion(databaseFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(Room.LOG_TAG, "Unable to read database version.", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentVersion == mDatabaseVersion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) {
|
|
||||||
// From the current version to the desired version a migration is required, i.e.
|
|
||||||
// we won't be performing a copy destructive migration.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mContext.deleteDatabase(databaseName)) {
|
|
||||||
try {
|
|
||||||
copyDatabaseFile(databaseFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// We are more forgiving copying a database on a destructive migration since
|
|
||||||
// there is already a database file that can be opened.
|
|
||||||
Log.w(Room.LOG_TAG, "Unable to copy database file.", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(Room.LOG_TAG, "Failed to delete database file ("
|
|
||||||
+ databaseName + ") for a copy destructive migration.");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
copyLock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void copyDatabaseFile(File destinationFile) throws IOException {
|
|
||||||
ReadableByteChannel input;
|
|
||||||
if (mCopyFromAssetPath != null) {
|
|
||||||
input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
|
|
||||||
} else if (mCopyFromFile != null) {
|
|
||||||
input = new FileInputStream(mCopyFromFile).getChannel();
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("copyFromAssetPath and copyFromFile == null!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// An intermediate file is used so that we never end up with a half-copied database file
|
|
||||||
// in the internal directory.
|
|
||||||
File intermediateFile = File.createTempFile(
|
|
||||||
"room-copy-helper", ".tmp", mContext.getCacheDir());
|
|
||||||
intermediateFile.deleteOnExit();
|
|
||||||
FileChannel output = new FileOutputStream(intermediateFile).getChannel();
|
|
||||||
FileUtil.copy(input, output);
|
|
||||||
|
|
||||||
File parent = destinationFile.getParentFile();
|
|
||||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
|
||||||
throw new IOException("Failed to create directories for "
|
|
||||||
+ destinationFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!intermediateFile.renameTo(destinationFile)) {
|
|
||||||
throw new IOException("Failed to move intermediate file ("
|
|
||||||
+ intermediateFile.getAbsolutePath() + ") to destination ("
|
|
||||||
+ destinationFile.getAbsolutePath() + ").");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
|
|
||||||
* {@link SQLiteCopyOpenHelper}.
|
|
||||||
*/
|
|
||||||
class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private final String mCopyFromAssetPath;
|
|
||||||
@Nullable
|
|
||||||
private final File mCopyFromFile;
|
|
||||||
@NonNull
|
|
||||||
private final SupportSQLiteOpenHelper.Factory mDelegate;
|
|
||||||
|
|
||||||
SQLiteCopyOpenHelperFactory(
|
|
||||||
@Nullable String copyFromAssetPath,
|
|
||||||
@Nullable File copyFromFile,
|
|
||||||
@NonNull SupportSQLiteOpenHelper.Factory factory) {
|
|
||||||
mCopyFromAssetPath = copyFromAssetPath;
|
|
||||||
mCopyFromFile = copyFromFile;
|
|
||||||
mDelegate = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
|
|
||||||
return new SQLiteCopyOpenHelper(
|
|
||||||
configuration.context,
|
|
||||||
mCopyFromAssetPath,
|
|
||||||
mCopyFromFile,
|
|
||||||
configuration.callback.version,
|
|
||||||
mDelegate.create(configuration));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 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;
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a prepared SQLite state that can be re-used multiple times.
|
|
||||||
* <p>
|
|
||||||
* This class is used by generated code. After it is used, {@code release} must be called so that
|
|
||||||
* it can be used by other threads.
|
|
||||||
* <p>
|
|
||||||
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
|
|
||||||
* statement until it is released.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public abstract class SharedSQLiteStatement {
|
|
||||||
private final AtomicBoolean mLock = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
private final RoomDatabase mDatabase;
|
|
||||||
private volatile SupportSQLiteStatement mStmt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
|
|
||||||
* it automatically creates a new one.
|
|
||||||
*
|
|
||||||
* @param database The database to create the statement in.
|
|
||||||
*/
|
|
||||||
public SharedSQLiteStatement(RoomDatabase database) {
|
|
||||||
mDatabase = database;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the query.
|
|
||||||
*
|
|
||||||
* @return The SQL query to prepare.
|
|
||||||
*/
|
|
||||||
protected abstract String createQuery();
|
|
||||||
|
|
||||||
protected void assertNotMainThread() {
|
|
||||||
mDatabase.assertNotMainThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SupportSQLiteStatement createNewStatement() {
|
|
||||||
String query = createQuery();
|
|
||||||
return mDatabase.compileStatement(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SupportSQLiteStatement getStmt(boolean canUseCached) {
|
|
||||||
final SupportSQLiteStatement stmt;
|
|
||||||
if (canUseCached) {
|
|
||||||
if (mStmt == null) {
|
|
||||||
mStmt = createNewStatement();
|
|
||||||
}
|
|
||||||
stmt = mStmt;
|
|
||||||
} else {
|
|
||||||
// it is in use, create a one off statement
|
|
||||||
stmt = createNewStatement();
|
|
||||||
}
|
|
||||||
return stmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
|
|
||||||
*/
|
|
||||||
public SupportSQLiteStatement acquire() {
|
|
||||||
assertNotMainThread();
|
|
||||||
return getStmt(mLock.compareAndSet(false, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Must call this when statement will not be used anymore.
|
|
||||||
*
|
|
||||||
* @param statement The statement that was returned from acquire.
|
|
||||||
*/
|
|
||||||
public void release(SupportSQLiteStatement statement) {
|
|
||||||
if (statement == mStmt) {
|
|
||||||
mLock.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executor wrapper for performing database transactions serially.
|
|
||||||
* <p>
|
|
||||||
* Since database transactions are exclusive, this executor ensures that transactions are performed
|
|
||||||
* in-order and one at a time, preventing threads from blocking each other when multiple concurrent
|
|
||||||
* transactions are attempted.
|
|
||||||
*/
|
|
||||||
class TransactionExecutor implements Executor {
|
|
||||||
|
|
||||||
private final Executor mExecutor;
|
|
||||||
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
|
|
||||||
private Runnable mActive;
|
|
||||||
|
|
||||||
TransactionExecutor(@NonNull Executor executor) {
|
|
||||||
mExecutor = executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void execute(final Runnable command) {
|
|
||||||
mTasks.offer(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
command.run();
|
|
||||||
} finally {
|
|
||||||
scheduleNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (mActive == null) {
|
|
||||||
scheduleNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
synchronized void scheduleNext() {
|
|
||||||
if ((mActive = mTasks.poll()) != null) {
|
|
||||||
mExecutor.execute(mActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +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 androidx.room.migration;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for a database migration.
|
|
||||||
* <p>
|
|
||||||
* Each migration can move between 2 versions that are defined by {@link #startVersion} and
|
|
||||||
* {@link #endVersion}.
|
|
||||||
* <p>
|
|
||||||
* A migration can handle more than 1 version (e.g. if you have a faster path to choose when
|
|
||||||
* going version 3 to 5 without going to version 4). If Room opens a database at version
|
|
||||||
* 3 and latest version is >= 5, Room will use the migration object that can migrate from
|
|
||||||
* 3 to 5 instead of 3 to 4 and 4 to 5.
|
|
||||||
* <p>
|
|
||||||
* If there are not enough migrations provided to move from the current version to the latest
|
|
||||||
* version, Room will clear the database and recreate so even if you have no changes between 2
|
|
||||||
* versions, you should still provide a Migration object to the builder.
|
|
||||||
*/
|
|
||||||
public abstract class Migration {
|
|
||||||
public final int startVersion;
|
|
||||||
public final int endVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new migration between {@code startVersion} and {@code endVersion}.
|
|
||||||
*
|
|
||||||
* @param startVersion The start version of the database.
|
|
||||||
* @param endVersion The end version of the database after this migration is applied.
|
|
||||||
*/
|
|
||||||
public Migration(int startVersion, int endVersion) {
|
|
||||||
this.startVersion = startVersion;
|
|
||||||
this.endVersion = endVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should run the necessary migrations.
|
|
||||||
* <p>
|
|
||||||
* This class cannot access any generated Dao in this method.
|
|
||||||
* <p>
|
|
||||||
* This method is already called inside a transaction and that transaction might actually be a
|
|
||||||
* composite transaction of all necessary {@code Migration}s.
|
|
||||||
*
|
|
||||||
* @param database The database instance
|
|
||||||
*/
|
|
||||||
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
|
|
||||||
}
|
|
@ -1,129 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Room is a Database Object Mapping library that makes it easy to access database on Android
|
|
||||||
* applications.
|
|
||||||
* <p>
|
|
||||||
* Rather than hiding the details of SQLite, Room tries to embrace them by providing convenient APIs
|
|
||||||
* to query the database and also verify such queries at compile time. This allows you to access
|
|
||||||
* the full power of SQLite while having the type safety provided by Java SQL query builders.
|
|
||||||
* <p>
|
|
||||||
* There are 3 major components in Room.
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link androidx.room.Database Database}: This annotation marks a class as a database.
|
|
||||||
* It should be an abstract class that extends {@link androidx.room.RoomDatabase RoomDatabase}.
|
|
||||||
* At runtime, you can acquire an instance of it via {@link androidx.room.Room#databaseBuilder(
|
|
||||||
* android.content.Context,java.lang.Class, java.lang.String) Room.databaseBuilder} or
|
|
||||||
* {@link androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context, java.lang.Class)
|
|
||||||
* Room.inMemoryDatabaseBuilder}.
|
|
||||||
* <p>
|
|
||||||
* The database class defines the list of entities and data access objects in the database.
|
|
||||||
* It is also the main access point for the underlying connection.
|
|
||||||
* </li>
|
|
||||||
* <li>{@link androidx.room.Entity Entity}: This annotation marks a class as a database row.
|
|
||||||
* For each {@link androidx.room.Entity Entity}, a database table is created to hold the items.
|
|
||||||
* The Entity class must be referenced in the
|
|
||||||
* {@link androidx.room.Database#entities() Database#entities} array. Each field of the Entity
|
|
||||||
* (and its super class) is persisted in the database unless it is denoted otherwise
|
|
||||||
* (see {@link androidx.room.Entity Entity} docs for details).
|
|
||||||
* </li>
|
|
||||||
* <li>{@link androidx.room.Dao Dao}: This annotation marks a class or interface as a
|
|
||||||
* Data Access Object. Data access objects are the main components of Room that are
|
|
||||||
* responsible for defining the methods that access the database. The class that is annotated
|
|
||||||
* with {@link androidx.room.Database Database} must have an abstract method that has 0
|
|
||||||
* arguments and returns the class that is annotated with Dao. While generating the code at
|
|
||||||
* compile time, Room will generate an implementation of this class.
|
|
||||||
* <p>
|
|
||||||
* Using Dao classes for database access rather than query builders or direct queries allows you
|
|
||||||
* to keep a separation between different components and easily mock the database access while
|
|
||||||
* testing your application.
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
|
||||||
* Below is a sample of a simple database.
|
|
||||||
* <pre>
|
|
||||||
* // File: Song.java
|
|
||||||
* {@literal @}Entity
|
|
||||||
* public class User {
|
|
||||||
* {@literal @}PrimaryKey
|
|
||||||
* private int id;
|
|
||||||
* private String name;
|
|
||||||
* {@literal @}ColumnInfo(name = "release_year")
|
|
||||||
* private int releaseYear;
|
|
||||||
* // getters and setters are ignored for brevity but they are required for Room to work.
|
|
||||||
* }
|
|
||||||
* // File: SongDao.java
|
|
||||||
* {@literal @}Dao
|
|
||||||
* public interface SongDao {
|
|
||||||
* {@literal @}Query("SELECT * FROM song")
|
|
||||||
* List<Song> loadAll();
|
|
||||||
* {@literal @}Query("SELECT * FROM song WHERE id IN (:songIds)")
|
|
||||||
* List<Song> loadAllBySongId(int... songIds);
|
|
||||||
* {@literal @}Query("SELECT * FROM song WHERE name LIKE :name AND release_year = :year LIMIT 1")
|
|
||||||
* Song loadOneByNameAndReleaseYear(String first, int year);
|
|
||||||
* {@literal @}Insert
|
|
||||||
* void insertAll(Song... songs);
|
|
||||||
* {@literal @}Delete
|
|
||||||
* void delete(Song song);
|
|
||||||
* }
|
|
||||||
* // File: MusicDatabase.java
|
|
||||||
* {@literal @}Database(entities = {Song.java})
|
|
||||||
* public abstract class MusicDatabase extends RoomDatabase {
|
|
||||||
* public abstract SongDao userDao();
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* You can create an instance of {@code MusicDatabase} as follows:
|
|
||||||
* <pre>
|
|
||||||
* MusicDatabase db = Room
|
|
||||||
* .databaseBuilder(getApplicationContext(), MusicDatabase.class, "database-name")
|
|
||||||
* .build();
|
|
||||||
* </pre>
|
|
||||||
* Since Room verifies your queries at compile time, it also detects information about which tables
|
|
||||||
* are accessed by the query or what columns are present in the response.
|
|
||||||
* <p>
|
|
||||||
* You can observe a particular table for changes using the
|
|
||||||
* {@link androidx.room.InvalidationTracker InvalidationTracker} class which you can acquire via
|
|
||||||
* {@link androidx.room.RoomDatabase#getInvalidationTracker()
|
|
||||||
* RoomDatabase.getInvalidationTracker}.
|
|
||||||
* <p>
|
|
||||||
* For convenience, Room allows you to return {@link androidx.lifecycle.LiveData LiveData} from
|
|
||||||
* {@link androidx.room.Query Query} methods. It will automatically observe the related tables as
|
|
||||||
* long as the {@code LiveData} has active observers.
|
|
||||||
* <pre>
|
|
||||||
* // This live data will automatically dispatch changes as the database changes.
|
|
||||||
* {@literal @}Query("SELECT * FROM song ORDER BY name LIMIT 5")
|
|
||||||
* LiveData<Song> loadFirstFiveSongs();
|
|
||||||
* </pre>
|
|
||||||
* <p>
|
|
||||||
* You can also return arbitrary data objects from your query results as long as the fields in the
|
|
||||||
* object match the list of columns in the query response. This makes it very easy to write
|
|
||||||
* applications that drive the UI from persistent storage.
|
|
||||||
* <pre>
|
|
||||||
* class IdAndSongHeader {
|
|
||||||
* int id;
|
|
||||||
* {@literal @}ColumnInfo(name = "header")
|
|
||||||
* String header;
|
|
||||||
* }
|
|
||||||
* // DAO
|
|
||||||
* {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM user")
|
|
||||||
* public IdAndSongHeader[] loadSongHeaders();
|
|
||||||
* </pre>
|
|
||||||
* If there is a mismatch between the query result and the POJO, Room will print a warning during
|
|
||||||
* compilation.
|
|
||||||
* <p>
|
|
||||||
* Please see the documentation of individual classes for details.
|
|
||||||
*/
|
|
||||||
package androidx.room;
|
|
@ -1,195 +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 androidx.room.paging;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.paging.PositionalDataSource;
|
|
||||||
import androidx.room.InvalidationTracker;
|
|
||||||
import androidx.room.RoomDatabase;
|
|
||||||
import androidx.room.RoomSQLiteQuery;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple data source implementation that uses Limit & Offset to page the query.
|
|
||||||
* <p>
|
|
||||||
* This is NOT the most efficient way to do paging on SQLite. It is
|
|
||||||
* <a href="http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor">recommended</a> to use an indexed
|
|
||||||
* ORDER BY statement but that requires a more complex API. This solution is technically equal to
|
|
||||||
* receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
|
|
||||||
* never returns inconsistent data if it is invalidated.
|
|
||||||
*
|
|
||||||
* @param <T> Data type returned by the data source.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> {
|
|
||||||
private final RoomSQLiteQuery mSourceQuery;
|
|
||||||
private final String mCountQuery;
|
|
||||||
private final String mLimitOffsetQuery;
|
|
||||||
private final RoomDatabase mDb;
|
|
||||||
@SuppressWarnings("FieldCanBeLocal")
|
|
||||||
private final InvalidationTracker.Observer mObserver;
|
|
||||||
private final boolean mInTransaction;
|
|
||||||
|
|
||||||
protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
|
|
||||||
boolean inTransaction, String... tables) {
|
|
||||||
this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, 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) {
|
|
||||||
@Override
|
|
||||||
public void onInvalidated(@NonNull Set<String> tables) {
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
db.getInvalidationTracker().addWeakObserver(mObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count number of rows query can return
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public int countItems() {
|
|
||||||
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
|
|
||||||
mSourceQuery.getArgCount());
|
|
||||||
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
|
|
||||||
Cursor cursor = mDb.query(sqLiteQuery);
|
|
||||||
try {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
return cursor.getInt(0);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
sqLiteQuery.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInvalid() {
|
|
||||||
mDb.getInvalidationTracker().refreshVersionsSync();
|
|
||||||
return super.isInvalid();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
protected abstract List<T> convertRows(Cursor cursor);
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
|
||||||
public void loadInitial(@NonNull LoadInitialParams params,
|
|
||||||
@NonNull LoadInitialCallback<T> callback) {
|
|
||||||
List<T> list = Collections.emptyList();
|
|
||||||
int totalCount = 0;
|
|
||||||
int firstLoadPosition = 0;
|
|
||||||
RoomSQLiteQuery sqLiteQuery = null;
|
|
||||||
Cursor cursor = null;
|
|
||||||
mDb.beginTransaction();
|
|
||||||
try {
|
|
||||||
totalCount = countItems();
|
|
||||||
if (totalCount != 0) {
|
|
||||||
// bound the size requested, based on known count
|
|
||||||
firstLoadPosition = computeInitialLoadPosition(params, totalCount);
|
|
||||||
int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
|
|
||||||
|
|
||||||
sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
|
|
||||||
cursor = mDb.query(sqLiteQuery);
|
|
||||||
List<T> rows = convertRows(cursor);
|
|
||||||
mDb.setTransactionSuccessful();
|
|
||||||
list = rows;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
mDb.endTransaction();
|
|
||||||
if (sqLiteQuery != null) {
|
|
||||||
sqLiteQuery.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onResult(list, firstLoadPosition, totalCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadRange(@NonNull LoadRangeParams params,
|
|
||||||
@NonNull LoadRangeCallback<T> callback) {
|
|
||||||
callback.onResult(loadRange(params.startPosition, params.loadSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the rows from startPos to startPos + loadCount
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@NonNull
|
|
||||||
public List<T> loadRange(int startPosition, int loadCount) {
|
|
||||||
final RoomSQLiteQuery sqLiteQuery = getSQLiteQuery(startPosition, loadCount);
|
|
||||||
if (mInTransaction) {
|
|
||||||
mDb.beginTransaction();
|
|
||||||
Cursor cursor = null;
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private RoomSQLiteQuery getSQLiteQuery(int startPosition, int loadCount) {
|
|
||||||
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
|
|
||||||
mSourceQuery.getArgCount() + 2);
|
|
||||||
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
|
|
||||||
sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
|
|
||||||
sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
|
|
||||||
return sqLiteQuery;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for in-process and multi-process key-based lock mechanism for safely copying
|
|
||||||
* database files.
|
|
||||||
* <p>
|
|
||||||
* Acquiring the lock will be quick if no other thread or process has a lock with the same key.
|
|
||||||
* But if the lock is already held then acquiring it will block, until the other thread or process
|
|
||||||
* releases the lock. Note that the key and lock directory must be the same to achieve
|
|
||||||
* synchronization.
|
|
||||||
* <p>
|
|
||||||
* Locking is done via two levels:
|
|
||||||
* <ol>
|
|
||||||
* <li>
|
|
||||||
* Thread locking within the same JVM process is done via a map of String key to ReentrantLock
|
|
||||||
* objects.
|
|
||||||
* <li>
|
|
||||||
* Multi-process locking is done via a dummy file whose name contains the key and FileLock
|
|
||||||
* objects.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class CopyLock {
|
|
||||||
|
|
||||||
// in-process lock map
|
|
||||||
private static final Map<String, Lock> sThreadLocks = new HashMap<>();
|
|
||||||
|
|
||||||
private final File mCopyLockFile;
|
|
||||||
private final Lock mThreadLock;
|
|
||||||
private final boolean mFileLevelLock;
|
|
||||||
private FileChannel mLockChannel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a lock with {@code name} and using {@code lockDir} as the directory for the
|
|
||||||
* lock files.
|
|
||||||
* @param name the name of this lock.
|
|
||||||
* @param lockDir the directory where the lock files will be located.
|
|
||||||
* @param processLock whether to use file for process level locking or not.
|
|
||||||
*/
|
|
||||||
public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) {
|
|
||||||
mCopyLockFile = new File(lockDir, name + ".lck");
|
|
||||||
mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath());
|
|
||||||
mFileLevelLock = processLock;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to grab the lock, blocking if already held by another thread or process.
|
|
||||||
*/
|
|
||||||
public void lock() {
|
|
||||||
mThreadLock.lock();
|
|
||||||
if (mFileLevelLock) {
|
|
||||||
try {
|
|
||||||
mLockChannel = new FileOutputStream(mCopyLockFile).getChannel();
|
|
||||||
mLockChannel.lock();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException("Unable to grab copy lock.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the lock.
|
|
||||||
*/
|
|
||||||
public void unlock() {
|
|
||||||
if (mLockChannel != null) {
|
|
||||||
try {
|
|
||||||
mLockChannel.close();
|
|
||||||
} catch (IOException ignored) { }
|
|
||||||
}
|
|
||||||
mThreadLock.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Lock getThreadLock(String key) {
|
|
||||||
synchronized (sThreadLocks) {
|
|
||||||
Lock threadLock = sThreadLocks.get(key);
|
|
||||||
if (threadLock == null) {
|
|
||||||
threadLock = new ReentrantLock();
|
|
||||||
sThreadLocks.put(key, threadLock);
|
|
||||||
}
|
|
||||||
return threadLock;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cursor utilities for Room
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class CursorUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the given cursor into a in-memory cursor and then closes it.
|
|
||||||
* <p>
|
|
||||||
* This is useful for iterating over a cursor multiple times without the cost of JNI while
|
|
||||||
* reading or IO while filling the window at the expense of memory consumption.
|
|
||||||
*
|
|
||||||
* @param c the cursor to copy.
|
|
||||||
* @return a new cursor containing the same data as the given cursor.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public static Cursor copyAndClose(@NonNull Cursor c) {
|
|
||||||
final MatrixCursor matrixCursor;
|
|
||||||
try {
|
|
||||||
matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
final Object[] row = new Object[c.getColumnCount()];
|
|
||||||
for (int i = 0; i < c.getColumnCount(); i++) {
|
|
||||||
switch (c.getType(i)) {
|
|
||||||
case Cursor.FIELD_TYPE_NULL:
|
|
||||||
row[i] = null;
|
|
||||||
break;
|
|
||||||
case Cursor.FIELD_TYPE_INTEGER:
|
|
||||||
row[i] = c.getLong(i);
|
|
||||||
break;
|
|
||||||
case Cursor.FIELD_TYPE_FLOAT:
|
|
||||||
row[i] = c.getDouble(i);
|
|
||||||
break;
|
|
||||||
case Cursor.FIELD_TYPE_STRING:
|
|
||||||
row[i] = c.getString(i);
|
|
||||||
break;
|
|
||||||
case Cursor.FIELD_TYPE_BLOB:
|
|
||||||
row[i] = c.getBlob(i);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matrixCursor.addRow(row);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
return matrixCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
|
|
||||||
* If the column is not found, it retries with the specified name surrounded by backticks.
|
|
||||||
*
|
|
||||||
* @param c The cursor.
|
|
||||||
* @param name The name of the target column.
|
|
||||||
* @return The index of the column, or -1 if not found.
|
|
||||||
*/
|
|
||||||
public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
|
|
||||||
final int index = c.getColumnIndex(name);
|
|
||||||
if (index >= 0) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
return c.getColumnIndex("`" + name + "`");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
|
|
||||||
* If the column is not found, it retries with the specified name surrounded by backticks.
|
|
||||||
*
|
|
||||||
* @param c The cursor.
|
|
||||||
* @param name The name of the target column.
|
|
||||||
* @return The index of the column.
|
|
||||||
* @throws IllegalArgumentException if the column does not exist.
|
|
||||||
*/
|
|
||||||
public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
|
|
||||||
final int index = c.getColumnIndex(name);
|
|
||||||
if (index >= 0) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
return c.getColumnIndexOrThrow("`" + name + "`");
|
|
||||||
}
|
|
||||||
|
|
||||||
private CursorUtil() {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 android.database.AbstractWindowedCursor;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.room.RoomDatabase;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database utilities for Room
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class DBUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the SQLiteQuery on the given database.
|
|
||||||
* <p>
|
|
||||||
* This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
|
|
||||||
* {@code true} and either the api level is below a certain threshold or the full result of the
|
|
||||||
* query does not fit in a single window.
|
|
||||||
*
|
|
||||||
* @param db The database to perform the query on.
|
|
||||||
* @param sqLiteQuery The query to perform.
|
|
||||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
|
||||||
* @return Result of the query.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) {
|
|
||||||
final Cursor cursor = db.query(sqLiteQuery);
|
|
||||||
if (maybeCopy && cursor instanceof AbstractWindowedCursor) {
|
|
||||||
AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
|
|
||||||
int rowsInCursor = windowedCursor.getCount(); // Should fill the window.
|
|
||||||
int rowsInWindow;
|
|
||||||
if (windowedCursor.hasWindow()) {
|
|
||||||
rowsInWindow = windowedCursor.getWindow().getNumRows();
|
|
||||||
} else {
|
|
||||||
rowsInWindow = rowsInCursor;
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
|
|
||||||
return CursorUtil.copyAndClose(windowedCursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drops all FTS content sync triggers created by Room.
|
|
||||||
* <p>
|
|
||||||
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
|
|
||||||
* who's names start with 'room_fts_content_sync_'.
|
|
||||||
*
|
|
||||||
* @param db The database.
|
|
||||||
*/
|
|
||||||
public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) {
|
|
||||||
List<String> existingTriggers = new ArrayList<>();
|
|
||||||
Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'");
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
existingTriggers.add(cursor.getString(0));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String triggerName : existingTriggers) {
|
|
||||||
if (triggerName.startsWith("room_fts_content_sync_")) {
|
|
||||||
db.execSQL("DROP TRIGGER IF EXISTS " + triggerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the user version number out of the database header from the given file.
|
|
||||||
*
|
|
||||||
* @param databaseFile the database file.
|
|
||||||
* @return the database version
|
|
||||||
* @throws IOException if something goes wrong reading the file, such as bad database header or
|
|
||||||
* missing permissions.
|
|
||||||
*
|
|
||||||
* @see <a href="https://www.sqlite.org/fileformat.html#user_version_number">User Version
|
|
||||||
* Number</a>.
|
|
||||||
*/
|
|
||||||
public static int readVersion(@NonNull File databaseFile) throws IOException {
|
|
||||||
FileChannel input = null;
|
|
||||||
try {
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
|
||||||
input = new FileInputStream(databaseFile).getChannel();
|
|
||||||
input.tryLock(60, 4, true);
|
|
||||||
input.position(60);
|
|
||||||
int read = input.read(buffer);
|
|
||||||
if (read != 4) {
|
|
||||||
throw new IOException("Bad database header, unable to read 4 bytes at offset 60");
|
|
||||||
}
|
|
||||||
buffer.rewind();
|
|
||||||
return buffer.getInt(); // ByteBuffer is big-endian by default
|
|
||||||
} finally {
|
|
||||||
if (input != null) {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private DBUtil() {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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 android.annotation.SuppressLint;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.channels.Channels;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.nio.channels.ReadableByteChannel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File utilities for Room
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class FileUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies data from the input channel to the output file channel.
|
|
||||||
*
|
|
||||||
* @param input the input channel to copy.
|
|
||||||
* @param output the output channel to copy.
|
|
||||||
* @throws IOException if there is an I/O error.
|
|
||||||
*/
|
|
||||||
@SuppressLint("LambdaLast")
|
|
||||||
public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
|
|
||||||
throws IOException {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
|
||||||
output.transferFrom(input, 0, Long.MAX_VALUE);
|
|
||||||
} else {
|
|
||||||
InputStream inputStream = Channels.newInputStream(input);
|
|
||||||
OutputStream outputStream = Channels.newOutputStream(output);
|
|
||||||
int length;
|
|
||||||
byte[] buffer = new byte[1024 * 4];
|
|
||||||
while ((length = inputStream.read(buffer)) > 0) {
|
|
||||||
outputStream.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.force(false);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
output.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FileUtil() {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,220 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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 android.database.Cursor;
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds the information about an FTS table.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class FtsTableInfo {
|
|
||||||
|
|
||||||
// A set of valid FTS Options
|
|
||||||
private static final String[] FTS_OPTIONS = new String[] {
|
|
||||||
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
|
|
||||||
"order=", "prefix=", "uncompress="};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The table name
|
|
||||||
*/
|
|
||||||
public final String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The column names
|
|
||||||
*/
|
|
||||||
public final Set<String> columns;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The set of options. Each value in the set contains the option in the following format:
|
|
||||||
* <key>=<value>.
|
|
||||||
*/
|
|
||||||
public final Set<String> options;
|
|
||||||
|
|
||||||
public FtsTableInfo(String name, Set<String> columns, Set<String> options) {
|
|
||||||
this.name = name;
|
|
||||||
this.columns = columns;
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FtsTableInfo(String name, Set<String> columns, String createSql) {
|
|
||||||
this.name = name;
|
|
||||||
this.columns = columns;
|
|
||||||
this.options = parseOptions(createSql);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the table information from the given database.
|
|
||||||
*
|
|
||||||
* @param database The database to read the information from.
|
|
||||||
* @param tableName The table name.
|
|
||||||
* @return A FtsTableInfo containing the columns and options for the provided table name.
|
|
||||||
*/
|
|
||||||
public static FtsTableInfo read(SupportSQLiteDatabase database, String tableName) {
|
|
||||||
Set<String> columns = readColumns(database, tableName);
|
|
||||||
Set<String> options = readOptions(database, tableName);
|
|
||||||
|
|
||||||
return new FtsTableInfo(tableName, columns, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("TryFinallyCanBeTryWithResources")
|
|
||||||
private static Set<String> readColumns(SupportSQLiteDatabase database, String tableName) {
|
|
||||||
Cursor cursor = database.query("PRAGMA table_info(`" + tableName + "`)");
|
|
||||||
Set<String> columns = new HashSet<>();
|
|
||||||
try {
|
|
||||||
if (cursor.getColumnCount() > 0) {
|
|
||||||
int nameIndex = cursor.getColumnIndex("name");
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
columns.add(cursor.getString(nameIndex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("TryFinallyCanBeTryWithResources")
|
|
||||||
private static Set<String> readOptions(SupportSQLiteDatabase database, String tableName) {
|
|
||||||
String sql = "";
|
|
||||||
Cursor cursor = database.query(
|
|
||||||
"SELECT * FROM sqlite_master WHERE `name` = '" + tableName + "'");
|
|
||||||
try {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
sql = cursor.getString(cursor.getColumnIndexOrThrow("sql"));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
return parseOptions(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses FTS options from the create statement of an FTS table.
|
|
||||||
*
|
|
||||||
* This method assumes the given create statement is a valid well-formed SQLite statement as
|
|
||||||
* defined in the <a href="https://www.sqlite.org/lang_createvtab.html">CREATE VIRTUAL TABLE
|
|
||||||
* syntax diagram</a>.
|
|
||||||
*
|
|
||||||
* @param createStatement the "CREATE VIRTUAL TABLE" statement.
|
|
||||||
* @return the set of FTS option key and values in the create statement.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
|
||||||
static Set<String> parseOptions(String createStatement) {
|
|
||||||
if (createStatement.isEmpty()) {
|
|
||||||
return new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module arguments are within the parenthesis followed by the module name.
|
|
||||||
String argsString = createStatement.substring(
|
|
||||||
createStatement.indexOf('(') + 1,
|
|
||||||
createStatement.lastIndexOf(')'));
|
|
||||||
|
|
||||||
// Split the module argument string by the comma delimiter, keeping track of quotation so
|
|
||||||
// so that if the delimiter is found within a string literal we don't substring at the wrong
|
|
||||||
// index. SQLite supports four ways of quoting keywords, see:
|
|
||||||
// https://www.sqlite.org/lang_keywords.html
|
|
||||||
List<String> args = new ArrayList<>();
|
|
||||||
ArrayDeque<Character> quoteStack = new ArrayDeque<>();
|
|
||||||
int lastDelimiterIndex = -1;
|
|
||||||
for (int i = 0; i < argsString.length(); i++) {
|
|
||||||
char c = argsString.charAt(i);
|
|
||||||
switch (c) {
|
|
||||||
case '\'':
|
|
||||||
case '"':
|
|
||||||
case '`':
|
|
||||||
if (quoteStack.isEmpty()) {
|
|
||||||
quoteStack.push(c);
|
|
||||||
} else if (quoteStack.peek() == c) {
|
|
||||||
quoteStack.pop();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '[':
|
|
||||||
if (quoteStack.isEmpty()) {
|
|
||||||
quoteStack.push(c);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ']':
|
|
||||||
if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
|
|
||||||
quoteStack.pop();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ',':
|
|
||||||
if (quoteStack.isEmpty()) {
|
|
||||||
args.add(argsString.substring(lastDelimiterIndex + 1, i).trim());
|
|
||||||
lastDelimiterIndex = i;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args.add(argsString.substring(lastDelimiterIndex + 1).trim()); // Add final argument.
|
|
||||||
|
|
||||||
// Match args against valid options, otherwise they are column definitions.
|
|
||||||
HashSet<String> options = new HashSet<>();
|
|
||||||
for (String arg : args) {
|
|
||||||
for (String validOption : FTS_OPTIONS) {
|
|
||||||
if (arg.startsWith(validOption)) {
|
|
||||||
options.add(arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
FtsTableInfo that = (FtsTableInfo) o;
|
|
||||||
|
|
||||||
if (name != null ? !name.equals(that.name) : that.name != null) return false;
|
|
||||||
if (columns != null ? !columns.equals(that.columns) : that.columns != null) return false;
|
|
||||||
return options != null ? options.equals(that.options) : that.options == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = name != null ? name.hashCode() : 0;
|
|
||||||
result = 31 * result + (columns != null ? columns.hashCode() : 0);
|
|
||||||
result = 31 * result + (options != null ? options.hashCode() : 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "FtsTableInfo{"
|
|
||||||
+ "name='" + name + '\''
|
|
||||||
+ ", columns=" + columns
|
|
||||||
+ ", options=" + options
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Java 8 Sneaky Throw technique.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
|
||||||
public class SneakyThrow {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-throws a checked exception as if it was a runtime exception without wrapping it.
|
|
||||||
*
|
|
||||||
* @param e the exception to re-throw.
|
|
||||||
*/
|
|
||||||
public static void reThrow(@NonNull Exception e) {
|
|
||||||
sneakyThrow(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
|
|
||||||
throw (E) e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SneakyThrow() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 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 android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.StringTokenizer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @hide
|
|
||||||
*
|
|
||||||
* String utilities for Room
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* @return A new or recycled StringBuilder
|
|
||||||
*/
|
|
||||||
public static StringBuilder newStringBuilder() {
|
|
||||||
// TODO pool:
|
|
||||||
return new StringBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
|
|
||||||
* by a comma.
|
|
||||||
*
|
|
||||||
* @param builder The StringBuilder for the query
|
|
||||||
* @param count Number of placeholders
|
|
||||||
*/
|
|
||||||
public static void appendPlaceholders(StringBuilder builder, int count) {
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
builder.append("?");
|
|
||||||
if (i < count - 1) {
|
|
||||||
builder.append(",");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Splits a comma separated list of integers to integer list.
|
|
||||||
* <p>
|
|
||||||
* If an input is malformed, it is omitted from the result.
|
|
||||||
*
|
|
||||||
* @param input Comma separated list of integers.
|
|
||||||
* @return A List containing the integers or null if the input is null.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static List<Integer> splitToIntList(@Nullable String input) {
|
|
||||||
if (input == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<Integer> result = new ArrayList<>();
|
|
||||||
StringTokenizer tokenizer = new StringTokenizer(input, ",");
|
|
||||||
while (tokenizer.hasMoreElements()) {
|
|
||||||
final String item = tokenizer.nextToken();
|
|
||||||
try {
|
|
||||||
result.add(Integer.parseInt(item));
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Log.e("ROOM", "Malformed integer list", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Joins the given list of integers into a comma separated list.
|
|
||||||
*
|
|
||||||
* @param input The list of integers.
|
|
||||||
* @return Comma separated string composed of integers in the list. If the list is null, return
|
|
||||||
* value is null.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static String joinIntoString(@Nullable List<Integer> input) {
|
|
||||||
if (input == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int size = input.size();
|
|
||||||
if (size == 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
sb.append(Integer.toString(input.get(i)));
|
|
||||||
if (i < size - 1) {
|
|
||||||
sb.append(",");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private StringUtil() {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,665 +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 androidx.room.util;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds the information about a table.
|
|
||||||
* <p>
|
|
||||||
* It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
|
|
||||||
* <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
|
|
||||||
* documentation for more details.
|
|
||||||
* <p>
|
|
||||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
|
|
||||||
"SimplifiableIfStatement"})
|
|
||||||
// if you change this class, you must change TableInfoWriter.kt
|
|
||||||
public class TableInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifies from where the info object was created.
|
|
||||||
*/
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
@IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
|
|
||||||
@interface CreatedFrom {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier for when the info is created from an unknown source.
|
|
||||||
*/
|
|
||||||
public static final int CREATED_FROM_UNKNOWN = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier for when the info is created from an entity definition, such as generated code
|
|
||||||
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
|
|
||||||
*/
|
|
||||||
public static final int CREATED_FROM_ENTITY = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifier for when the info is created from the database itself, reading information from a
|
|
||||||
* PRAGMA, such as table_info.
|
|
||||||
*/
|
|
||||||
public static final int CREATED_FROM_DATABASE = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The table name.
|
|
||||||
*/
|
|
||||||
public final String name;
|
|
||||||
/**
|
|
||||||
* Unmodifiable map of columns keyed by column name.
|
|
||||||
*/
|
|
||||||
public final Map<String, Column> columns;
|
|
||||||
|
|
||||||
public final Set<ForeignKey> foreignKeys;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sometimes, Index information is not available (older versions). If so, we skip their
|
|
||||||
* verification.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final Set<Index> indices;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys,
|
|
||||||
Set<Index> indices) {
|
|
||||||
this.name = name;
|
|
||||||
this.columns = Collections.unmodifiableMap(columns);
|
|
||||||
this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
|
|
||||||
this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For backward compatibility with dbs created with older versions.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
|
|
||||||
this(name, columns, foreignKeys, Collections.<Index>emptySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
TableInfo tableInfo = (TableInfo) o;
|
|
||||||
|
|
||||||
if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
|
|
||||||
if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
|
|
||||||
: tableInfo.foreignKeys != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (indices == null || tableInfo.indices == null) {
|
|
||||||
// if one us is missing index information, seems like we couldn't acquire the
|
|
||||||
// information so we better skip.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return indices.equals(tableInfo.indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = name != null ? name.hashCode() : 0;
|
|
||||||
result = 31 * result + (columns != null ? columns.hashCode() : 0);
|
|
||||||
result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
|
|
||||||
// skip index, it is not reliable for comparison.
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "TableInfo{"
|
|
||||||
+ "name='" + name + '\''
|
|
||||||
+ ", columns=" + columns
|
|
||||||
+ ", foreignKeys=" + foreignKeys
|
|
||||||
+ ", indices=" + indices
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the table information from the given database.
|
|
||||||
*
|
|
||||||
* @param database The database to read the information from.
|
|
||||||
* @param tableName The table name.
|
|
||||||
* @return A TableInfo containing the schema information for the provided table name.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("SameParameterValue")
|
|
||||||
public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
|
|
||||||
Map<String, Column> columns = readColumns(database, tableName);
|
|
||||||
Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
|
|
||||||
Set<Index> indices = readIndices(database, tableName);
|
|
||||||
return new TableInfo(tableName, columns, foreignKeys, indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
|
|
||||||
String tableName) {
|
|
||||||
Set<ForeignKey> foreignKeys = new HashSet<>();
|
|
||||||
// this seems to return everything in order but it is not documented so better be safe
|
|
||||||
Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
|
|
||||||
try {
|
|
||||||
final int idColumnIndex = cursor.getColumnIndex("id");
|
|
||||||
final int seqColumnIndex = cursor.getColumnIndex("seq");
|
|
||||||
final int tableColumnIndex = cursor.getColumnIndex("table");
|
|
||||||
final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
|
|
||||||
final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
|
|
||||||
|
|
||||||
final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor);
|
|
||||||
final int count = cursor.getCount();
|
|
||||||
for (int position = 0; position < count; position++) {
|
|
||||||
cursor.moveToPosition(position);
|
|
||||||
final int seq = cursor.getInt(seqColumnIndex);
|
|
||||||
if (seq != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final int id = cursor.getInt(idColumnIndex);
|
|
||||||
List<String> myColumns = new ArrayList<>();
|
|
||||||
List<String> refColumns = new ArrayList<>();
|
|
||||||
for (ForeignKeyWithSequence key : ordered) {
|
|
||||||
if (key.mId == id) {
|
|
||||||
myColumns.add(key.mFrom);
|
|
||||||
refColumns.add(key.mTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreignKeys.add(new ForeignKey(
|
|
||||||
cursor.getString(tableColumnIndex),
|
|
||||||
cursor.getString(onDeleteColumnIndex),
|
|
||||||
cursor.getString(onUpdateColumnIndex),
|
|
||||||
myColumns,
|
|
||||||
refColumns
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
return foreignKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) {
|
|
||||||
final int idColumnIndex = cursor.getColumnIndex("id");
|
|
||||||
final int seqColumnIndex = cursor.getColumnIndex("seq");
|
|
||||||
final int fromColumnIndex = cursor.getColumnIndex("from");
|
|
||||||
final int toColumnIndex = cursor.getColumnIndex("to");
|
|
||||||
final int count = cursor.getCount();
|
|
||||||
List<ForeignKeyWithSequence> result = new ArrayList<>();
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
cursor.moveToPosition(i);
|
|
||||||
result.add(new ForeignKeyWithSequence(
|
|
||||||
cursor.getInt(idColumnIndex),
|
|
||||||
cursor.getInt(seqColumnIndex),
|
|
||||||
cursor.getString(fromColumnIndex),
|
|
||||||
cursor.getString(toColumnIndex)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Collections.sort(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
|
|
||||||
String tableName) {
|
|
||||||
Cursor cursor = database
|
|
||||||
.query("PRAGMA table_info(`" + tableName + "`)");
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
Map<String, Column> columns = new HashMap<>();
|
|
||||||
try {
|
|
||||||
if (cursor.getColumnCount() > 0) {
|
|
||||||
int nameIndex = cursor.getColumnIndex("name");
|
|
||||||
int typeIndex = cursor.getColumnIndex("type");
|
|
||||||
int notNullIndex = cursor.getColumnIndex("notnull");
|
|
||||||
int pkIndex = cursor.getColumnIndex("pk");
|
|
||||||
int defaultValueIndex = cursor.getColumnIndex("dflt_value");
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
final String name = cursor.getString(nameIndex);
|
|
||||||
final String type = cursor.getString(typeIndex);
|
|
||||||
final boolean notNull = 0 != cursor.getInt(notNullIndex);
|
|
||||||
final int primaryKeyPosition = cursor.getInt(pkIndex);
|
|
||||||
final String defaultValue = cursor.getString(defaultValueIndex);
|
|
||||||
columns.put(name,
|
|
||||||
new Column(name, type, notNull, primaryKeyPosition, defaultValue,
|
|
||||||
CREATED_FROM_DATABASE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return null if we cannot read the indices due to older sqlite implementations.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) {
|
|
||||||
Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
|
|
||||||
try {
|
|
||||||
final int nameColumnIndex = cursor.getColumnIndex("name");
|
|
||||||
final int originColumnIndex = cursor.getColumnIndex("origin");
|
|
||||||
final int uniqueIndex = cursor.getColumnIndex("unique");
|
|
||||||
if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
|
|
||||||
// we cannot read them so better not validate any index.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
HashSet<Index> indices = new HashSet<>();
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String origin = cursor.getString(originColumnIndex);
|
|
||||||
if (!"c".equals(origin)) {
|
|
||||||
// Ignore auto-created indices
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String name = cursor.getString(nameColumnIndex);
|
|
||||||
boolean unique = cursor.getInt(uniqueIndex) == 1;
|
|
||||||
Index index = readIndex(database, name, unique);
|
|
||||||
if (index == null) {
|
|
||||||
// we cannot read it properly so better not read it
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
indices.add(index);
|
|
||||||
}
|
|
||||||
return indices;
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return null if we cannot read the index due to older sqlite implementations.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
|
|
||||||
Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
// we cannot read them so better not validate any index.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final TreeMap<Integer, String> results = new TreeMap<>();
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
int cid = cursor.getInt(cidColumnIndex);
|
|
||||||
if (cid < 0) {
|
|
||||||
// Ignore SQLite row ID
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int seq = cursor.getInt(seqnoColumnIndex);
|
|
||||||
String columnName = cursor.getString(nameColumnIndex);
|
|
||||||
results.put(seq, columnName);
|
|
||||||
}
|
|
||||||
final List<String> columns = new ArrayList<>(results.size());
|
|
||||||
columns.addAll(results.values());
|
|
||||||
return new Index(name, unique, columns);
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the information about a database column.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public static class Column {
|
|
||||||
/**
|
|
||||||
* The column name.
|
|
||||||
*/
|
|
||||||
public final String name;
|
|
||||||
/**
|
|
||||||
* The column type affinity.
|
|
||||||
*/
|
|
||||||
public final String type;
|
|
||||||
/**
|
|
||||||
* The column type after it is normalized to one of the basic types according to
|
|
||||||
* https://www.sqlite.org/datatype3.html Section 3.1.
|
|
||||||
* <p>
|
|
||||||
* This is the value Room uses for equality check.
|
|
||||||
*/
|
|
||||||
@ColumnInfo.SQLiteTypeAffinity
|
|
||||||
public final int affinity;
|
|
||||||
/**
|
|
||||||
* Whether or not the column can be NULL.
|
|
||||||
*/
|
|
||||||
public final boolean notNull;
|
|
||||||
/**
|
|
||||||
* The position of the column in the list of primary keys, 0 if the column is not part
|
|
||||||
* of the primary key.
|
|
||||||
* <p>
|
|
||||||
* This information is only available in API 20+.
|
|
||||||
* <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
|
|
||||||
* On older platforms, it will be 1 if the column is part of the primary key and 0
|
|
||||||
* otherwise.
|
|
||||||
* <p>
|
|
||||||
* The {@link #equals(Object)} implementation handles this inconsistency based on
|
|
||||||
* API levels os if you are using a custom SQLite deployment, it may return false
|
|
||||||
* positives.
|
|
||||||
*/
|
|
||||||
public final int primaryKeyPosition;
|
|
||||||
/**
|
|
||||||
* The default value of this column.
|
|
||||||
*/
|
|
||||||
public final String defaultValue;
|
|
||||||
|
|
||||||
@CreatedFrom
|
|
||||||
private final int mCreatedFrom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
|
|
||||||
this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if you change this constructor, you must change TableInfoWriter.kt
|
|
||||||
public Column(String name, String type, boolean notNull, int primaryKeyPosition,
|
|
||||||
String defaultValue, @CreatedFrom int createdFrom) {
|
|
||||||
this.name = name;
|
|
||||||
this.type = type;
|
|
||||||
this.notNull = notNull;
|
|
||||||
this.primaryKeyPosition = primaryKeyPosition;
|
|
||||||
this.affinity = findAffinity(type);
|
|
||||||
this.defaultValue = defaultValue;
|
|
||||||
this.mCreatedFrom = createdFrom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements https://www.sqlite.org/datatype3.html section 3.1
|
|
||||||
*
|
|
||||||
* @param type The type that was given to the sqlite
|
|
||||||
* @return The normalized type which is one of the 5 known affinities
|
|
||||||
*/
|
|
||||||
@ColumnInfo.SQLiteTypeAffinity
|
|
||||||
private static int findAffinity(@Nullable String type) {
|
|
||||||
if (type == null) {
|
|
||||||
return ColumnInfo.BLOB;
|
|
||||||
}
|
|
||||||
String uppercaseType = type.toUpperCase(Locale.US);
|
|
||||||
if (uppercaseType.contains("INT")) {
|
|
||||||
return ColumnInfo.INTEGER;
|
|
||||||
}
|
|
||||||
if (uppercaseType.contains("CHAR")
|
|
||||||
|| uppercaseType.contains("CLOB")
|
|
||||||
|| uppercaseType.contains("TEXT")) {
|
|
||||||
return ColumnInfo.TEXT;
|
|
||||||
}
|
|
||||||
if (uppercaseType.contains("BLOB")) {
|
|
||||||
return ColumnInfo.BLOB;
|
|
||||||
}
|
|
||||||
if (uppercaseType.contains("REAL")
|
|
||||||
|| uppercaseType.contains("FLOA")
|
|
||||||
|| uppercaseType.contains("DOUB")) {
|
|
||||||
return ColumnInfo.REAL;
|
|
||||||
}
|
|
||||||
// sqlite returns NUMERIC here but it is like a catch all. We already
|
|
||||||
// have UNDEFINED so it is better to use UNDEFINED for consistency.
|
|
||||||
return ColumnInfo.UNDEFINED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Column column = (Column) o;
|
|
||||||
if (Build.VERSION.SDK_INT >= 20) {
|
|
||||||
if (primaryKeyPosition != column.primaryKeyPosition) return false;
|
|
||||||
} else {
|
|
||||||
if (isPrimaryKey() != column.isPrimaryKey()) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name.equals(column.name)) return false;
|
|
||||||
//noinspection SimplifiableIfStatement
|
|
||||||
if (notNull != column.notNull) return false;
|
|
||||||
|
|
||||||
// Only validate default value if it was defined in an entity, i.e. if the info
|
|
||||||
// from the compiler itself has it. b/136019383
|
|
||||||
if (mCreatedFrom == CREATED_FROM_ENTITY
|
|
||||||
&& column.mCreatedFrom == CREATED_FROM_DATABASE
|
|
||||||
&& (defaultValue != null && !defaultValue.equals(column.defaultValue))) {
|
|
||||||
return false;
|
|
||||||
} else if (mCreatedFrom == CREATED_FROM_DATABASE
|
|
||||||
&& column.mCreatedFrom == CREATED_FROM_ENTITY
|
|
||||||
&& (column.defaultValue != null && !column.defaultValue.equals(defaultValue))) {
|
|
||||||
return false;
|
|
||||||
} else if (mCreatedFrom != CREATED_FROM_UNKNOWN
|
|
||||||
&& mCreatedFrom == column.mCreatedFrom
|
|
||||||
&& (defaultValue != null ? !defaultValue.equals(column.defaultValue)
|
|
||||||
: column.defaultValue != null)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return affinity == column.affinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether this column is part of the primary key or not.
|
|
||||||
*
|
|
||||||
* @return True if this column is part of the primary key, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isPrimaryKey() {
|
|
||||||
return primaryKeyPosition > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = name.hashCode();
|
|
||||||
result = 31 * result + affinity;
|
|
||||||
result = 31 * result + (notNull ? 1231 : 1237);
|
|
||||||
result = 31 * result + primaryKeyPosition;
|
|
||||||
// Default value is not part of the hashcode since we conditionally check it for
|
|
||||||
// equality which would break the equals + hashcode contract.
|
|
||||||
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Column{"
|
|
||||||
+ "name='" + name + '\''
|
|
||||||
+ ", type='" + type + '\''
|
|
||||||
+ ", affinity='" + affinity + '\''
|
|
||||||
+ ", notNull=" + notNull
|
|
||||||
+ ", primaryKeyPosition=" + primaryKeyPosition
|
|
||||||
+ ", defaultValue='" + defaultValue + '\''
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the information about an SQLite foreign key
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public static class ForeignKey {
|
|
||||||
@NonNull
|
|
||||||
public final String referenceTable;
|
|
||||||
@NonNull
|
|
||||||
public final String onDelete;
|
|
||||||
@NonNull
|
|
||||||
public final String onUpdate;
|
|
||||||
@NonNull
|
|
||||||
public final List<String> columnNames;
|
|
||||||
@NonNull
|
|
||||||
public final List<String> referenceColumnNames;
|
|
||||||
|
|
||||||
public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
|
|
||||||
@NonNull String onUpdate,
|
|
||||||
@NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) {
|
|
||||||
this.referenceTable = referenceTable;
|
|
||||||
this.onDelete = onDelete;
|
|
||||||
this.onUpdate = onUpdate;
|
|
||||||
this.columnNames = Collections.unmodifiableList(columnNames);
|
|
||||||
this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
ForeignKey that = (ForeignKey) o;
|
|
||||||
|
|
||||||
if (!referenceTable.equals(that.referenceTable)) return false;
|
|
||||||
if (!onDelete.equals(that.onDelete)) return false;
|
|
||||||
if (!onUpdate.equals(that.onUpdate)) return false;
|
|
||||||
//noinspection SimplifiableIfStatement
|
|
||||||
if (!columnNames.equals(that.columnNames)) return false;
|
|
||||||
return referenceColumnNames.equals(that.referenceColumnNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = referenceTable.hashCode();
|
|
||||||
result = 31 * result + onDelete.hashCode();
|
|
||||||
result = 31 * result + onUpdate.hashCode();
|
|
||||||
result = 31 * result + columnNames.hashCode();
|
|
||||||
result = 31 * result + referenceColumnNames.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ForeignKey{"
|
|
||||||
+ "referenceTable='" + referenceTable + '\''
|
|
||||||
+ ", onDelete='" + onDelete + '\''
|
|
||||||
+ ", onUpdate='" + onUpdate + '\''
|
|
||||||
+ ", columnNames=" + columnNames
|
|
||||||
+ ", referenceColumnNames=" + referenceColumnNames
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporary data holder for a foreign key row in the pragma result. We need this to ensure
|
|
||||||
* sorting in the generated foreign key object.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> {
|
|
||||||
final int mId;
|
|
||||||
final int mSequence;
|
|
||||||
final String mFrom;
|
|
||||||
final String mTo;
|
|
||||||
|
|
||||||
ForeignKeyWithSequence(int id, int sequence, String from, String to) {
|
|
||||||
mId = id;
|
|
||||||
mSequence = sequence;
|
|
||||||
mFrom = from;
|
|
||||||
mTo = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(@NonNull ForeignKeyWithSequence o) {
|
|
||||||
final int idCmp = mId - o.mId;
|
|
||||||
if (idCmp == 0) {
|
|
||||||
return mSequence - o.mSequence;
|
|
||||||
} else {
|
|
||||||
return idCmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the information about an SQLite index
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public static class Index {
|
|
||||||
// should match the value in Index.kt
|
|
||||||
public static final String DEFAULT_PREFIX = "index_";
|
|
||||||
public final String name;
|
|
||||||
public final boolean unique;
|
|
||||||
public final List<String> columns;
|
|
||||||
|
|
||||||
public Index(String name, boolean unique, List<String> columns) {
|
|
||||||
this.name = name;
|
|
||||||
this.unique = unique;
|
|
||||||
this.columns = columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Index index = (Index) o;
|
|
||||||
if (unique != index.unique) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!columns.equals(index.columns)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (name.startsWith(Index.DEFAULT_PREFIX)) {
|
|
||||||
return index.name.startsWith(Index.DEFAULT_PREFIX);
|
|
||||||
} else {
|
|
||||||
return name.equals(index.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result;
|
|
||||||
if (name.startsWith(DEFAULT_PREFIX)) {
|
|
||||||
result = DEFAULT_PREFIX.hashCode();
|
|
||||||
} else {
|
|
||||||
result = name.hashCode();
|
|
||||||
}
|
|
||||||
result = 31 * result + (unique ? 1 : 0);
|
|
||||||
result = 31 * result + columns.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Index{"
|
|
||||||
+ "name='" + name + '\''
|
|
||||||
+ ", unique=" + unique
|
|
||||||
+ ", columns=" + columns
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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 android.database.Cursor;
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo;
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds the information about a view.
|
|
||||||
* <p>
|
|
||||||
* This derives information from sqlite_master.
|
|
||||||
* <p>
|
|
||||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
|
||||||
*
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
public class ViewInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The view name
|
|
||||||
*/
|
|
||||||
public final String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SQL of CREATE VIEW.
|
|
||||||
*/
|
|
||||||
public final String sql;
|
|
||||||
|
|
||||||
public ViewInfo(String name, String sql) {
|
|
||||||
this.name = name;
|
|
||||||
this.sql = sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
ViewInfo viewInfo = (ViewInfo) o;
|
|
||||||
return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null)
|
|
||||||
&& (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = name != null ? name.hashCode() : 0;
|
|
||||||
result = 31 * result + (sql != null ? sql.hashCode() : 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ViewInfo{"
|
|
||||||
+ "name='" + name + '\''
|
|
||||||
+ ", sql='" + sql + '\''
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the view information from the given database.
|
|
||||||
*
|
|
||||||
* @param database The database to read the information from.
|
|
||||||
* @param viewName The view name.
|
|
||||||
* @return A ViewInfo containing the schema information for the provided view name.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("SameParameterValue")
|
|
||||||
public static ViewInfo read(SupportSQLiteDatabase database, String viewName) {
|
|
||||||
Cursor cursor = database.query("SELECT name, sql FROM sqlite_master "
|
|
||||||
+ "WHERE type = 'view' AND name = '" + viewName + "'");
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
return new ViewInfo(cursor.getString(0), cursor.getString(1));
|
|
||||||
} else {
|
|
||||||
return new ViewInfo(viewName, null);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue