mirror of https://github.com/M66B/FairEmail.git
parent
ac11efb0a0
commit
1fcfee1d3f
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
}
|
@ -0,0 +1,294 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,853 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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 {
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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() + ").");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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;
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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() {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
}
|
@ -0,0 +1,665 @@
|
||||
/*
|
||||
* 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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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