Removed ROOM inline compilation

pull/159/head
M66B 5 years ago
parent 86c0638fa0
commit 519e617dc7

@ -40,11 +40,6 @@ android {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/README.md'
@ -137,9 +132,6 @@ repositories {
}
configurations.all {
// Workaround https://issuetracker.google.com/issues/138441698
// Support @69c481c39a17d4e1e44a4eb298bb81c48f226eef
exclude group: "androidx.room", module: "room-runtime"
// Workaround https://issuetracker.google.com/issues/134685570
exclude group: "androidx.lifecycle", module: "lifecycle-livedata"
}
@ -204,9 +196,6 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.room/room-runtime
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-common:$room_version" // because of exclude
// https://mvnrepository.com/artifact/androidx.sqlite/sqlite-framework
implementation "androidx.sqlite:sqlite-framework:2.0.1" // because of exclude
annotationProcessor "androidx.room:room-compiler:$room_version"
// https://mvnrepository.com/artifact/androidx.paging/paging-runtime

@ -1,33 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
/**
* RPC Callbacks for {@link IMultiInstanceInvalidationService}.
*
* @hide
*/
interface IMultiInstanceInvalidationCallback {
/**
* Called when invalidation is detected in another instance of the same database.
*
* @param tables List of invalidated table names
*/
oneway void onInvalidation(in String[] tables);
}

@ -1,61 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.room.IMultiInstanceInvalidationCallback;
/**
* RPC Service that controls interaction about multi-instance invalidation.
*
* @hide
*/
interface IMultiInstanceInvalidationService {
/**
* Registers a new {@link IMultiInstanceInvalidationCallback} as a client of this service.
*
* @param callback The RPC callback.
* @param name The name of the database file as it is passed to {@link RoomDatabase.Builder}.
* @return A new client ID. The client needs to hold on to this ID and pass it to the service
* for subsequent calls.
*/
int registerCallback(IMultiInstanceInvalidationCallback callback, String name);
/**
* Unregisters the specified {@link IMultiInstanceInvalidationCallback} from this service.
* <p>
* Clients might die without explicitly calling this method. In that case, the service should
* handle the clean up.
*
* @param callback The RPC callback.
* @param clientId The client ID returned from {@link #registerCallback}.
*/
void unregisterCallback(IMultiInstanceInvalidationCallback callback, int clientId);
/**
* Broadcasts invalidation of database tables to other clients registered to this service.
* <p>
* The broadcast is delivered to {@link IMultiInstanceInvalidationCallback#onInvalidation} of
* the registered clients. The client calling this method will not receive its own broadcast.
* Clients that are associated with a different database file will not be notified.
*
* @param clientId The client ID returned from {@link #registerCallback}.
* @param tables The names of invalidated tables.
*/
oneway void broadcastInvalidation(int clientId, in String[] tables);
}

@ -1,294 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* Configuration class for a {@link RoomDatabase}.
*/
@SuppressWarnings("WeakerAccess")
public class DatabaseConfiguration {
/**
* The factory to use to access the database.
*/
@NonNull
public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
/**
* The context to use while connecting to the database.
*/
@NonNull
public final Context context;
/**
* The name of the database file or null if it is an in-memory database.
*/
@Nullable
public final String name;
/**
* Collection of available migrations.
*/
@NonNull
public final RoomDatabase.MigrationContainer migrationContainer;
@Nullable
public final List<RoomDatabase.Callback> callbacks;
/**
* Whether Room should throw an exception for queries run on the main thread.
*/
public final boolean allowMainThreadQueries;
/**
* The journal mode for this database.
*/
public final RoomDatabase.JournalMode journalMode;
/**
* The Executor used to execute asynchronous queries.
*/
@NonNull
public final Executor queryExecutor;
/**
* The Executor used to execute asynchronous transactions.
*/
@NonNull
public final Executor transactionExecutor;
/**
* If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
* synchronized with other instances of the same {@link RoomDatabase} file, including those
* in a separate process.
*/
public final boolean multiInstanceInvalidation;
/**
* If true, Room should crash if a migration is missing.
*/
public final boolean requireMigration;
/**
* If true, Room should perform a destructive migration when downgrading without an available
* migration.
*/
public final boolean allowDestructiveMigrationOnDowngrade;
/**
* The collection of schema versions from which migrations aren't required.
*/
private final Set<Integer> mMigrationNotRequiredFrom;
/**
* The assets path to a pre-packaged database to copy from.
*/
@Nullable
public final String copyFromAssetPath;
/**
* The pre-packaged database file to copy from.
*/
@Nullable
public final File copyFromFile;
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param requireMigration True if Room should require a valid migration if version changes,
* instead of recreating the tables.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<androidx.room.RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
boolean requireMigration,
@Nullable Set<Integer> migrationNotRequiredFrom) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
requireMigration, false, migrationNotRequiredFrom, null, null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, null, null);
}
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile) {
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
this.context = context;
this.name = name;
this.migrationContainer = migrationContainer;
this.callbacks = callbacks;
this.allowMainThreadQueries = allowMainThreadQueries;
this.journalMode = journalMode;
this.queryExecutor = queryExecutor;
this.transactionExecutor = transactionExecutor;
this.multiInstanceInvalidation = multiInstanceInvalidation;
this.requireMigration = requireMigration;
this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
this.copyFromAssetPath = copyFromAssetPath;
this.copyFromFile = copyFromFile;
}
/**
* Returns whether a migration is required from the specified version.
*
* @param version The schema version.
* @return True if a valid migration is required, false otherwise.
*
* @deprecated Use {@link #isMigrationRequired(int, int)} which takes
* {@link #allowDestructiveMigrationOnDowngrade} into account.
*/
@Deprecated
public boolean isMigrationRequiredFrom(int version) {
return isMigrationRequired(version, version + 1);
}
/**
* Returns whether a migration is required between two versions.
*
* @param fromVersion The old schema version.
* @param toVersion The new schema version.
* @return True if a valid migration is required, false otherwise.
*/
public boolean isMigrationRequired(int fromVersion, int toVersion) {
// Migrations are not required if its a downgrade AND destructive migration during downgrade
// has been allowed.
final boolean isDowngrade = fromVersion > toVersion;
if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
return false;
}
// Migrations are required between the two versions if we generally require migrations
// AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
// exceptions.
return requireMigration
&& (mMigrationNotRequiredFrom == null
|| !mMigrationNotRequiredFrom.contains(fromVersion));
}
}

@ -1,115 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteStatement;
/**
* Implementations of this class knows how to delete or update a particular entity.
* <p>
* This is an internal library class and all of its implementations are auto-generated.
*
* @param <T> The type parameter of the entity to be deleted
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
/**
* Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
* database.
*
* @param database The database to delete / update the item in.
*/
public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
super(database);
}
/**
* Create the deletion or update query
*
* @return An SQL query that can delete or update instances of T.
*/
@Override
protected abstract String createQuery();
/**
* Binds the entity into the given statement.
*
* @param statement The SQLite statement that prepared for the query returned from
* createQuery.
* @param entity The entity of type T.
*/
protected abstract void bind(SupportSQLiteStatement statement, T entity);
/**
* Deletes or updates the given entities in the database and returns the affected row count.
*
* @param entity The entity to delete or update
* @return The number of affected rows
*/
public final int handle(T entity) {
final SupportSQLiteStatement stmt = acquire();
try {
bind(stmt, entity);
return stmt.executeUpdateDelete();
} finally {
release(stmt);
}
}
/**
* Deletes or updates the given entities in the database and returns the affected row count.
*
* @param entities Entities to delete or update
* @return The number of affected rows
*/
public final int handleMultiple(Iterable<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
int total = 0;
for (T entity : entities) {
bind(stmt, entity);
total += stmt.executeUpdateDelete();
}
return total;
} finally {
release(stmt);
}
}
/**
* Deletes or updates the given entities in the database and returns the affected row count.
*
* @param entities Entities to delete or update
* @return The number of affected rows
*/
public final int handleMultiple(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
int total = 0;
for (T entity : entities) {
bind(stmt, entity);
total += stmt.executeUpdateDelete();
}
return total;
} finally {
release(stmt);
}
}
}

@ -1,251 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Implementations of this class knows how to insert a particular entity.
* <p>
* This is an internal library class and all of its implementations are auto-generated.
*
* @param <T> The type parameter of the entity to be inserted
* @hide
*/
@SuppressWarnings({"WeakerAccess", "unused"})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
/**
* Creates an InsertionAdapter that can insert the entity type T into the given database.
*
* @param database The database to insert into.
*/
public EntityInsertionAdapter(RoomDatabase database) {
super(database);
}
/**
* Binds the entity into the given statement.
*
* @param statement The SQLite statement that prepared for the query returned from
* createInsertQuery.
* @param entity The entity of type T.
*/
protected abstract void bind(SupportSQLiteStatement statement, T entity);
/**
* Inserts the entity into the database.
*
* @param entity The entity to insert
*/
public final void insert(T entity) {
final SupportSQLiteStatement stmt = acquire();
try {
bind(stmt, entity);
stmt.executeInsert();
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database.
*
* @param entities Entities to insert
*/
public final void insert(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
for (T entity : entities) {
bind(stmt, entity);
stmt.executeInsert();
}
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database.
*
* @param entities Entities to insert
*/
public final void insert(Iterable<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
for (T entity : entities) {
bind(stmt, entity);
stmt.executeInsert();
}
} finally {
release(stmt);
}
}
/**
* Inserts the given entity into the database and returns the row id.
*
* @param entity The entity to insert
* @return The SQLite row id or -1 if no row is inserted
*/
public final long insertAndReturnId(T entity) {
final SupportSQLiteStatement stmt = acquire();
try {
bind(stmt, entity);
return stmt.executeInsert();
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final long[] insertAndReturnIdsArray(Collection<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final long[] result = new long[entities.size()];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final long[] insertAndReturnIdsArray(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final long[] result = new long[entities.length];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final Long[] insertAndReturnIdsArrayBox(Collection<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final Long[] result = new Long[entities.size()];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final Long[] result = new Long[entities.length];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final List<Long> insertAndReturnIdsList(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final List<Long> result = new ArrayList<>(entities.length);
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result.add(index, stmt.executeInsert());
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final List<Long> insertAndReturnIdsList(Collection<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final List<Long> result = new ArrayList<>(entities.size());
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result.add(index, stmt.executeInsert());
index++;
}
return result;
} finally {
release(stmt);
}
}
}

@ -1,59 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* A helper class that maintains {@link RoomTrackingLiveData} instances for an
* {@link InvalidationTracker}.
* <p>
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
* developer does not hold onto the returned LiveData.
*/
class InvalidationLiveDataContainer {
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final Set<LiveData> mLiveDataSet = Collections.newSetFromMap(
new IdentityHashMap<LiveData, Boolean>()
);
private final RoomDatabase mDatabase;
InvalidationLiveDataContainer(RoomDatabase database) {
mDatabase = database;
}
<T> LiveData<T> create(String[] tableNames, boolean inTransaction,
Callable<T> computeFunction) {
return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
tableNames);
}
void onActive(LiveData liveData) {
mLiveDataSet.add(liveData);
}
void onInactive(LiveData liveData) {
mLiveDataSet.remove(liveData);
}
}

@ -1,853 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.arch.core.internal.SafeIterableMap;
import androidx.lifecycle.LiveData;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
/**
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
* these tables.
*/
// Some details on how the InvalidationTracker works:
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
// tables.
// * Each update (write operation) on one of the observed tables triggers an update into the
// memory table table, flipping the invalidated flag ON.
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
// It works as an Observer, and notifies other instances of table invalidation.
public class InvalidationTracker {
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
private static final String TABLE_ID_COLUMN_NAME = "table_id";
private static final String INVALIDATED_COLUMN_NAME = "invalidated";
private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
+ "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
+ INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
@VisibleForTesting
static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
+ " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
@VisibleForTesting
static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
@NonNull
@VisibleForTesting
final HashMap<String, Integer> mTableIdLookup;
final String[] mTableNames;
@NonNull
private Map<String, Set<String>> mViewTables;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final RoomDatabase mDatabase;
AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
private volatile boolean mInitialized = false;
@SuppressWarnings("WeakerAccess") /* synthetic access */
volatile SupportSQLiteStatement mCleanupStatement;
private ObservedTableTracker mObservedTableTracker;
private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;
// should be accessed with synchronization only.
@VisibleForTesting
@SuppressLint("RestrictedApi")
final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;
/**
* Used by the generated code.
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public InvalidationTracker(RoomDatabase database, String... tableNames) {
this(database, new HashMap<String, String>(), Collections.<String, Set<String>>emptyMap(),
tableNames);
}
/**
* Used by the generated code.
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
Map<String, Set<String>> viewTables, String... tableNames) {
mDatabase = database;
mObservedTableTracker = new ObservedTableTracker(tableNames.length);
mTableIdLookup = new HashMap<>();
mViewTables = viewTables;
mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
final int size = tableNames.length;
mTableNames = new String[size];
for (int id = 0; id < size; id++) {
final String tableName = tableNames[id].toLowerCase(Locale.US);
mTableIdLookup.put(tableName, id);
String shadowTableName = shadowTablesMap.get(tableNames[id]);
if (shadowTableName != null) {
mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
} else {
mTableNames[id] = tableName;
}
}
// Adjust table id lookup for those tables whose shadow table is another already mapped
// table (e.g. external content fts tables).
for (Map.Entry<String, String> shadowTableEntry : shadowTablesMap.entrySet()) {
String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
if (mTableIdLookup.containsKey(shadowTableName)) {
String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
}
}
}
/**
* Internal method to initialize table tracking.
* <p>
* You should never call this method, it is called by the generated code.
*/
void internalInit(SupportSQLiteDatabase database) {
synchronized (this) {
if (mInitialized) {
Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
return;
}
// These actions are not in a transaction because temp_store is not allowed to be
// performed on a transaction, and recursive_triggers is not affected by transactions.
database.execSQL("PRAGMA temp_store = MEMORY;");
database.execSQL("PRAGMA recursive_triggers='ON';");
database.execSQL(CREATE_TRACKING_TABLE_SQL);
syncTriggers(database);
mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
mInitialized = true;
}
}
void startMultiInstanceInvalidation(Context context, String name) {
mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this,
mDatabase.getQueryExecutor());
}
void stopMultiInstanceInvalidation() {
if (mMultiInstanceInvalidationClient != null) {
mMultiInstanceInvalidationClient.stop();
mMultiInstanceInvalidationClient = null;
}
}
private static void appendTriggerName(StringBuilder builder, String tableName,
String triggerType) {
builder.append("`")
.append("room_table_modification_trigger_")
.append(tableName)
.append("_")
.append(triggerType)
.append("`");
}
private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
final String tableName = mTableNames[tableId];
StringBuilder stringBuilder = new StringBuilder();
for (String trigger : TRIGGERS) {
stringBuilder.setLength(0);
stringBuilder.append("DROP TRIGGER IF EXISTS ");
appendTriggerName(stringBuilder, tableName, trigger);
writableDb.execSQL(stringBuilder.toString());
}
}
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
writableDb.execSQL(
"INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
final String tableName = mTableNames[tableId];
StringBuilder stringBuilder = new StringBuilder();
for (String trigger : TRIGGERS) {
stringBuilder.setLength(0);
stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
appendTriggerName(stringBuilder, tableName, trigger);
stringBuilder.append(" AFTER ")
.append(trigger)
.append(" ON `")
.append(tableName)
.append("` BEGIN UPDATE ")
.append(UPDATE_TABLE_NAME)
.append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
.append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
.append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
.append("; END");
writableDb.execSQL(stringBuilder.toString());
}
}
/**
* Adds the given observer to the observers list and it will be notified if any table it
* observes changes.
* <p>
* Database changes are pulled on another thread so in some race conditions, the observer might
* be invoked for changes that were done before it is added.
* <p>
* If the observer already exists, this is a no-op call.
* <p>
* If one of the tables in the Observer does not exist in the database, this method throws an
* {@link IllegalArgumentException}.
*
* @param observer The observer which listens the database for changes.
*/
@SuppressLint("RestrictedApi")
@WorkerThread
public void addObserver(@NonNull Observer observer) {
final String[] tableNames = resolveViews(observer.mTables);
int[] tableIds = new int[tableNames.length];
final int size = tableNames.length;
for (int i = 0; i < size; i++) {
Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
if (tableId == null) {
throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
}
tableIds[i] = tableId;
}
ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
ObserverWrapper currentObserver;
synchronized (mObserverMap) {
currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
}
if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
syncTriggers();
}
}
private String[] validateAndResolveTableNames(String[] tableNames) {
String[] resolved = resolveViews(tableNames);
for (String tableName : resolved) {
if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
throw new IllegalArgumentException("There is no table with name " + tableName);
}
}
return resolved;
}
/**
* Resolves the list of tables and views into a list of unique tables that are underlying them.
*
* @param names The names of tables or views.
* @return The names of the underlying tables.
*/
private String[] resolveViews(String[] names) {
Set<String> tables = new HashSet<>();
for (String name : names) {
final String lowercase = name.toLowerCase(Locale.US);
if (mViewTables.containsKey(lowercase)) {
tables.addAll(mViewTables.get(lowercase));
} else {
tables.add(name);
}
}
return tables.toArray(new String[tables.size()]);
}
/**
* Adds an observer but keeps a weak reference back to it.
* <p>
* Note that you cannot remove this observer once added. It will be automatically removed
* when the observer is GC'ed.
*
* @param observer The observer to which InvalidationTracker will keep a weak reference.
* @hide
*/
@SuppressWarnings("unused")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public void addWeakObserver(Observer observer) {
addObserver(new WeakObserver(this, observer));
}
/**
* Removes the observer from the observers list.
*
* @param observer The observer to remove.
*/
@SuppressLint("RestrictedApi")
@SuppressWarnings("WeakerAccess")
@WorkerThread
public void removeObserver(@NonNull final Observer observer) {
ObserverWrapper wrapper;
synchronized (mObserverMap) {
wrapper = mObserverMap.remove(observer);
}
if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
syncTriggers();
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean ensureInitialization() {
if (!mDatabase.isOpen()) {
return false;
}
if (!mInitialized) {
// trigger initialization
mDatabase.getOpenHelper().getWritableDatabase();
}
if (!mInitialized) {
Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
return false;
}
return true;
}
@VisibleForTesting
Runnable mRefreshRunnable = new Runnable() {
@Override
public void run() {
final Lock closeLock = mDatabase.getCloseLock();
Set<Integer> invalidatedTableIds = null;
try {
closeLock.lock();
if (!ensureInitialization()) {
return;
}
if (!mPendingRefresh.compareAndSet(true, false)) {
// no pending refresh
return;
}
if (mDatabase.inTransaction()) {
// current thread is in a transaction. when it ends, it will invoke
// refreshRunnable again. mPendingRefresh is left as false on purpose
// so that the last transaction can flip it on again.
return;
}
if (mDatabase.mWriteAheadLoggingEnabled) {
// This transaction has to be on the underlying DB rather than the RoomDatabase
// in order to avoid a recursive loop after endTransaction.
SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
db.beginTransaction();
try {
invalidatedTableIds = checkUpdatedTable();
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} else {
invalidatedTableIds = checkUpdatedTable();
}
} catch (IllegalStateException | SQLiteException exception) {
// may happen if db is closed. just log.
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
exception);
} finally {
closeLock.unlock();
}
if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
synchronized (mObserverMap) {
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
}
}
}
}
private Set<Integer> checkUpdatedTable() {
HashSet<Integer> invalidatedTableIds = new HashSet<>();
Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
//noinspection TryFinallyCanBeTryWithResources
try {
while (cursor.moveToNext()) {
final int tableId = cursor.getInt(0);
invalidatedTableIds.add(tableId);
}
} finally {
cursor.close();
}
if (!invalidatedTableIds.isEmpty()) {
mCleanupStatement.executeUpdateDelete();
}
return invalidatedTableIds;
}
};
/**
* Enqueues a task to refresh the list of updated tables.
* <p>
* This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
* if you have another connection to the database or directly use {@link
* SupportSQLiteDatabase}, you may need to call this manually.
*/
@SuppressWarnings("WeakerAccess")
public void refreshVersionsAsync() {
// TODO we should consider doing this sync instead of async.
if (mPendingRefresh.compareAndSet(false, true)) {
mDatabase.getQueryExecutor().execute(mRefreshRunnable);
}
}
/**
* Check versions for tables, and run observers synchronously if tables have been updated.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@WorkerThread
public void refreshVersionsSync() {
syncTriggers();
mRefreshRunnable.run();
}
/**
* Notifies all the registered {@link Observer}s of table changes.
* <p>
* This can be used for notifying invalidation that cannot be detected by this
* {@link InvalidationTracker}, for example, invalidation from another process.
*
* @param tables The invalidated tables.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void notifyObserversByTableNames(String... tables) {
synchronized (mObserverMap) {
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
if (!entry.getKey().isRemote()) {
entry.getValue().notifyByTableNames(tables);
}
}
}
}
void syncTriggers(SupportSQLiteDatabase database) {
if (database.inTransaction()) {
// we won't run this inside another transaction.
return;
}
try {
// This method runs in a while loop because while changes are synced to db, another
// runnable may be skipped. If we cause it to skip, we need to do its work.
while (true) {
Lock closeLock = mDatabase.getCloseLock();
closeLock.lock();
try {
// there is a potential race condition where another mSyncTriggers runnable
// can start running right after we get the tables list to sync.
final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
if (tablesToSync == null) {
return;
}
final int limit = tablesToSync.length;
database.beginTransaction();
try {
for (int tableId = 0; tableId < limit; tableId++) {
switch (tablesToSync[tableId]) {
case ObservedTableTracker.ADD:
startTrackingTable(database, tableId);
break;
case ObservedTableTracker.REMOVE:
stopTrackingTable(database, tableId);
break;
}
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
mObservedTableTracker.onSyncCompleted();
} finally {
closeLock.unlock();
}
}
} catch (IllegalStateException | SQLiteException exception) {
// may happen if db is closed. just log.
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
exception);
}
}
/**
* Called by RoomDatabase before each beginTransaction call.
* <p>
* It is important that pending trigger changes are applied to the database before any query
* runs. Otherwise, we may miss some changes.
* <p>
* This api should eventually be public.
*/
void syncTriggers() {
if (!mDatabase.isOpen()) {
return;
}
syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
}
/**
* Creates a LiveData that computes the given function once and for every other invalidation
* of the database.
* <p>
* Holds a strong reference to the created LiveData as long as it is active.
*
* @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
*
* @param computeFunction The function that calculates the value
* @param tableNames The list of tables to observe
* @param <T> The return type
* @return A new LiveData that computes the given function when the given list of tables
* invalidates.
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
return createLiveData(tableNames, false, computeFunction);
}
/**
* Creates a LiveData that computes the given function once and for every other invalidation
* of the database.
* <p>
* Holds a strong reference to the created LiveData as long as it is active.
*
* @param tableNames The list of tables to observe
* @param inTransaction True if the computeFunction will be done in a transaction, false
* otherwise.
* @param computeFunction The function that calculates the value
* @param <T> The return type
* @return A new LiveData that computes the given function when the given list of tables
* invalidates.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
Callable<T> computeFunction) {
return mInvalidationLiveDataContainer.create(
validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
}
/**
* Wraps an observer and keeps the table information.
* <p>
* Internally table ids are used which may change from database to database so the table
* related information is kept here rather than in the Observer.
*/
@SuppressWarnings("WeakerAccess")
static class ObserverWrapper {
final int[] mTableIds;
private final String[] mTableNames;
final Observer mObserver;
private final Set<String> mSingleTableSet;
ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
mObserver = observer;
mTableIds = tableIds;
mTableNames = tableNames;
if (tableIds.length == 1) {
HashSet<String> set = new HashSet<>();
set.add(mTableNames[0]);
mSingleTableSet = Collections.unmodifiableSet(set);
} else {
mSingleTableSet = null;
}
}
/**
* Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
* based on the given invalid status set.
*
* @param invalidatedTablesIds The table ids of the tables that are invalidated.
*/
void notifyByTableInvalidStatus(Set<Integer> invalidatedTablesIds) {
Set<String> invalidatedTables = null;
final int size = mTableIds.length;
for (int index = 0; index < size; index++) {
final int tableId = mTableIds[index];
if (invalidatedTablesIds.contains(tableId)) {
if (size == 1) {
// Optimization for a single-table observer
invalidatedTables = mSingleTableSet;
} else {
if (invalidatedTables == null) {
invalidatedTables = new HashSet<>(size);
}
invalidatedTables.add(mTableNames[index]);
}
}
}
if (invalidatedTables != null) {
mObserver.onInvalidated(invalidatedTables);
}
}
/**
* Notifies the underlying {@link #mObserver} if it observes any of the specified
* {@code tables}.
*
* @param tables The invalidated table names.
*/
void notifyByTableNames(String[] tables) {
Set<String> invalidatedTables = null;
if (mTableNames.length == 1) {
for (String table : tables) {
if (table.equalsIgnoreCase(mTableNames[0])) {
// Optimization for a single-table observer
invalidatedTables = mSingleTableSet;
break;
}
}
} else {
HashSet<String> set = new HashSet<>();
for (String table : tables) {
for (String ourTable : mTableNames) {
if (ourTable.equalsIgnoreCase(table)) {
set.add(ourTable);
break;
}
}
}
if (set.size() > 0) {
invalidatedTables = set;
}
}
if (invalidatedTables != null) {
mObserver.onInvalidated(invalidatedTables);
}
}
}
/**
* An observer that can listen for changes in the database.
*/
public abstract static class Observer {
final String[] mTables;
/**
* Observes the given list of tables and views.
*
* @param firstTable The name of the table or view.
* @param rest More names of tables or views.
*/
@SuppressWarnings("unused")
protected Observer(@NonNull String firstTable, String... rest) {
mTables = Arrays.copyOf(rest, rest.length + 1);
mTables[rest.length] = firstTable;
}
/**
* Observes the given list of tables and views.
*
* @param tables The list of tables or views to observe for changes.
*/
public Observer(@NonNull String[] tables) {
// copy tables in case user modifies them afterwards
mTables = Arrays.copyOf(tables, tables.length);
}
/**
* Called when one of the observed tables is invalidated in the database.
*
* @param tables A set of invalidated tables. This is useful when the observer targets
* multiple tables and you want to know which table is invalidated. This will
* be names of underlying tables when you are observing views.
*/
public abstract void onInvalidated(@NonNull Set<String> tables);
boolean isRemote() {
return false;
}
}
/**
* Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
* triggers in the database.
* <p>
* This class is thread safe
*/
static class ObservedTableTracker {
static final int NO_OP = 0; // don't change trigger state for this table
static final int ADD = 1; // add triggers for this table
static final int REMOVE = 2; // remove triggers for this table
// number of observers per table
final long[] mTableObservers;
// trigger state for each table at last sync
// this field is updated when syncAndGet is called.
final boolean[] mTriggerStates;
// when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
final int[] mTriggerStateChanges;
boolean mNeedsSync;
/**
* After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
* returning any non-null value from getTablesToSync.
* This allows us to workaround any multi-threaded state syncing issues.
*/
boolean mPendingSync;
ObservedTableTracker(int tableCount) {
mTableObservers = new long[tableCount];
mTriggerStates = new boolean[tableCount];
mTriggerStateChanges = new int[tableCount];
Arrays.fill(mTableObservers, 0);
Arrays.fill(mTriggerStates, false);
}
/**
* @return true if # of triggers is affected.
*/
boolean onAdded(int... tableIds) {
boolean needTriggerSync = false;
synchronized (this) {
for (int tableId : tableIds) {
final long prevObserverCount = mTableObservers[tableId];
mTableObservers[tableId] = prevObserverCount + 1;
if (prevObserverCount == 0) {
mNeedsSync = true;
needTriggerSync = true;
}
}
}
return needTriggerSync;
}
/**
* @return true if # of triggers is affected.
*/
boolean onRemoved(int... tableIds) {
boolean needTriggerSync = false;
synchronized (this) {
for (int tableId : tableIds) {
final long prevObserverCount = mTableObservers[tableId];
mTableObservers[tableId] = prevObserverCount - 1;
if (prevObserverCount == 1) {
mNeedsSync = true;
needTriggerSync = true;
}
}
}
return needTriggerSync;
}
/**
* If this returns non-null, you must call onSyncCompleted.
*
* @return int[] An int array where the index for each tableId has the action for that
* table.
*/
@Nullable
int[] getTablesToSync() {
synchronized (this) {
if (!mNeedsSync || mPendingSync) {
return null;
}
final int tableCount = mTableObservers.length;
for (int i = 0; i < tableCount; i++) {
final boolean newState = mTableObservers[i] > 0;
if (newState != mTriggerStates[i]) {
mTriggerStateChanges[i] = newState ? ADD : REMOVE;
} else {
mTriggerStateChanges[i] = NO_OP;
}
mTriggerStates[i] = newState;
}
mPendingSync = true;
mNeedsSync = false;
return mTriggerStateChanges;
}
}
/**
* if getTablesToSync returned non-null, the called should call onSyncCompleted once it
* is done.
*/
void onSyncCompleted() {
synchronized (this) {
mPendingSync = false;
}
}
}
/**
* An Observer wrapper that keeps a weak reference to the given object.
* <p>
* This class will automatically unsubscribe when the wrapped observer goes out of memory.
*/
static class WeakObserver extends Observer {
final InvalidationTracker mTracker;
final WeakReference<Observer> mDelegateRef;
WeakObserver(InvalidationTracker tracker, Observer delegate) {
super(delegate.mTables);
mTracker = tracker;
mDelegateRef = new WeakReference<>(delegate);
}
@Override
public void onInvalidated(@NonNull Set<String> tables) {
final Observer observer = mDelegateRef.get();
if (observer == null) {
mTracker.removeObserver(this);
} else {
observer.onInvalidated(tables);
}
}
}
}

@ -1,200 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
* {@link MultiInstanceInvalidationService}.
*/
class MultiInstanceInvalidationClient {
/**
* The application context.
*/
// synthetic access
@SuppressWarnings("WeakerAccess")
final Context mAppContext;
/**
* The name of the database file.
*/
// synthetic access
@SuppressWarnings("WeakerAccess")
final String mName;
/**
* The client ID assigned by {@link MultiInstanceInvalidationService}.
*/
// synthetic access
@SuppressWarnings("WeakerAccess")
int mClientId;
// synthetic access
@SuppressWarnings("WeakerAccess")
final InvalidationTracker mInvalidationTracker;
// synthetic access
@SuppressWarnings("WeakerAccess")
final InvalidationTracker.Observer mObserver;
// synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
IMultiInstanceInvalidationService mService;
// synthetic access
@SuppressWarnings("WeakerAccess")
final Executor mExecutor;
// synthetic access
@SuppressWarnings("WeakerAccess")
final IMultiInstanceInvalidationCallback mCallback =
new IMultiInstanceInvalidationCallback.Stub() {
@Override
public void onInvalidation(final String[] tables) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mInvalidationTracker.notifyObserversByTableNames(tables);
}
});
}
};
// synthetic access
@SuppressWarnings("WeakerAccess")
final AtomicBoolean mStopped = new AtomicBoolean(false);
// synthetic access
@SuppressWarnings("WeakerAccess")
final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
mExecutor.execute(mSetUpRunnable);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mExecutor.execute(mRemoveObserverRunnable);
mService = null;
}
};
// synthetic access
@SuppressWarnings("WeakerAccess")
final Runnable mSetUpRunnable = new Runnable() {
@Override
public void run() {
try {
final IMultiInstanceInvalidationService service = mService;
if (service != null) {
mClientId = service.registerCallback(mCallback, mName);
mInvalidationTracker.addObserver(mObserver);
}
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
}
}
};
// synthetic access
@SuppressWarnings("WeakerAccess")
final Runnable mRemoveObserverRunnable = new Runnable() {
@Override
public void run() {
mInvalidationTracker.removeObserver(mObserver);
}
};
private final Runnable mTearDownRunnable = new Runnable() {
@Override
public void run() {
mInvalidationTracker.removeObserver(mObserver);
try {
final IMultiInstanceInvalidationService service = mService;
if (service != null) {
service.unregisterCallback(mCallback, mClientId);
}
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
}
mAppContext.unbindService(mServiceConnection);
}
};
/**
* @param context The Context to be used for binding
* {@link IMultiInstanceInvalidationService}.
* @param name The name of the database file.
* @param invalidationTracker The {@link InvalidationTracker}
* @param executor The background executor.
*/
MultiInstanceInvalidationClient(Context context, String name,
InvalidationTracker invalidationTracker, Executor executor) {
mAppContext = context.getApplicationContext();
mName = name;
mInvalidationTracker = invalidationTracker;
mExecutor = executor;
mObserver = new InvalidationTracker.Observer(invalidationTracker.mTableNames) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
if (mStopped.get()) {
return;
}
try {
final IMultiInstanceInvalidationService service = mService;
if (service != null) {
service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
}
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
}
}
@Override
boolean isRemote() {
return true;
}
};
Intent intent = new Intent(mAppContext, MultiInstanceInvalidationService.class);
mAppContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
void stop() {
if (mStopped.compareAndSet(false, true)) {
mExecutor.execute(mTearDownRunnable);
}
}
}

@ -1,134 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.HashMap;
/**
* A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
* This service runs in the main app process. All the instances of {@link InvalidationTracker}
* (potentially in other processes) has to connect to this service.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class MultiInstanceInvalidationService extends Service {
// synthetic access
@SuppressWarnings("WeakerAccess")
int mMaxClientId = 0;
// synthetic access
@SuppressWarnings("WeakerAccess")
final HashMap<Integer, String> mClientNames = new HashMap<>();
// synthetic access
@SuppressWarnings("WeakerAccess")
final RemoteCallbackList<IMultiInstanceInvalidationCallback> mCallbackList =
new RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
@Override
public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
Object cookie) {
mClientNames.remove((int) cookie);
}
};
private final IMultiInstanceInvalidationService.Stub mBinder =
new IMultiInstanceInvalidationService.Stub() {
// Assigns a client ID to the client.
@Override
public int registerCallback(IMultiInstanceInvalidationCallback callback,
String name) {
if (name == null) {
return 0;
}
synchronized (mCallbackList) {
int clientId = ++mMaxClientId;
// Use the client ID as the RemoteCallbackList cookie.
if (mCallbackList.register(callback, clientId)) {
mClientNames.put(clientId, name);
return clientId;
} else {
--mMaxClientId;
return 0;
}
}
}
// Explicitly removes the client.
// The client can die without calling this. In that case, mCallbackList
// .onCallbackDied() can take care of removal.
@Override
public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
int clientId) {
synchronized (mCallbackList) {
mCallbackList.unregister(callback);
mClientNames.remove(clientId);
}
}
// Broadcasts table invalidation to other instances of the same database file.
// The broadcast is not sent to the caller itself.
@Override
public void broadcastInvalidation(int clientId, String[] tables) {
synchronized (mCallbackList) {
String name = mClientNames.get(clientId);
if (name == null) {
Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
return;
}
int count = mCallbackList.beginBroadcast();
try {
for (int i = 0; i < count; i++) {
int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
String targetName = mClientNames.get(targetClientId);
if (clientId == targetClientId // This is the caller itself.
|| !name.equals(targetName)) { // Not the same file.
continue;
}
try {
IMultiInstanceInvalidationCallback callback =
mCallbackList.getBroadcastItem(i);
callback.onInvalidation(tables);
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
}
}
} finally {
mCallbackList.finishBroadcast();
}
}
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

@ -1,109 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.Context;
import androidx.annotation.NonNull;
/**
* Utility class for Room.
*/
@SuppressWarnings("unused")
public class Room {
static final String LOG_TAG = "ROOM";
/**
* The master table where room keeps its metadata information.
*/
public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
/**
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
* should keep a reference to it and re-use it.
*
* @param context The context for the database. This is usually the Application context.
* @param klass The abstract class which is annotated with {@link Database} and extends
* {@link RoomDatabase}.
* @param name The name of the database file.
* @param <T> The type of the database class.
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
*/
@SuppressWarnings("WeakerAccess")
@NonNull
public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder(
@NonNull Context context, @NonNull Class<T> klass, @NonNull String name) {
//noinspection ConstantConditions
if (name == null || name.trim().length() == 0) {
throw new IllegalArgumentException("Cannot build a database with null or empty name."
+ " If you are trying to create an in memory database, use Room"
+ ".inMemoryDatabaseBuilder");
}
return new RoomDatabase.Builder<>(context, klass, name);
}
/**
* Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
* database disappears when the process is killed.
* Once a database is built, you should keep a reference to it and re-use it.
*
* @param context The context for the database. This is usually the Application context.
* @param klass The abstract class which is annotated with {@link Database} and extends
* {@link RoomDatabase}.
* @param <T> The type of the database class.
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
*/
@NonNull
public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder(
@NonNull Context context, @NonNull Class<T> klass) {
return new RoomDatabase.Builder<>(context, klass, null);
}
@SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
@NonNull
static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {
final String fullPackage = klass.getPackage().getName();
String name = klass.getCanonicalName();
final String postPackageName = fullPackage.isEmpty()
? name
: (name.substring(fullPackage.length() + 1));
final String implName = postPackageName.replace('.', '_') + suffix;
//noinspection TryWithIdenticalCatches
try {
@SuppressWarnings("unchecked")
final Class<T> aClass = (Class<T>) Class.forName(
fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
return aClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot find implementation for "
+ klass.getCanonicalName() + ". " + implName + " does not exist");
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access the constructor"
+ klass.getCanonicalName());
} catch (InstantiationException e) {
throw new RuntimeException("Failed to create an instance of "
+ klass.getCanonicalName());
}
}
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
@SuppressWarnings("PrivateConstructorForUtilityClass")
public Room() {
}
}

File diff suppressed because it is too large Load Diff

@ -1,277 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.util.List;
/**
* An open helper that holds a reference to the configuration until the database is opened.
*
* @hide
*/
@SuppressWarnings("unused")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
@Nullable
private DatabaseConfiguration mConfiguration;
@NonNull
private final Delegate mDelegate;
@NonNull
private final String mIdentityHash;
/**
* Room v1 had a bug where the hash was not consistent if fields are reordered.
* The new has fixes it but we still need to accept the legacy hash.
*/
@NonNull // b/64290754
private final String mLegacyHash;
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
@NonNull String identityHash, @NonNull String legacyHash) {
super(delegate.version);
mConfiguration = configuration;
mDelegate = delegate;
mIdentityHash = identityHash;
mLegacyHash = legacyHash;
}
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
@NonNull String legacyHash) {
this(configuration, delegate, "", legacyHash);
}
@Override
public void onConfigure(SupportSQLiteDatabase db) {
super.onConfigure(db);
}
@Override
public void onCreate(SupportSQLiteDatabase db) {
boolean isEmptyDatabase = hasEmptySchema(db);
mDelegate.createAllTables(db);
if (!isEmptyDatabase) {
// A 0 version pre-populated database goes through the create path because the
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
// find the database not to be empty, then it is a pre-populated, we must validate it to
// see if its suitable for usage.
ValidationResult result = mDelegate.onValidateSchema(db);
if (!result.isValid) {
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
+ result.expectedFoundMsg);
}
}
updateIdentity(db);
mDelegate.onCreate(db);
}
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
boolean migrated = false;
if (mConfiguration != null) {
List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
oldVersion, newVersion);
if (migrations != null) {
mDelegate.onPreMigrate(db);
for (Migration migration : migrations) {
migration.migrate(db);
}
ValidationResult result = mDelegate.onValidateSchema(db);
if (!result.isValid) {
throw new IllegalStateException("Migration didn't properly handle: "
+ result.expectedFoundMsg);
}
mDelegate.onPostMigrate(db);
updateIdentity(db);
migrated = true;
}
}
if (!migrated) {
if (mConfiguration != null
&& !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
mDelegate.dropAllTables(db);
mDelegate.createAllTables(db);
} else {
throw new IllegalStateException("A migration from " + oldVersion + " to "
+ newVersion + " was required but not found. Please provide the "
+ "necessary Migration path via "
+ "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ "destructive migrations via one of the "
+ "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
}
}
}
@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onOpen(SupportSQLiteDatabase db) {
super.onOpen(db);
checkIdentity(db);
mDelegate.onOpen(db);
// there might be too many configurations etc, just clear it.
mConfiguration = null;
}
private void checkIdentity(SupportSQLiteDatabase db) {
if (hasRoomMasterTable(db)) {
String identityHash = null;
Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
//noinspection TryFinallyCanBeTryWithResources
try {
if (cursor.moveToFirst()) {
identityHash = cursor.getString(0);
}
} finally {
cursor.close();
}
if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
}
} else {
// No room_master_table, this might an a pre-populated DB, we must validate to see if
// its suitable for usage.
ValidationResult result = mDelegate.onValidateSchema(db);
if (!result.isValid) {
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
+ result.expectedFoundMsg);
}
mDelegate.onPostMigrate(db);
updateIdentity(db);
}
}
private void updateIdentity(SupportSQLiteDatabase db) {
createMasterTableIfNotExists(db);
db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
}
private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
db.execSQL(RoomMasterTable.CREATE_QUERY);
}
private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
+ RoomMasterTable.TABLE_NAME + "'");
//noinspection TryFinallyCanBeTryWithResources
try {
return cursor.moveToFirst() && cursor.getInt(0) != 0;
} finally {
cursor.close();
}
}
private static boolean hasEmptySchema(SupportSQLiteDatabase db) {
Cursor cursor = db.query(
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'");
//noinspection TryFinallyCanBeTryWithResources
try {
return cursor.moveToFirst() && cursor.getInt(0) == 0;
} finally {
cursor.close();
}
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract static class Delegate {
public final int version;
public Delegate(int version) {
this.version = version;
}
protected abstract void dropAllTables(SupportSQLiteDatabase database);
protected abstract void createAllTables(SupportSQLiteDatabase database);
protected abstract void onOpen(SupportSQLiteDatabase database);
protected abstract void onCreate(SupportSQLiteDatabase database);
/**
* Called after a migration run to validate database integrity.
*
* @param db The SQLite database.
*
* @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)}
*/
@Deprecated
protected void validateMigration(SupportSQLiteDatabase db) {
throw new UnsupportedOperationException("validateMigration is deprecated");
}
/**
* Called after a migration run or pre-package database copy to validate database integrity.
*
* @param db The SQLite database.
*/
@SuppressWarnings("deprecation")
@NonNull
protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) {
validateMigration(db);
return new ValidationResult(true, null);
}
/**
* Called before migrations execute to perform preliminary work.
* @param database The SQLite database.
*/
protected void onPreMigrate(SupportSQLiteDatabase database) {
}
/**
* Called after migrations execute to perform additional work.
* @param database The SQLite database.
*/
protected void onPostMigrate(SupportSQLiteDatabase database) {
}
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class ValidationResult {
public final boolean isValid;
@Nullable
public final String expectedFoundMsg;
public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) {
this.isValid = isValid;
this.expectedFoundMsg = expectedFoundMsg;
}
}
}

@ -1,299 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.sqlite.db.SupportSQLiteProgram;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
/**
* This class is used as an intermediate place to keep binding arguments so that we can run
* Cursor queries with correct types rather than passing everything as a string.
* <p>
* Because it is relatively a big object, they are pooled and must be released after each use.
*
* @hide
*/
@SuppressWarnings("unused")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
// Maximum number of queries we'll keep cached.
static final int POOL_LIMIT = 15;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
// clear the bigger queries (# of arguments).
static final int DESIRED_POOL_SIZE = 10;
private volatile String mQuery;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final long[] mLongBindings;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final double[] mDoubleBindings;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final String[] mStringBindings;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final byte[][] mBlobBindings;
@Binding
private final int[] mBindingTypes;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final int mCapacity;
// number of arguments in the query
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
int mArgCount;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
/**
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
*
* @param supportSQLiteQuery The query to copy from
* @return A new query copied from the provided one.
*/
public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
supportSQLiteQuery.getSql(),
supportSQLiteQuery.getArgCount());
supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
@Override
public void bindNull(int index) {
query.bindNull(index);
}
@Override
public void bindLong(int index, long value) {
query.bindLong(index, value);
}
@Override
public void bindDouble(int index, double value) {
query.bindDouble(index, value);
}
@Override
public void bindString(int index, String value) {
query.bindString(index, value);
}
@Override
public void bindBlob(int index, byte[] value) {
query.bindBlob(index, value);
}
@Override
public void clearBindings() {
query.clearBindings();
}
@Override
public void close() {
// ignored.
}
});
return query;
}
/**
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
* given query.
*
* @param query The query to prepare
* @param argumentCount The number of query arguments
* @return A RoomSQLiteQuery that holds the given query and has space for the given number of
* arguments.
*/
@SuppressWarnings("WeakerAccess")
public static RoomSQLiteQuery acquire(String query, int argumentCount) {
synchronized (sQueryPool) {
final Map.Entry<Integer, RoomSQLiteQuery> entry =
sQueryPool.ceilingEntry(argumentCount);
if (entry != null) {
sQueryPool.remove(entry.getKey());
final RoomSQLiteQuery sqliteQuery = entry.getValue();
sqliteQuery.init(query, argumentCount);
return sqliteQuery;
}
}
RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
sqLiteQuery.init(query, argumentCount);
return sqLiteQuery;
}
private RoomSQLiteQuery(int capacity) {
mCapacity = capacity;
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
int limit = capacity + 1;
//noinspection WrongConstant
mBindingTypes = new int[limit];
mLongBindings = new long[limit];
mDoubleBindings = new double[limit];
mStringBindings = new String[limit];
mBlobBindings = new byte[limit][];
}
@SuppressWarnings("WeakerAccess")
void init(String query, int argCount) {
mQuery = query;
mArgCount = argCount;
}
/**
* Releases the query back to the pool.
* <p>
* After released, the statement might be returned when {@link #acquire(String, int)} is called
* so you should never re-use it after releasing.
*/
@SuppressWarnings("WeakerAccess")
public void release() {
synchronized (sQueryPool) {
sQueryPool.put(mCapacity, this);
prunePoolLocked();
}
}
private static void prunePoolLocked() {
if (sQueryPool.size() > POOL_LIMIT) {
int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
while (toBeRemoved-- > 0) {
iterator.next();
iterator.remove();
}
}
}
@Override
public String getSql() {
return mQuery;
}
@Override
public int getArgCount() {
return mArgCount;
}
@Override
public void bindTo(SupportSQLiteProgram program) {
for (int index = 1; index <= mArgCount; index++) {
switch (mBindingTypes[index]) {
case NULL:
program.bindNull(index);
break;
case LONG:
program.bindLong(index, mLongBindings[index]);
break;
case DOUBLE:
program.bindDouble(index, mDoubleBindings[index]);
break;
case STRING:
program.bindString(index, mStringBindings[index]);
break;
case BLOB:
program.bindBlob(index, mBlobBindings[index]);
break;
}
}
}
@Override
public void bindNull(int index) {
mBindingTypes[index] = NULL;
}
@Override
public void bindLong(int index, long value) {
mBindingTypes[index] = LONG;
mLongBindings[index] = value;
}
@Override
public void bindDouble(int index, double value) {
mBindingTypes[index] = DOUBLE;
mDoubleBindings[index] = value;
}
@Override
public void bindString(int index, String value) {
mBindingTypes[index] = STRING;
mStringBindings[index] = value;
}
@Override
public void bindBlob(int index, byte[] value) {
mBindingTypes[index] = BLOB;
mBlobBindings[index] = value;
}
@Override
public void close() {
// no-op. not calling release because it is internal API.
}
/**
* Copies arguments from another RoomSQLiteQuery into this query.
*
* @param other The other query, which holds the arguments to be copied.
*/
public void copyArgumentsFrom(RoomSQLiteQuery other) {
int argCount = other.getArgCount() + 1; // +1 for the binding offsets
System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
}
@Override
public void clearBindings() {
Arrays.fill(mBindingTypes, NULL);
Arrays.fill(mStringBindings, null);
Arrays.fill(mBlobBindings, null);
mQuery = null;
// no need to clear others
}
private static final int NULL = 1;
private static final int LONG = 2;
private static final int DOUBLE = 3;
private static final int STRING = 4;
private static final int BLOB = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
@interface Binding {
}
}

@ -1,169 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.annotation.SuppressLint;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.lifecycle.LiveData;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A LiveData implementation that closely works with {@link InvalidationTracker} to implement
* database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
* as they are active.
* <p>
* We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
* observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
* memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
* {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
* extra query on every UI rotation.
* <p>
* This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
* strongly by the {@link InvalidationTracker} as long as it is active.
*/
class RoomTrackingLiveData<T> extends LiveData<T> {
@SuppressWarnings("WeakerAccess")
final RoomDatabase mDatabase;
@SuppressWarnings("WeakerAccess")
final boolean mInTransaction;
@SuppressWarnings("WeakerAccess")
final Callable<T> mComputeFunction;
private final InvalidationLiveDataContainer mContainer;
@SuppressWarnings("WeakerAccess")
final InvalidationTracker.Observer mObserver;
@SuppressWarnings("WeakerAccess")
final AtomicBoolean mInvalid = new AtomicBoolean(true);
@SuppressWarnings("WeakerAccess")
final AtomicBoolean mComputing = new AtomicBoolean(false);
@SuppressWarnings("WeakerAccess")
final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
@SuppressWarnings("WeakerAccess")
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
if (mRegisteredObserver.compareAndSet(false, true)) {
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
}
boolean computed;
do {
computed = false;
// compute can happen only in 1 thread but no reason to lock others.
if (mComputing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
try {
value = mComputeFunction.call();
} catch (Exception e) {
eu.faircode.email.Log.w(e);
//throw new RuntimeException("Exception while computing database"
// + " live data.", e);
computed = false;
}
}
if (computed) {
postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
// check invalid after releasing compute lock to avoid the following scenario.
// Thread A runs compute()
// Thread A checks invalid, it is false
// Main thread sets invalid to true
// Thread B runs, fails to acquire compute lock and skips
// Thread A releases compute lock
// We've left invalid in set state. The check below recovers.
} while (computed && mInvalid.get());
}
};
@SuppressWarnings("WeakerAccess")
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
getQueryExecutor().execute(mRefreshRunnable);
}
}
}
};
@SuppressLint("RestrictedApi")
RoomTrackingLiveData(
RoomDatabase database,
InvalidationLiveDataContainer container,
boolean inTransaction,
Callable<T> computeFunction,
String[] tableNames) {
mDatabase = database;
mInTransaction = inTransaction;
mComputeFunction = computeFunction;
mContainer = container;
mObserver = new InvalidationTracker.Observer(tableNames) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
};
}
@Override
protected void onActive() {
super.onActive();
mContainer.onActive(this);
getQueryExecutor().execute(mRefreshRunnable);
}
@Override
protected void onInactive() {
super.onInactive();
mContainer.onInactive(this);
}
Executor getQueryExecutor() {
if (mInTransaction) {
return mDatabase.getTransactionExecutor();
} else {
return mDatabase.getQueryExecutor();
}
}
}

@ -1,205 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.room.util.CopyLock;
import androidx.room.util.DBUtil;
import androidx.room.util.FileUtil;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
/**
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
* storage.
*/
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
@NonNull
private final Context mContext;
@Nullable
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
private final int mDatabaseVersion;
@NonNull
private final SupportSQLiteOpenHelper mDelegate;
@Nullable
private DatabaseConfiguration mDatabaseConfiguration;
private boolean mVerified;
SQLiteCopyOpenHelper(
@NonNull Context context,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
int databaseVersion,
@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
mContext = context;
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
mDatabaseVersion = databaseVersion;
mDelegate = supportSQLiteOpenHelper;
}
@Override
public String getDatabaseName() {
return mDelegate.getDatabaseName();
}
@Override
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegate.setWriteAheadLoggingEnabled(enabled);
}
@Override
public synchronized SupportSQLiteDatabase getWritableDatabase() {
if (!mVerified) {
verifyDatabaseFile();
mVerified = true;
}
return mDelegate.getWritableDatabase();
}
@Override
public synchronized SupportSQLiteDatabase getReadableDatabase() {
if (!mVerified) {
verifyDatabaseFile();
mVerified = true;
}
return mDelegate.getReadableDatabase();
}
@Override
public synchronized void close() {
mDelegate.close();
mVerified = false;
}
// Can't be constructor param because the factory is needed by the database builder which in
// turn is the one that actually builds the configuration.
void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) {
mDatabaseConfiguration = databaseConfiguration;
}
private void verifyDatabaseFile() {
String databaseName = getDatabaseName();
File databaseFile = mContext.getDatabasePath(databaseName);
boolean processLevelLock = mDatabaseConfiguration == null
|| mDatabaseConfiguration.multiInstanceInvalidation;
CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock);
try {
// Acquire a copy lock, this lock works across threads and processes, preventing
// concurrent copy attempts from occurring.
copyLock.lock();
if (!databaseFile.exists()) {
try {
// No database file found, copy and be done.
copyDatabaseFile(databaseFile);
return;
} catch (IOException e) {
throw new RuntimeException("Unable to copy database file.", e);
}
}
if (mDatabaseConfiguration == null) {
return;
}
// A database file is present, check if we need to re-copy it.
int currentVersion;
try {
currentVersion = DBUtil.readVersion(databaseFile);
} catch (IOException e) {
Log.w(Room.LOG_TAG, "Unable to read database version.", e);
return;
}
if (currentVersion == mDatabaseVersion) {
return;
}
if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) {
// From the current version to the desired version a migration is required, i.e.
// we won't be performing a copy destructive migration.
return;
}
if (mContext.deleteDatabase(databaseName)) {
try {
copyDatabaseFile(databaseFile);
} catch (IOException e) {
// We are more forgiving copying a database on a destructive migration since
// there is already a database file that can be opened.
Log.w(Room.LOG_TAG, "Unable to copy database file.", e);
}
} else {
Log.w(Room.LOG_TAG, "Failed to delete database file ("
+ databaseName + ") for a copy destructive migration.");
}
} finally {
copyLock.unlock();
}
}
private void copyDatabaseFile(File destinationFile) throws IOException {
ReadableByteChannel input;
if (mCopyFromAssetPath != null) {
input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
} else if (mCopyFromFile != null) {
input = new FileInputStream(mCopyFromFile).getChannel();
} else {
throw new IllegalStateException("copyFromAssetPath and copyFromFile == null!");
}
// An intermediate file is used so that we never end up with a half-copied database file
// in the internal directory.
File intermediateFile = File.createTempFile(
"room-copy-helper", ".tmp", mContext.getCacheDir());
intermediateFile.deleteOnExit();
FileChannel output = new FileOutputStream(intermediateFile).getChannel();
FileUtil.copy(input, output);
File parent = destinationFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("Failed to create directories for "
+ destinationFile.getAbsolutePath());
}
if (!intermediateFile.renameTo(destinationFile)) {
throw new IOException("Failed to move intermediate file ("
+ intermediateFile.getAbsolutePath() + ") to destination ("
+ destinationFile.getAbsolutePath() + ").");
}
}
}

@ -1,56 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
/**
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
* {@link SQLiteCopyOpenHelper}.
*/
class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@Nullable
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
@NonNull
private final SupportSQLiteOpenHelper.Factory mDelegate;
SQLiteCopyOpenHelperFactory(
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@NonNull SupportSQLiteOpenHelper.Factory factory) {
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
mDelegate = factory;
}
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
return new SQLiteCopyOpenHelper(
configuration.context,
mCopyFromAssetPath,
mCopyFromFile,
configuration.callback.version,
mDelegate.create(configuration));
}
}

@ -1,100 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Represents a prepared SQLite state that can be re-used multiple times.
* <p>
* This class is used by generated code. After it is used, {@code release} must be called so that
* it can be used by other threads.
* <p>
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
* statement until it is released.
*
* @hide
*/
@SuppressWarnings({"WeakerAccess", "unused"})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract class SharedSQLiteStatement {
private final AtomicBoolean mLock = new AtomicBoolean(false);
private final RoomDatabase mDatabase;
private volatile SupportSQLiteStatement mStmt;
/**
* Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
* it automatically creates a new one.
*
* @param database The database to create the statement in.
*/
public SharedSQLiteStatement(RoomDatabase database) {
mDatabase = database;
}
/**
* Create the query.
*
* @return The SQL query to prepare.
*/
protected abstract String createQuery();
protected void assertNotMainThread() {
mDatabase.assertNotMainThread();
}
private SupportSQLiteStatement createNewStatement() {
String query = createQuery();
return mDatabase.compileStatement(query);
}
private SupportSQLiteStatement getStmt(boolean canUseCached) {
final SupportSQLiteStatement stmt;
if (canUseCached) {
if (mStmt == null) {
mStmt = createNewStatement();
}
stmt = mStmt;
} else {
// it is in use, create a one off statement
stmt = createNewStatement();
}
return stmt;
}
/**
* Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
*/
public SupportSQLiteStatement acquire() {
assertNotMainThread();
return getStmt(mLock.compareAndSet(false, true));
}
/**
* Must call this when statement will not be used anymore.
*
* @param statement The statement that was returned from acquire.
*/
public void release(SupportSQLiteStatement statement) {
if (statement == mStmt) {
mLock.set(false);
}
}
}

@ -1,62 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.NonNull;
import java.util.ArrayDeque;
import java.util.concurrent.Executor;
/**
* Executor wrapper for performing database transactions serially.
* <p>
* Since database transactions are exclusive, this executor ensures that transactions are performed
* in-order and one at a time, preventing threads from blocking each other when multiple concurrent
* transactions are attempted.
*/
class TransactionExecutor implements Executor {
private final Executor mExecutor;
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
private Runnable mActive;
TransactionExecutor(@NonNull Executor executor) {
mExecutor = executor;
}
public synchronized void execute(final Runnable command) {
mTasks.offer(new Runnable() {
public void run() {
try {
command.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
@SuppressWarnings("WeakerAccess")
synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
mExecutor.execute(mActive);
}
}
}

@ -1,63 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.migration;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteDatabase;
/**
* Base class for a database migration.
* <p>
* Each migration can move between 2 versions that are defined by {@link #startVersion} and
* {@link #endVersion}.
* <p>
* A migration can handle more than 1 version (e.g. if you have a faster path to choose when
* going version 3 to 5 without going to version 4). If Room opens a database at version
* 3 and latest version is &gt;= 5, Room will use the migration object that can migrate from
* 3 to 5 instead of 3 to 4 and 4 to 5.
* <p>
* If there are not enough migrations provided to move from the current version to the latest
* version, Room will clear the database and recreate so even if you have no changes between 2
* versions, you should still provide a Migration object to the builder.
*/
public abstract class Migration {
public final int startVersion;
public final int endVersion;
/**
* Creates a new migration between {@code startVersion} and {@code endVersion}.
*
* @param startVersion The start version of the database.
* @param endVersion The end version of the database after this migration is applied.
*/
public Migration(int startVersion, int endVersion) {
this.startVersion = startVersion;
this.endVersion = endVersion;
}
/**
* Should run the necessary migrations.
* <p>
* This class cannot access any generated Dao in this method.
* <p>
* This method is already called inside a transaction and that transaction might actually be a
* composite transaction of all necessary {@code Migration}s.
*
* @param database The database instance
*/
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
}

@ -1,129 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Room is a Database Object Mapping library that makes it easy to access database on Android
* applications.
* <p>
* Rather than hiding the details of SQLite, Room tries to embrace them by providing convenient APIs
* to query the database and also verify such queries at compile time. This allows you to access
* the full power of SQLite while having the type safety provided by Java SQL query builders.
* <p>
* There are 3 major components in Room.
* <ul>
* <li>{@link androidx.room.Database Database}: This annotation marks a class as a database.
* It should be an abstract class that extends {@link androidx.room.RoomDatabase RoomDatabase}.
* At runtime, you can acquire an instance of it via {@link androidx.room.Room#databaseBuilder(
* android.content.Context,java.lang.Class, java.lang.String) Room.databaseBuilder} or
* {@link androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context, java.lang.Class)
* Room.inMemoryDatabaseBuilder}.
* <p>
* The database class defines the list of entities and data access objects in the database.
* It is also the main access point for the underlying connection.
* </li>
* <li>{@link androidx.room.Entity Entity}: This annotation marks a class as a database row.
* For each {@link androidx.room.Entity Entity}, a database table is created to hold the items.
* The Entity class must be referenced in the
* {@link androidx.room.Database#entities() Database#entities} array. Each field of the Entity
* (and its super class) is persisted in the database unless it is denoted otherwise
* (see {@link androidx.room.Entity Entity} docs for details).
* </li>
* <li>{@link androidx.room.Dao Dao}: This annotation marks a class or interface as a
* Data Access Object. Data access objects are the main components of Room that are
* responsible for defining the methods that access the database. The class that is annotated
* with {@link androidx.room.Database Database} must have an abstract method that has 0
* arguments and returns the class that is annotated with Dao. While generating the code at
* compile time, Room will generate an implementation of this class.
* <p>
* Using Dao classes for database access rather than query builders or direct queries allows you
* to keep a separation between different components and easily mock the database access while
* testing your application.
* </li>
* </ul>
* Below is a sample of a simple database.
* <pre>
* // File: Song.java
* {@literal @}Entity
* public class User {
* {@literal @}PrimaryKey
* private int id;
* private String name;
* {@literal @}ColumnInfo(name = "release_year")
* private int releaseYear;
* // getters and setters are ignored for brevity but they are required for Room to work.
* }
* // File: SongDao.java
* {@literal @}Dao
* public interface SongDao {
* {@literal @}Query("SELECT * FROM song")
* List&lt;Song&gt; loadAll();
* {@literal @}Query("SELECT * FROM song WHERE id IN (:songIds)")
* List&lt;Song&gt; 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&lt;Song&gt; loadFirstFiveSongs();
* </pre>
* <p>
* You can also return arbitrary data objects from your query results as long as the fields in the
* object match the list of columns in the query response. This makes it very easy to write
* applications that drive the UI from persistent storage.
* <pre>
* class IdAndSongHeader {
* int id;
* {@literal @}ColumnInfo(name = "header")
* String header;
* }
* // DAO
* {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM user")
* public IdAndSongHeader[] loadSongHeaders();
* </pre>
* If there is a mismatch between the query result and the POJO, Room will print a warning during
* compilation.
* <p>
* Please see the documentation of individual classes for details.
*/
package androidx.room;

@ -1,195 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.paging;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.paging.PositionalDataSource;
import androidx.room.InvalidationTracker;
import androidx.room.RoomDatabase;
import androidx.room.RoomSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* A simple data source implementation that uses Limit & Offset to page the query.
* <p>
* This is NOT the most efficient way to do paging on SQLite. It is
* <a href="http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor">recommended</a> to use an indexed
* ORDER BY statement but that requires a more complex API. This solution is technically equal to
* receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
* never returns inconsistent data if it is invalidated.
*
* @param <T> Data type returned by the data source.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> {
private final RoomSQLiteQuery mSourceQuery;
private final String mCountQuery;
private final String mLimitOffsetQuery;
private final RoomDatabase mDb;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationTracker.Observer mObserver;
private final boolean mInTransaction;
protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
boolean inTransaction, String... tables) {
this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, tables);
}
protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
boolean inTransaction, String... tables) {
mDb = db;
mSourceQuery = query;
mInTransaction = inTransaction;
mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
mObserver = new InvalidationTracker.Observer(tables) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
invalidate();
}
};
db.getInvalidationTracker().addWeakObserver(mObserver);
}
/**
* Count number of rows query can return
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
public int countItems() {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
mSourceQuery.getArgCount());
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
Cursor cursor = mDb.query(sqLiteQuery);
try {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
}
return 0;
} finally {
cursor.close();
sqLiteQuery.release();
}
}
@Override
public boolean isInvalid() {
mDb.getInvalidationTracker().refreshVersionsSync();
return super.isInvalid();
}
@SuppressWarnings("WeakerAccess")
protected abstract List<T> convertRows(Cursor cursor);
@SuppressWarnings("deprecation")
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<T> callback) {
List<T> list = Collections.emptyList();
int totalCount = 0;
int firstLoadPosition = 0;
RoomSQLiteQuery sqLiteQuery = null;
Cursor cursor = null;
mDb.beginTransaction();
try {
totalCount = countItems();
if (totalCount != 0) {
// bound the size requested, based on known count
firstLoadPosition = computeInitialLoadPosition(params, totalCount);
int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
cursor = mDb.query(sqLiteQuery);
List<T> rows = convertRows(cursor);
mDb.setTransactionSuccessful();
list = rows;
}
} finally {
if (cursor != null) {
cursor.close();
}
mDb.endTransaction();
if (sqLiteQuery != null) {
sqLiteQuery.release();
}
}
callback.onResult(list, firstLoadPosition, totalCount);
}
@Override
public void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<T> callback) {
callback.onResult(loadRange(params.startPosition, params.loadSize));
}
/**
* Return the rows from startPos to startPos + loadCount
*
* @hide
*/
@SuppressWarnings("deprecation")
@NonNull
public List<T> loadRange(int startPosition, int loadCount) {
final RoomSQLiteQuery sqLiteQuery = getSQLiteQuery(startPosition, loadCount);
if (mInTransaction) {
mDb.beginTransaction();
Cursor cursor = null;
//noinspection TryFinallyCanBeTryWithResources
try {
cursor = mDb.query(sqLiteQuery);
List<T> rows = convertRows(cursor);
mDb.setTransactionSuccessful();
return rows;
} finally {
if (cursor != null) {
cursor.close();
}
mDb.endTransaction();
sqLiteQuery.release();
}
} else {
Cursor cursor = mDb.query(sqLiteQuery);
//noinspection TryFinallyCanBeTryWithResources
try {
return convertRows(cursor);
} finally {
cursor.close();
sqLiteQuery.release();
}
}
}
private RoomSQLiteQuery getSQLiteQuery(int startPosition, int loadCount) {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
mSourceQuery.getArgCount() + 2);
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
return sqLiteQuery;
}
}

@ -1,112 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Utility class for in-process and multi-process key-based lock mechanism for safely copying
* database files.
* <p>
* Acquiring the lock will be quick if no other thread or process has a lock with the same key.
* But if the lock is already held then acquiring it will block, until the other thread or process
* releases the lock. Note that the key and lock directory must be the same to achieve
* synchronization.
* <p>
* Locking is done via two levels:
* <ol>
* <li>
* Thread locking within the same JVM process is done via a map of String key to ReentrantLock
* objects.
* <li>
* Multi-process locking is done via a dummy file whose name contains the key and FileLock
* objects.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class CopyLock {
// in-process lock map
private static final Map<String, Lock> sThreadLocks = new HashMap<>();
private final File mCopyLockFile;
private final Lock mThreadLock;
private final boolean mFileLevelLock;
private FileChannel mLockChannel;
/**
* Creates a lock with {@code name} and using {@code lockDir} as the directory for the
* lock files.
* @param name the name of this lock.
* @param lockDir the directory where the lock files will be located.
* @param processLock whether to use file for process level locking or not.
*/
public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) {
mCopyLockFile = new File(lockDir, name + ".lck");
mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath());
mFileLevelLock = processLock;
}
/**
* Attempts to grab the lock, blocking if already held by another thread or process.
*/
public void lock() {
mThreadLock.lock();
if (mFileLevelLock) {
try {
mLockChannel = new FileOutputStream(mCopyLockFile).getChannel();
mLockChannel.lock();
} catch (IOException e) {
throw new IllegalStateException("Unable to grab copy lock.", e);
}
}
}
/**
* Releases the lock.
*/
public void unlock() {
if (mLockChannel != null) {
try {
mLockChannel.close();
} catch (IOException ignored) { }
}
mThreadLock.unlock();
}
private static Lock getThreadLock(String key) {
synchronized (sThreadLocks) {
Lock threadLock = sThreadLocks.get(key);
if (threadLock == null) {
threadLock = new ReentrantLock();
sThreadLocks.put(key, threadLock);
}
return threadLock;
}
}
}

@ -1,113 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import android.database.MatrixCursor;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
/**
* Cursor utilities for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class CursorUtil {
/**
* Copies the given cursor into a in-memory cursor and then closes it.
* <p>
* This is useful for iterating over a cursor multiple times without the cost of JNI while
* reading or IO while filling the window at the expense of memory consumption.
*
* @param c the cursor to copy.
* @return a new cursor containing the same data as the given cursor.
*/
@NonNull
public static Cursor copyAndClose(@NonNull Cursor c) {
final MatrixCursor matrixCursor;
try {
matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
while (c.moveToNext()) {
final Object[] row = new Object[c.getColumnCount()];
for (int i = 0; i < c.getColumnCount(); i++) {
switch (c.getType(i)) {
case Cursor.FIELD_TYPE_NULL:
row[i] = null;
break;
case Cursor.FIELD_TYPE_INTEGER:
row[i] = c.getLong(i);
break;
case Cursor.FIELD_TYPE_FLOAT:
row[i] = c.getDouble(i);
break;
case Cursor.FIELD_TYPE_STRING:
row[i] = c.getString(i);
break;
case Cursor.FIELD_TYPE_BLOB:
row[i] = c.getBlob(i);
break;
default:
throw new IllegalStateException();
}
}
matrixCursor.addRow(row);
}
} finally {
c.close();
}
return matrixCursor;
}
/**
* Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
* If the column is not found, it retries with the specified name surrounded by backticks.
*
* @param c The cursor.
* @param name The name of the target column.
* @return The index of the column, or -1 if not found.
*/
public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
final int index = c.getColumnIndex(name);
if (index >= 0) {
return index;
}
return c.getColumnIndex("`" + name + "`");
}
/**
* Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
* If the column is not found, it retries with the specified name surrounded by backticks.
*
* @param c The cursor.
* @param name The name of the target column.
* @return The index of the column.
* @throws IllegalArgumentException if the column does not exist.
*/
public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
final int index = c.getColumnIndex(name);
if (index >= 0) {
return index;
}
return c.getColumnIndexOrThrow("`" + name + "`");
}
private CursorUtil() {
}
}

@ -1,137 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.AbstractWindowedCursor;
import android.database.Cursor;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
/**
* Database utilities for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class DBUtil {
/**
* Performs the SQLiteQuery on the given database.
* <p>
* This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
* {@code true} and either the api level is below a certain threshold or the full result of the
* query does not fit in a single window.
*
* @param db The database to perform the query on.
* @param sqLiteQuery The query to perform.
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
* @return Result of the query.
*/
@NonNull
public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) {
final Cursor cursor = db.query(sqLiteQuery);
if (maybeCopy && cursor instanceof AbstractWindowedCursor) {
AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
int rowsInCursor = windowedCursor.getCount(); // Should fill the window.
int rowsInWindow;
if (windowedCursor.hasWindow()) {
rowsInWindow = windowedCursor.getWindow().getNumRows();
} else {
rowsInWindow = rowsInCursor;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
return CursorUtil.copyAndClose(windowedCursor);
}
}
return cursor;
}
/**
* Drops all FTS content sync triggers created by Room.
* <p>
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
* who's names start with 'room_fts_content_sync_'.
*
* @param db The database.
*/
public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) {
List<String> existingTriggers = new ArrayList<>();
Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'");
//noinspection TryFinallyCanBeTryWithResources
try {
while (cursor.moveToNext()) {
existingTriggers.add(cursor.getString(0));
}
} finally {
cursor.close();
}
for (String triggerName : existingTriggers) {
if (triggerName.startsWith("room_fts_content_sync_")) {
db.execSQL("DROP TRIGGER IF EXISTS " + triggerName);
}
}
}
/**
* Reads the user version number out of the database header from the given file.
*
* @param databaseFile the database file.
* @return the database version
* @throws IOException if something goes wrong reading the file, such as bad database header or
* missing permissions.
*
* @see <a href="https://www.sqlite.org/fileformat.html#user_version_number">User Version
* Number</a>.
*/
public static int readVersion(@NonNull File databaseFile) throws IOException {
FileChannel input = null;
try {
ByteBuffer buffer = ByteBuffer.allocate(4);
input = new FileInputStream(databaseFile).getChannel();
input.tryLock(60, 4, true);
input.position(60);
int read = input.read(buffer);
if (read != 4) {
throw new IOException("Bad database header, unable to read 4 bytes at offset 60");
}
buffer.rewind();
return buffer.getInt(); // ByteBuffer is big-endian by default
} finally {
if (input != null) {
input.close();
}
}
}
private DBUtil() {
}
}

@ -1,71 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.annotation.SuppressLint;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
/**
* File utilities for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class FileUtil {
/**
* Copies data from the input channel to the output file channel.
*
* @param input the input channel to copy.
* @param output the output channel to copy.
* @throws IOException if there is an I/O error.
*/
@SuppressLint("LambdaLast")
public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
throws IOException {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
output.transferFrom(input, 0, Long.MAX_VALUE);
} else {
InputStream inputStream = Channels.newInputStream(input);
OutputStream outputStream = Channels.newOutputStream(output);
int length;
byte[] buffer = new byte[1024 * 4];
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
}
output.force(false);
} finally {
input.close();
output.close();
}
}
private FileUtil() {
}
}

@ -1,220 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A data class that holds the information about an FTS table.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class FtsTableInfo {
// A set of valid FTS Options
private static final String[] FTS_OPTIONS = new String[] {
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
"order=", "prefix=", "uncompress="};
/**
* The table name
*/
public final String name;
/**
* The column names
*/
public final Set<String> columns;
/**
* The set of options. Each value in the set contains the option in the following format:
* &lt;key&gt;=&lt;value&gt;.
*/
public final Set<String> options;
public FtsTableInfo(String name, Set<String> columns, Set<String> options) {
this.name = name;
this.columns = columns;
this.options = options;
}
public FtsTableInfo(String name, Set<String> columns, String createSql) {
this.name = name;
this.columns = columns;
this.options = parseOptions(createSql);
}
/**
* Reads the table information from the given database.
*
* @param database The database to read the information from.
* @param tableName The table name.
* @return A FtsTableInfo containing the columns and options for the provided table name.
*/
public static FtsTableInfo read(SupportSQLiteDatabase database, String tableName) {
Set<String> columns = readColumns(database, tableName);
Set<String> options = readOptions(database, tableName);
return new FtsTableInfo(tableName, columns, options);
}
@SuppressWarnings("TryFinallyCanBeTryWithResources")
private static Set<String> readColumns(SupportSQLiteDatabase database, String tableName) {
Cursor cursor = database.query("PRAGMA table_info(`" + tableName + "`)");
Set<String> columns = new HashSet<>();
try {
if (cursor.getColumnCount() > 0) {
int nameIndex = cursor.getColumnIndex("name");
while (cursor.moveToNext()) {
columns.add(cursor.getString(nameIndex));
}
}
} finally {
cursor.close();
}
return columns;
}
@SuppressWarnings("TryFinallyCanBeTryWithResources")
private static Set<String> readOptions(SupportSQLiteDatabase database, String tableName) {
String sql = "";
Cursor cursor = database.query(
"SELECT * FROM sqlite_master WHERE `name` = '" + tableName + "'");
try {
if (cursor.moveToFirst()) {
sql = cursor.getString(cursor.getColumnIndexOrThrow("sql"));
}
} finally {
cursor.close();
}
return parseOptions(sql);
}
/**
* Parses FTS options from the create statement of an FTS table.
*
* This method assumes the given create statement is a valid well-formed SQLite statement as
* defined in the <a href="https://www.sqlite.org/lang_createvtab.html">CREATE VIRTUAL TABLE
* syntax diagram</a>.
*
* @param createStatement the "CREATE VIRTUAL TABLE" statement.
* @return the set of FTS option key and values in the create statement.
*/
@VisibleForTesting
@SuppressWarnings("WeakerAccess") /* synthetic access */
static Set<String> parseOptions(String createStatement) {
if (createStatement.isEmpty()) {
return new HashSet<>();
}
// Module arguments are within the parenthesis followed by the module name.
String argsString = createStatement.substring(
createStatement.indexOf('(') + 1,
createStatement.lastIndexOf(')'));
// Split the module argument string by the comma delimiter, keeping track of quotation so
// so that if the delimiter is found within a string literal we don't substring at the wrong
// index. SQLite supports four ways of quoting keywords, see:
// https://www.sqlite.org/lang_keywords.html
List<String> args = new ArrayList<>();
ArrayDeque<Character> quoteStack = new ArrayDeque<>();
int lastDelimiterIndex = -1;
for (int i = 0; i < argsString.length(); i++) {
char c = argsString.charAt(i);
switch (c) {
case '\'':
case '"':
case '`':
if (quoteStack.isEmpty()) {
quoteStack.push(c);
} else if (quoteStack.peek() == c) {
quoteStack.pop();
}
break;
case '[':
if (quoteStack.isEmpty()) {
quoteStack.push(c);
}
break;
case ']':
if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
quoteStack.pop();
}
break;
case ',':
if (quoteStack.isEmpty()) {
args.add(argsString.substring(lastDelimiterIndex + 1, i).trim());
lastDelimiterIndex = i;
}
break;
}
}
args.add(argsString.substring(lastDelimiterIndex + 1).trim()); // Add final argument.
// Match args against valid options, otherwise they are column definitions.
HashSet<String> options = new HashSet<>();
for (String arg : args) {
for (String validOption : FTS_OPTIONS) {
if (arg.startsWith(validOption)) {
options.add(arg);
}
}
}
return options;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FtsTableInfo that = (FtsTableInfo) o;
if (name != null ? !name.equals(that.name) : that.name != null) return false;
if (columns != null ? !columns.equals(that.columns) : that.columns != null) return false;
return options != null ? options.equals(that.options) : that.options == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (columns != null ? columns.hashCode() : 0);
result = 31 * result + (options != null ? options.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "FtsTableInfo{"
+ "name='" + name + '\''
+ ", columns=" + columns
+ ", options=" + options
+ '}';
}
}

@ -1,47 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
/**
* Java 8 Sneaky Throw technique.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class SneakyThrow {
/**
* Re-throws a checked exception as if it was a runtime exception without wrapping it.
*
* @param e the exception to re-throw.
*/
public static void reThrow(@NonNull Exception e) {
sneakyThrow(e);
}
@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
throw (E) e;
}
private SneakyThrow() {
}
}

@ -1,118 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
* @hide
*
* String utilities for Room
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class StringUtil {
@SuppressWarnings("unused")
public static final String[] EMPTY_STRING_ARRAY = new String[0];
/**
* Returns a new StringBuilder to be used while producing SQL queries.
*
* @return A new or recycled StringBuilder
*/
public static StringBuilder newStringBuilder() {
// TODO pool:
return new StringBuilder();
}
/**
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
* by a comma.
*
* @param builder The StringBuilder for the query
* @param count Number of placeholders
*/
public static void appendPlaceholders(StringBuilder builder, int count) {
for (int i = 0; i < count; i++) {
builder.append("?");
if (i < count - 1) {
builder.append(",");
}
}
}
/**
* Splits a comma separated list of integers to integer list.
* <p>
* If an input is malformed, it is omitted from the result.
*
* @param input Comma separated list of integers.
* @return A List containing the integers or null if the input is null.
*/
@Nullable
public static List<Integer> splitToIntList(@Nullable String input) {
if (input == null) {
return null;
}
List<Integer> result = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(input, ",");
while (tokenizer.hasMoreElements()) {
final String item = tokenizer.nextToken();
try {
result.add(Integer.parseInt(item));
} catch (NumberFormatException ex) {
Log.e("ROOM", "Malformed integer list", ex);
}
}
return result;
}
/**
* Joins the given list of integers into a comma separated list.
*
* @param input The list of integers.
* @return Comma separated string composed of integers in the list. If the list is null, return
* value is null.
*/
@Nullable
public static String joinIntoString(@Nullable List<Integer> input) {
if (input == null) {
return null;
}
final int size = input.size();
if (size == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(Integer.toString(input.get(i)));
if (i < size - 1) {
sb.append(",");
}
}
return sb.toString();
}
private StringUtil() {
}
}

@ -1,665 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import android.os.Build;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.ColumnInfo;
import androidx.sqlite.db.SupportSQLiteDatabase;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* A data class that holds the information about a table.
* <p>
* It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
* <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
* documentation for more details.
* <p>
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
"SimplifiableIfStatement"})
// if you change this class, you must change TableInfoWriter.kt
public class TableInfo {
/**
* Identifies from where the info object was created.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
@interface CreatedFrom {
}
/**
* Identifier for when the info is created from an unknown source.
*/
public static final int CREATED_FROM_UNKNOWN = 0;
/**
* Identifier for when the info is created from an entity definition, such as generated code
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
*/
public static final int CREATED_FROM_ENTITY = 1;
/**
* Identifier for when the info is created from the database itself, reading information from a
* PRAGMA, such as table_info.
*/
public static final int CREATED_FROM_DATABASE = 2;
/**
* The table name.
*/
public final String name;
/**
* Unmodifiable map of columns keyed by column name.
*/
public final Map<String, Column> columns;
public final Set<ForeignKey> foreignKeys;
/**
* Sometimes, Index information is not available (older versions). If so, we skip their
* verification.
*/
@Nullable
public final Set<Index> indices;
@SuppressWarnings("unused")
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys,
Set<Index> indices) {
this.name = name;
this.columns = Collections.unmodifiableMap(columns);
this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
}
/**
* For backward compatibility with dbs created with older versions.
*/
@SuppressWarnings("unused")
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
this(name, columns, foreignKeys, Collections.<Index>emptySet());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TableInfo tableInfo = (TableInfo) o;
if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
return false;
}
if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
: tableInfo.foreignKeys != null) {
return false;
}
if (indices == null || tableInfo.indices == null) {
// if one us is missing index information, seems like we couldn't acquire the
// information so we better skip.
return true;
}
return indices.equals(tableInfo.indices);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (columns != null ? columns.hashCode() : 0);
result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
// skip index, it is not reliable for comparison.
return result;
}
@Override
public String toString() {
return "TableInfo{"
+ "name='" + name + '\''
+ ", columns=" + columns
+ ", foreignKeys=" + foreignKeys
+ ", indices=" + indices
+ '}';
}
/**
* Reads the table information from the given database.
*
* @param database The database to read the information from.
* @param tableName The table name.
* @return A TableInfo containing the schema information for the provided table name.
*/
@SuppressWarnings("SameParameterValue")
public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
Map<String, Column> columns = readColumns(database, tableName);
Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
Set<Index> indices = readIndices(database, tableName);
return new TableInfo(tableName, columns, foreignKeys, indices);
}
private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
String tableName) {
Set<ForeignKey> foreignKeys = new HashSet<>();
// this seems to return everything in order but it is not documented so better be safe
Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
try {
final int idColumnIndex = cursor.getColumnIndex("id");
final int seqColumnIndex = cursor.getColumnIndex("seq");
final int tableColumnIndex = cursor.getColumnIndex("table");
final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor);
final int count = cursor.getCount();
for (int position = 0; position < count; position++) {
cursor.moveToPosition(position);
final int seq = cursor.getInt(seqColumnIndex);
if (seq != 0) {
continue;
}
final int id = cursor.getInt(idColumnIndex);
List<String> myColumns = new ArrayList<>();
List<String> refColumns = new ArrayList<>();
for (ForeignKeyWithSequence key : ordered) {
if (key.mId == id) {
myColumns.add(key.mFrom);
refColumns.add(key.mTo);
}
}
foreignKeys.add(new ForeignKey(
cursor.getString(tableColumnIndex),
cursor.getString(onDeleteColumnIndex),
cursor.getString(onUpdateColumnIndex),
myColumns,
refColumns
));
}
} finally {
cursor.close();
}
return foreignKeys;
}
private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) {
final int idColumnIndex = cursor.getColumnIndex("id");
final int seqColumnIndex = cursor.getColumnIndex("seq");
final int fromColumnIndex = cursor.getColumnIndex("from");
final int toColumnIndex = cursor.getColumnIndex("to");
final int count = cursor.getCount();
List<ForeignKeyWithSequence> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
cursor.moveToPosition(i);
result.add(new ForeignKeyWithSequence(
cursor.getInt(idColumnIndex),
cursor.getInt(seqColumnIndex),
cursor.getString(fromColumnIndex),
cursor.getString(toColumnIndex)
));
}
Collections.sort(result);
return result;
}
private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
String tableName) {
Cursor cursor = database
.query("PRAGMA table_info(`" + tableName + "`)");
//noinspection TryFinallyCanBeTryWithResources
Map<String, Column> columns = new HashMap<>();
try {
if (cursor.getColumnCount() > 0) {
int nameIndex = cursor.getColumnIndex("name");
int typeIndex = cursor.getColumnIndex("type");
int notNullIndex = cursor.getColumnIndex("notnull");
int pkIndex = cursor.getColumnIndex("pk");
int defaultValueIndex = cursor.getColumnIndex("dflt_value");
while (cursor.moveToNext()) {
final String name = cursor.getString(nameIndex);
final String type = cursor.getString(typeIndex);
final boolean notNull = 0 != cursor.getInt(notNullIndex);
final int primaryKeyPosition = cursor.getInt(pkIndex);
final String defaultValue = cursor.getString(defaultValueIndex);
columns.put(name,
new Column(name, type, notNull, primaryKeyPosition, defaultValue,
CREATED_FROM_DATABASE));
}
}
} finally {
cursor.close();
}
return columns;
}
/**
* @return null if we cannot read the indices due to older sqlite implementations.
*/
@Nullable
private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) {
Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
try {
final int nameColumnIndex = cursor.getColumnIndex("name");
final int originColumnIndex = cursor.getColumnIndex("origin");
final int uniqueIndex = cursor.getColumnIndex("unique");
if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
// we cannot read them so better not validate any index.
return null;
}
HashSet<Index> indices = new HashSet<>();
while (cursor.moveToNext()) {
String origin = cursor.getString(originColumnIndex);
if (!"c".equals(origin)) {
// Ignore auto-created indices
continue;
}
String name = cursor.getString(nameColumnIndex);
boolean unique = cursor.getInt(uniqueIndex) == 1;
Index index = readIndex(database, name, unique);
if (index == null) {
// we cannot read it properly so better not read it
return null;
}
indices.add(index);
}
return indices;
} finally {
cursor.close();
}
}
/**
* @return null if we cannot read the index due to older sqlite implementations.
*/
@Nullable
private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
try {
final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
final int cidColumnIndex = cursor.getColumnIndex("cid");
final int nameColumnIndex = cursor.getColumnIndex("name");
if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) {
// we cannot read them so better not validate any index.
return null;
}
final TreeMap<Integer, String> results = new TreeMap<>();
while (cursor.moveToNext()) {
int cid = cursor.getInt(cidColumnIndex);
if (cid < 0) {
// Ignore SQLite row ID
continue;
}
int seq = cursor.getInt(seqnoColumnIndex);
String columnName = cursor.getString(nameColumnIndex);
results.put(seq, columnName);
}
final List<String> columns = new ArrayList<>(results.size());
columns.addAll(results.values());
return new Index(name, unique, columns);
} finally {
cursor.close();
}
}
/**
* Holds the information about a database column.
*/
@SuppressWarnings("WeakerAccess")
public static class Column {
/**
* The column name.
*/
public final String name;
/**
* The column type affinity.
*/
public final String type;
/**
* The column type after it is normalized to one of the basic types according to
* https://www.sqlite.org/datatype3.html Section 3.1.
* <p>
* This is the value Room uses for equality check.
*/
@ColumnInfo.SQLiteTypeAffinity
public final int affinity;
/**
* Whether or not the column can be NULL.
*/
public final boolean notNull;
/**
* The position of the column in the list of primary keys, 0 if the column is not part
* of the primary key.
* <p>
* This information is only available in API 20+.
* <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
* On older platforms, it will be 1 if the column is part of the primary key and 0
* otherwise.
* <p>
* The {@link #equals(Object)} implementation handles this inconsistency based on
* API levels os if you are using a custom SQLite deployment, it may return false
* positives.
*/
public final int primaryKeyPosition;
/**
* The default value of this column.
*/
public final String defaultValue;
@CreatedFrom
private final int mCreatedFrom;
/**
* @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
*/
@Deprecated
public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
}
// if you change this constructor, you must change TableInfoWriter.kt
public Column(String name, String type, boolean notNull, int primaryKeyPosition,
String defaultValue, @CreatedFrom int createdFrom) {
this.name = name;
this.type = type;
this.notNull = notNull;
this.primaryKeyPosition = primaryKeyPosition;
this.affinity = findAffinity(type);
this.defaultValue = defaultValue;
this.mCreatedFrom = createdFrom;
}
/**
* Implements https://www.sqlite.org/datatype3.html section 3.1
*
* @param type The type that was given to the sqlite
* @return The normalized type which is one of the 5 known affinities
*/
@ColumnInfo.SQLiteTypeAffinity
private static int findAffinity(@Nullable String type) {
if (type == null) {
return ColumnInfo.BLOB;
}
String uppercaseType = type.toUpperCase(Locale.US);
if (uppercaseType.contains("INT")) {
return ColumnInfo.INTEGER;
}
if (uppercaseType.contains("CHAR")
|| uppercaseType.contains("CLOB")
|| uppercaseType.contains("TEXT")) {
return ColumnInfo.TEXT;
}
if (uppercaseType.contains("BLOB")) {
return ColumnInfo.BLOB;
}
if (uppercaseType.contains("REAL")
|| uppercaseType.contains("FLOA")
|| uppercaseType.contains("DOUB")) {
return ColumnInfo.REAL;
}
// sqlite returns NUMERIC here but it is like a catch all. We already
// have UNDEFINED so it is better to use UNDEFINED for consistency.
return ColumnInfo.UNDEFINED;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Column column = (Column) o;
if (Build.VERSION.SDK_INT >= 20) {
if (primaryKeyPosition != column.primaryKeyPosition) return false;
} else {
if (isPrimaryKey() != column.isPrimaryKey()) return false;
}
if (!name.equals(column.name)) return false;
//noinspection SimplifiableIfStatement
if (notNull != column.notNull) return false;
// Only validate default value if it was defined in an entity, i.e. if the info
// from the compiler itself has it. b/136019383
if (mCreatedFrom == CREATED_FROM_ENTITY
&& column.mCreatedFrom == CREATED_FROM_DATABASE
&& (defaultValue != null && !defaultValue.equals(column.defaultValue))) {
return false;
} else if (mCreatedFrom == CREATED_FROM_DATABASE
&& column.mCreatedFrom == CREATED_FROM_ENTITY
&& (column.defaultValue != null && !column.defaultValue.equals(defaultValue))) {
return false;
} else if (mCreatedFrom != CREATED_FROM_UNKNOWN
&& mCreatedFrom == column.mCreatedFrom
&& (defaultValue != null ? !defaultValue.equals(column.defaultValue)
: column.defaultValue != null)) {
return false;
}
return affinity == column.affinity;
}
/**
* Returns whether this column is part of the primary key or not.
*
* @return True if this column is part of the primary key, false otherwise.
*/
public boolean isPrimaryKey() {
return primaryKeyPosition > 0;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + affinity;
result = 31 * result + (notNull ? 1231 : 1237);
result = 31 * result + primaryKeyPosition;
// Default value is not part of the hashcode since we conditionally check it for
// equality which would break the equals + hashcode contract.
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Column{"
+ "name='" + name + '\''
+ ", type='" + type + '\''
+ ", affinity='" + affinity + '\''
+ ", notNull=" + notNull
+ ", primaryKeyPosition=" + primaryKeyPosition
+ ", defaultValue='" + defaultValue + '\''
+ '}';
}
}
/**
* Holds the information about an SQLite foreign key
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class ForeignKey {
@NonNull
public final String referenceTable;
@NonNull
public final String onDelete;
@NonNull
public final String onUpdate;
@NonNull
public final List<String> columnNames;
@NonNull
public final List<String> referenceColumnNames;
public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
@NonNull String onUpdate,
@NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) {
this.referenceTable = referenceTable;
this.onDelete = onDelete;
this.onUpdate = onUpdate;
this.columnNames = Collections.unmodifiableList(columnNames);
this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ForeignKey that = (ForeignKey) o;
if (!referenceTable.equals(that.referenceTable)) return false;
if (!onDelete.equals(that.onDelete)) return false;
if (!onUpdate.equals(that.onUpdate)) return false;
//noinspection SimplifiableIfStatement
if (!columnNames.equals(that.columnNames)) return false;
return referenceColumnNames.equals(that.referenceColumnNames);
}
@Override
public int hashCode() {
int result = referenceTable.hashCode();
result = 31 * result + onDelete.hashCode();
result = 31 * result + onUpdate.hashCode();
result = 31 * result + columnNames.hashCode();
result = 31 * result + referenceColumnNames.hashCode();
return result;
}
@Override
public String toString() {
return "ForeignKey{"
+ "referenceTable='" + referenceTable + '\''
+ ", onDelete='" + onDelete + '\''
+ ", onUpdate='" + onUpdate + '\''
+ ", columnNames=" + columnNames
+ ", referenceColumnNames=" + referenceColumnNames
+ '}';
}
}
/**
* Temporary data holder for a foreign key row in the pragma result. We need this to ensure
* sorting in the generated foreign key object.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> {
final int mId;
final int mSequence;
final String mFrom;
final String mTo;
ForeignKeyWithSequence(int id, int sequence, String from, String to) {
mId = id;
mSequence = sequence;
mFrom = from;
mTo = to;
}
@Override
public int compareTo(@NonNull ForeignKeyWithSequence o) {
final int idCmp = mId - o.mId;
if (idCmp == 0) {
return mSequence - o.mSequence;
} else {
return idCmp;
}
}
}
/**
* Holds the information about an SQLite index
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class Index {
// should match the value in Index.kt
public static final String DEFAULT_PREFIX = "index_";
public final String name;
public final boolean unique;
public final List<String> columns;
public Index(String name, boolean unique, List<String> columns) {
this.name = name;
this.unique = unique;
this.columns = columns;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Index index = (Index) o;
if (unique != index.unique) {
return false;
}
if (!columns.equals(index.columns)) {
return false;
}
if (name.startsWith(Index.DEFAULT_PREFIX)) {
return index.name.startsWith(Index.DEFAULT_PREFIX);
} else {
return name.equals(index.name);
}
}
@Override
public int hashCode() {
int result;
if (name.startsWith(DEFAULT_PREFIX)) {
result = DEFAULT_PREFIX.hashCode();
} else {
result = name.hashCode();
}
result = 31 * result + (unique ? 1 : 0);
result = 31 * result + columns.hashCode();
return result;
}
@Override
public String toString() {
return "Index{"
+ "name='" + name + '\''
+ ", unique=" + unique
+ ", columns=" + columns
+ '}';
}
}
}

@ -1,97 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteDatabase;
/**
* A data class that holds the information about a view.
* <p>
* This derives information from sqlite_master.
* <p>
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class ViewInfo {
/**
* The view name
*/
public final String name;
/**
* The SQL of CREATE VIEW.
*/
public final String sql;
public ViewInfo(String name, String sql) {
this.name = name;
this.sql = sql;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ViewInfo viewInfo = (ViewInfo) o;
return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null)
&& (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (sql != null ? sql.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "ViewInfo{"
+ "name='" + name + '\''
+ ", sql='" + sql + '\''
+ '}';
}
/**
* Reads the view information from the given database.
*
* @param database The database to read the information from.
* @param viewName The view name.
* @return A ViewInfo containing the schema information for the provided view name.
*/
@SuppressWarnings("SameParameterValue")
public static ViewInfo read(SupportSQLiteDatabase database, String viewName) {
Cursor cursor = database.query("SELECT name, sql FROM sqlite_master "
+ "WHERE type = 'view' AND name = '" + viewName + "'");
//noinspection TryFinallyCanBeTryWithResources
try {
if (cursor.moveToFirst()) {
return new ViewInfo(cursor.getString(0), cursor.getString(1));
} else {
return new ViewInfo(viewName, null);
}
} finally {
cursor.close();
}
}
}
Loading…
Cancel
Save