├── .gitignore
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── runtime
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── aidl
│ └── androidx
│ │ └── room
│ │ ├── IMultiInstanceInvalidationCallback.aidl
│ │ └── IMultiInstanceInvalidationService.aidl
│ └── java
│ └── androidx
│ └── room
│ ├── DatabaseConfiguration.java
│ ├── EntityDeletionOrUpdateAdapter.java
│ ├── EntityInsertionAdapter.java
│ ├── InvalidationLiveDataContainer.java
│ ├── InvalidationTracker.java
│ ├── MultiInstanceInvalidationClient.java
│ ├── MultiInstanceInvalidationService.java
│ ├── Room.java
│ ├── RoomDatabase.java
│ ├── RoomOpenHelper.java
│ ├── RoomSQLiteQuery.java
│ ├── RoomTrackingLiveData.java
│ ├── SQLiteCopyOpenHelper.java
│ ├── SQLiteCopyOpenHelperFactory.java
│ ├── SharedSQLiteStatement.java
│ ├── TransactionExecutor.java
│ ├── migration
│ └── Migration.java
│ ├── package-info.java
│ ├── paging
│ └── LimitOffsetDataSource.java
│ └── util
│ ├── CopyLock.java
│ ├── CursorUtil.java
│ ├── DBUtil.java
│ ├── FileUtil.java
│ ├── FtsTableInfo.java
│ ├── SneakyThrow.java
│ ├── StringUtil.java
│ ├── TableInfo.java
│ └── ViewInfo.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Room-Runtime
2 | [](https://jitpack.io/#topjohnwu/room-runtime)
3 |
4 | This is a modified version of the AndroidX Room Persistent library, specifically the [room-runtime](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-room-release/room/runtime/) component.
5 |
6 | ## Why
7 |
8 | The goal is to allow full Proguard/R8 class obfuscation. In official room runtime, this proguard rule `-keep class * extends androidx.room.RoomDatabase` prevents any possible obfuscation for `RoomDatabase` classes. The reason why this rule is needed is because the generated `RoomDatabase` implementation will be found and created via classname reflection at runtime.
9 |
10 | ## Download
11 |
12 | ```gradle
13 | android {
14 | compileOptions {
15 | sourceCompatibility JavaVersion.VERSION_1_8
16 | targetCompatibility JavaVersion.VERSION_1_8
17 | }
18 | }
19 | repositories {
20 | maven { url 'https://jitpack.io' }
21 | }
22 | dependencies {
23 | // Remove androidx.room:room-runtime and replace it with the following
24 | modules {
25 | module('androidx.room:room-runtime') {
26 | replacedBy('com.github.topjohnwu:room-runtime')
27 | }
28 | }
29 | implementation "com.github.topjohnwu:room-runtime:${vRoom}"
30 | }
31 | ```
32 |
33 | ## Usage
34 |
35 | In order to remove the usage of reflection, you will have to create a `RoomDatabase.Factory` to create database instances. Implement the factory and set it as shown below in static blocks of your `Application` or main `Activity`:
36 |
37 | ```java
38 | Room.setFactory(clazz -> {
39 | switch (clazz) {
40 | case MyRoomDB1.class:
41 | return new MyRoomDB1_Impl();
42 | case MyRoomDB2.class:
43 | return new MyRoomDB2_Impl();
44 | default:
45 | return null;
46 | }
47 | });
48 | ```
49 |
50 | Or in Kotlin:
51 |
52 | ```kotlin
53 | Room.setFactory {
54 | when(it) {
55 | MyRoomDB1::class.java -> MyRoomDB1_Impl()
56 | MyRoomDB2::class.java -> MyRoomDB2_Impl()
57 | else -> null
58 | }
59 | }
60 | ```
61 |
62 | Note that your factory class has to handle **ALL** `RoomDatabase` throughout your app. There might be additional `RoomDatabase` used in your dependencies (for example, `WorkManager`). To find out all possible `RoomDatabase` used in your application, the easiest way is to add this to your proguard rules: `-whyareyoukeeping class * extends androidx.room.RoomDatabase`, and build your project before switching to this implementation. It will print out all `RoomDatabase` classes in your project, and you can implement your `RoomDatabase.Factory` accordingly.
63 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | jcenter()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.5.1'
11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 |
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
29 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/topjohnwu/room-runtime/5375ae9f88cb0608f361005621e1cd4445ba9f49/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jun 06 01:54:02 PDT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/runtime/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'com.github.dcendents.android-maven'
3 |
4 | android {
5 | compileSdkVersion 29
6 | buildToolsVersion "29.0.2"
7 |
8 | defaultConfig {
9 | minSdkVersion 14
10 | targetSdkVersion 29
11 | consumerProguardFiles 'proguard-rules.pro'
12 | }
13 |
14 | compileOptions {
15 | sourceCompatibility JavaVersion.VERSION_1_8
16 | targetCompatibility JavaVersion.VERSION_1_8
17 | }
18 |
19 | }
20 |
21 | dependencies {
22 | implementation fileTree(dir: 'libs', include: ['*.jar'])
23 |
24 | def vRoom = "2.2.1"
25 | api "androidx.room:room-common:${vRoom}"
26 |
27 | def vSql = "2.0.1"
28 | api "androidx.sqlite:sqlite-framework:${vSql}"
29 | api "androidx.sqlite:sqlite:${vSql}"
30 |
31 | implementation 'androidx.arch.core:core-runtime:2.0.1'
32 |
33 | compileOnly 'androidx.lifecycle:lifecycle-livedata-core:2.0.0'
34 | compileOnly 'androidx.paging:paging-common:2.0.0'
35 | }
36 |
37 | task sourcesJar(type: Jar) {
38 | archiveClassifier.set('sources')
39 | from android.sourceSets.main.java.sourceFiles
40 | }
41 |
--------------------------------------------------------------------------------
/runtime/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # -keep class * extends androidx.room.RoomDatabase
2 | -dontwarn androidx.room.paging.**
3 |
--------------------------------------------------------------------------------
/runtime/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/runtime/src/main/aidl/androidx/room/IMultiInstanceInvalidationCallback.aidl:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | /**
20 | * RPC Callbacks for {@link IMultiInstanceInvalidationService}.
21 | *
22 | * @hide
23 | */
24 | interface IMultiInstanceInvalidationCallback {
25 |
26 | /**
27 | * Called when invalidation is detected in another instance of the same database.
28 | *
29 | * @param tables List of invalidated table names
30 | */
31 | oneway void onInvalidation(in String[] tables);
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/runtime/src/main/aidl/androidx/room/IMultiInstanceInvalidationService.aidl:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.room.IMultiInstanceInvalidationCallback;
20 |
21 | /**
22 | * RPC Service that controls interaction about multi-instance invalidation.
23 | *
24 | * @hide
25 | */
26 | interface IMultiInstanceInvalidationService {
27 |
28 | /**
29 | * Registers a new {@link IMultiInstanceInvalidationCallback} as a client of this service.
30 | *
31 | * @param callback The RPC callback.
32 | * @param name The name of the database file as it is passed to {@link RoomDatabase.Builder}.
33 | * @return A new client ID. The client needs to hold on to this ID and pass it to the service
34 | * for subsequent calls.
35 | */
36 | int registerCallback(IMultiInstanceInvalidationCallback callback, String name);
37 |
38 | /**
39 | * Unregisters the specified {@link IMultiInstanceInvalidationCallback} from this service.
40 | *
41 | * Clients might die without explicitly calling this method. In that case, the service should
42 | * handle the clean up.
43 | *
44 | * @param callback The RPC callback.
45 | * @param clientId The client ID returned from {@link #registerCallback}.
46 | */
47 | void unregisterCallback(IMultiInstanceInvalidationCallback callback, int clientId);
48 |
49 | /**
50 | * Broadcasts invalidation of database tables to other clients registered to this service.
51 | *
52 | * The broadcast is delivered to {@link IMultiInstanceInvalidationCallback#onInvalidation} of
53 | * the registered clients. The client calling this method will not receive its own broadcast.
54 | * Clients that are associated with a different database file will not be notified.
55 | *
56 | * @param clientId The client ID returned from {@link #registerCallback}.
57 | * @param tables The names of invalidated tables.
58 | */
59 | oneway void broadcastInvalidation(int clientId, in String[] tables);
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/DatabaseConfiguration.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import android.content.Context;
20 |
21 | import androidx.annotation.NonNull;
22 | import androidx.annotation.Nullable;
23 | import androidx.annotation.RestrictTo;
24 | import androidx.sqlite.db.SupportSQLiteOpenHelper;
25 |
26 | import java.io.File;
27 | import java.util.List;
28 | import java.util.Set;
29 | import java.util.concurrent.Executor;
30 |
31 | /**
32 | * Configuration class for a {@link RoomDatabase}.
33 | */
34 | @SuppressWarnings("WeakerAccess")
35 | public class DatabaseConfiguration {
36 |
37 | /**
38 | * The factory to use to access the database.
39 | */
40 | @NonNull
41 | public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
42 | /**
43 | * The context to use while connecting to the database.
44 | */
45 | @NonNull
46 | public final Context context;
47 | /**
48 | * The name of the database file or null if it is an in-memory database.
49 | */
50 | @Nullable
51 | public final String name;
52 |
53 | /**
54 | * Collection of available migrations.
55 | */
56 | @NonNull
57 | public final RoomDatabase.MigrationContainer migrationContainer;
58 |
59 | @Nullable
60 | public final List callbacks;
61 |
62 | /**
63 | * Whether Room should throw an exception for queries run on the main thread.
64 | */
65 | public final boolean allowMainThreadQueries;
66 |
67 | /**
68 | * The journal mode for this database.
69 | */
70 | public final RoomDatabase.JournalMode journalMode;
71 |
72 | /**
73 | * The Executor used to execute asynchronous queries.
74 | */
75 | @NonNull
76 | public final Executor queryExecutor;
77 |
78 | /**
79 | * The Executor used to execute asynchronous transactions.
80 | */
81 | @NonNull
82 | public final Executor transactionExecutor;
83 |
84 | /**
85 | * If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
86 | * synchronized with other instances of the same {@link RoomDatabase} file, including those
87 | * in a separate process.
88 | */
89 | public final boolean multiInstanceInvalidation;
90 |
91 | /**
92 | * If true, Room should crash if a migration is missing.
93 | */
94 | public final boolean requireMigration;
95 |
96 | /**
97 | * If true, Room should perform a destructive migration when downgrading without an available
98 | * migration.
99 | */
100 | public final boolean allowDestructiveMigrationOnDowngrade;
101 |
102 | /**
103 | * The collection of schema versions from which migrations aren't required.
104 | */
105 | private final Set mMigrationNotRequiredFrom;
106 |
107 | /**
108 | * The assets path to a pre-packaged database to copy from.
109 | */
110 | @Nullable
111 | public final String copyFromAssetPath;
112 |
113 | /**
114 | * The pre-packaged database file to copy from.
115 | */
116 | @Nullable
117 | public final File copyFromFile;
118 |
119 |
120 | /**
121 | * Creates a database configuration with the given values.
122 | *
123 | * @deprecated Use {@link #DatabaseConfiguration(Context, String,
124 | * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
125 | * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
126 | *
127 | * @param context The application context.
128 | * @param name Name of the database, can be null if it is in memory.
129 | * @param sqliteOpenHelperFactory The open helper factory to use.
130 | * @param migrationContainer The migration container for migrations.
131 | * @param callbacks The list of callbacks for database events.
132 | * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
133 | * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
134 | * @param queryExecutor The Executor used to execute asynchronous queries.
135 | * @param requireMigration True if Room should require a valid migration if version changes,
136 | * instead of recreating the tables.
137 | * @param migrationNotRequiredFrom The collection of schema versions from which migrations
138 | * aren't required.
139 | *
140 | * @hide
141 | */
142 | @Deprecated
143 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
144 | public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
145 | @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
146 | @NonNull RoomDatabase.MigrationContainer migrationContainer,
147 | @Nullable List callbacks,
148 | boolean allowMainThreadQueries,
149 | RoomDatabase.JournalMode journalMode,
150 | @NonNull Executor queryExecutor,
151 | boolean requireMigration,
152 | @Nullable Set migrationNotRequiredFrom) {
153 | this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
154 | allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
155 | requireMigration, false, migrationNotRequiredFrom, null, null);
156 | }
157 |
158 | /**
159 | * Creates a database configuration with the given values.
160 | *
161 | * @deprecated Use {@link #DatabaseConfiguration(Context, String,
162 | * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
163 | * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
164 | *
165 | * @param context The application context.
166 | * @param name Name of the database, can be null if it is in memory.
167 | * @param sqliteOpenHelperFactory The open helper factory to use.
168 | * @param migrationContainer The migration container for migrations.
169 | * @param callbacks The list of callbacks for database events.
170 | * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
171 | * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
172 | * @param queryExecutor The Executor used to execute asynchronous queries.
173 | * @param transactionExecutor The Executor used to execute asynchronous transactions.
174 | * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
175 | * @param requireMigration True if Room should require a valid migration if version changes,
176 | * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
177 | * migration is supplied during a downgrade.
178 | * @param migrationNotRequiredFrom The collection of schema versions from which migrations
179 | * aren't required.
180 | *
181 | * @hide
182 | */
183 | @Deprecated
184 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
185 | public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
186 | @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
187 | @NonNull RoomDatabase.MigrationContainer migrationContainer,
188 | @Nullable List callbacks,
189 | boolean allowMainThreadQueries,
190 | RoomDatabase.JournalMode journalMode,
191 | @NonNull Executor queryExecutor,
192 | @NonNull Executor transactionExecutor,
193 | boolean multiInstanceInvalidation,
194 | boolean requireMigration,
195 | boolean allowDestructiveMigrationOnDowngrade,
196 | @Nullable Set migrationNotRequiredFrom) {
197 | this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
198 | allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
199 | multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
200 | migrationNotRequiredFrom, null, null);
201 | }
202 |
203 | /**
204 | * Creates a database configuration with the given values.
205 | *
206 | * @param context The application context.
207 | * @param name Name of the database, can be null if it is in memory.
208 | * @param sqliteOpenHelperFactory The open helper factory to use.
209 | * @param migrationContainer The migration container for migrations.
210 | * @param callbacks The list of callbacks for database events.
211 | * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
212 | * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
213 | * @param queryExecutor The Executor used to execute asynchronous queries.
214 | * @param transactionExecutor The Executor used to execute asynchronous transactions.
215 | * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
216 | * @param requireMigration True if Room should require a valid migration if version changes,
217 | * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
218 | * migration is supplied during a downgrade.
219 | * @param migrationNotRequiredFrom The collection of schema versions from which migrations
220 | * aren't required.
221 | * @param copyFromAssetPath The assets path to the pre-packaged database.
222 | * @param copyFromFile The pre-packaged database file.
223 | *
224 | * @hide
225 | */
226 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
227 | public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
228 | @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
229 | @NonNull RoomDatabase.MigrationContainer migrationContainer,
230 | @Nullable List callbacks,
231 | boolean allowMainThreadQueries,
232 | RoomDatabase.JournalMode journalMode,
233 | @NonNull Executor queryExecutor,
234 | @NonNull Executor transactionExecutor,
235 | boolean multiInstanceInvalidation,
236 | boolean requireMigration,
237 | boolean allowDestructiveMigrationOnDowngrade,
238 | @Nullable Set migrationNotRequiredFrom,
239 | @Nullable String copyFromAssetPath,
240 | @Nullable File copyFromFile) {
241 | this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
242 | this.context = context;
243 | this.name = name;
244 | this.migrationContainer = migrationContainer;
245 | this.callbacks = callbacks;
246 | this.allowMainThreadQueries = allowMainThreadQueries;
247 | this.journalMode = journalMode;
248 | this.queryExecutor = queryExecutor;
249 | this.transactionExecutor = transactionExecutor;
250 | this.multiInstanceInvalidation = multiInstanceInvalidation;
251 | this.requireMigration = requireMigration;
252 | this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
253 | this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
254 | this.copyFromAssetPath = copyFromAssetPath;
255 | this.copyFromFile = copyFromFile;
256 | }
257 |
258 | /**
259 | * Returns whether a migration is required from the specified version.
260 | *
261 | * @param version The schema version.
262 | * @return True if a valid migration is required, false otherwise.
263 | *
264 | * @deprecated Use {@link #isMigrationRequired(int, int)} which takes
265 | * {@link #allowDestructiveMigrationOnDowngrade} into account.
266 | */
267 | @Deprecated
268 | public boolean isMigrationRequiredFrom(int version) {
269 | return isMigrationRequired(version, version + 1);
270 | }
271 |
272 | /**
273 | * Returns whether a migration is required between two versions.
274 | *
275 | * @param fromVersion The old schema version.
276 | * @param toVersion The new schema version.
277 | * @return True if a valid migration is required, false otherwise.
278 | */
279 | public boolean isMigrationRequired(int fromVersion, int toVersion) {
280 | // Migrations are not required if its a downgrade AND destructive migration during downgrade
281 | // has been allowed.
282 | final boolean isDowngrade = fromVersion > toVersion;
283 | if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
284 | return false;
285 | }
286 |
287 | // Migrations are required between the two versions if we generally require migrations
288 | // AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
289 | // exceptions.
290 | return requireMigration
291 | && (mMigrationNotRequiredFrom == null
292 | || !mMigrationNotRequiredFrom.contains(fromVersion));
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.annotation.RestrictTo;
20 | import androidx.sqlite.db.SupportSQLiteStatement;
21 |
22 | /**
23 | * Implementations of this class knows how to delete or update a particular entity.
24 | *
25 | * This is an internal library class and all of its implementations are auto-generated.
26 | *
27 | * @param The type parameter of the entity to be deleted
28 | * @hide
29 | */
30 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
31 | @SuppressWarnings({"WeakerAccess", "unused"})
32 | public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteStatement {
33 | /**
34 | * Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
35 | * database.
36 | *
37 | * @param database The database to delete / update the item in.
38 | */
39 | public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
40 | super(database);
41 | }
42 |
43 | /**
44 | * Create the deletion or update query
45 | *
46 | * @return An SQL query that can delete or update instances of T.
47 | */
48 | @Override
49 | protected abstract String createQuery();
50 |
51 | /**
52 | * Binds the entity into the given statement.
53 | *
54 | * @param statement The SQLite statement that prepared for the query returned from
55 | * createQuery.
56 | * @param entity The entity of type T.
57 | */
58 | protected abstract void bind(SupportSQLiteStatement statement, T entity);
59 |
60 | /**
61 | * Deletes or updates the given entities in the database and returns the affected row count.
62 | *
63 | * @param entity The entity to delete or update
64 | * @return The number of affected rows
65 | */
66 | public final int handle(T entity) {
67 | final SupportSQLiteStatement stmt = acquire();
68 | try {
69 | bind(stmt, entity);
70 | return stmt.executeUpdateDelete();
71 | } finally {
72 | release(stmt);
73 | }
74 | }
75 |
76 | /**
77 | * Deletes or updates the given entities in the database and returns the affected row count.
78 | *
79 | * @param entities Entities to delete or update
80 | * @return The number of affected rows
81 | */
82 | public final int handleMultiple(Iterable extends T> entities) {
83 | final SupportSQLiteStatement stmt = acquire();
84 | try {
85 | int total = 0;
86 | for (T entity : entities) {
87 | bind(stmt, entity);
88 | total += stmt.executeUpdateDelete();
89 | }
90 | return total;
91 | } finally {
92 | release(stmt);
93 | }
94 | }
95 |
96 | /**
97 | * Deletes or updates the given entities in the database and returns the affected row count.
98 | *
99 | * @param entities Entities to delete or update
100 | * @return The number of affected rows
101 | */
102 | public final int handleMultiple(T[] entities) {
103 | final SupportSQLiteStatement stmt = acquire();
104 | try {
105 | int total = 0;
106 | for (T entity : entities) {
107 | bind(stmt, entity);
108 | total += stmt.executeUpdateDelete();
109 | }
110 | return total;
111 | } finally {
112 | release(stmt);
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/EntityInsertionAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.annotation.RestrictTo;
20 | import androidx.sqlite.db.SupportSQLiteStatement;
21 |
22 | import java.util.ArrayList;
23 | import java.util.Collection;
24 | import java.util.List;
25 |
26 | /**
27 | * Implementations of this class knows how to insert a particular entity.
28 | *
29 | * This is an internal library class and all of its implementations are auto-generated.
30 | *
31 | * @param The type parameter of the entity to be inserted
32 | * @hide
33 | */
34 | @SuppressWarnings({"WeakerAccess", "unused"})
35 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
36 | public abstract class EntityInsertionAdapter extends SharedSQLiteStatement {
37 | /**
38 | * Creates an InsertionAdapter that can insert the entity type T into the given database.
39 | *
40 | * @param database The database to insert into.
41 | */
42 | public EntityInsertionAdapter(RoomDatabase database) {
43 | super(database);
44 | }
45 |
46 | /**
47 | * Binds the entity into the given statement.
48 | *
49 | * @param statement The SQLite statement that prepared for the query returned from
50 | * createInsertQuery.
51 | * @param entity The entity of type T.
52 | */
53 | protected abstract void bind(SupportSQLiteStatement statement, T entity);
54 |
55 | /**
56 | * Inserts the entity into the database.
57 | *
58 | * @param entity The entity to insert
59 | */
60 | public final void insert(T entity) {
61 | final SupportSQLiteStatement stmt = acquire();
62 | try {
63 | bind(stmt, entity);
64 | stmt.executeInsert();
65 | } finally {
66 | release(stmt);
67 | }
68 | }
69 |
70 | /**
71 | * Inserts the given entities into the database.
72 | *
73 | * @param entities Entities to insert
74 | */
75 | public final void insert(T[] entities) {
76 | final SupportSQLiteStatement stmt = acquire();
77 | try {
78 | for (T entity : entities) {
79 | bind(stmt, entity);
80 | stmt.executeInsert();
81 | }
82 | } finally {
83 | release(stmt);
84 | }
85 | }
86 |
87 | /**
88 | * Inserts the given entities into the database.
89 | *
90 | * @param entities Entities to insert
91 | */
92 | public final void insert(Iterable extends T> entities) {
93 | final SupportSQLiteStatement stmt = acquire();
94 | try {
95 | for (T entity : entities) {
96 | bind(stmt, entity);
97 | stmt.executeInsert();
98 | }
99 | } finally {
100 | release(stmt);
101 | }
102 | }
103 |
104 | /**
105 | * Inserts the given entity into the database and returns the row id.
106 | *
107 | * @param entity The entity to insert
108 | * @return The SQLite row id or -1 if no row is inserted
109 | */
110 | public final long insertAndReturnId(T entity) {
111 | final SupportSQLiteStatement stmt = acquire();
112 | try {
113 | bind(stmt, entity);
114 | return stmt.executeInsert();
115 | } finally {
116 | release(stmt);
117 | }
118 | }
119 |
120 | /**
121 | * Inserts the given entities into the database and returns the row ids.
122 | *
123 | * @param entities Entities to insert
124 | * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
125 | */
126 | public final long[] insertAndReturnIdsArray(Collection extends T> entities) {
127 | final SupportSQLiteStatement stmt = acquire();
128 | try {
129 | final long[] result = new long[entities.size()];
130 | int index = 0;
131 | for (T entity : entities) {
132 | bind(stmt, entity);
133 | result[index] = stmt.executeInsert();
134 | index++;
135 | }
136 | return result;
137 | } finally {
138 | release(stmt);
139 | }
140 | }
141 |
142 | /**
143 | * Inserts the given entities into the database and returns the row ids.
144 | *
145 | * @param entities Entities to insert
146 | * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
147 | */
148 | public final long[] insertAndReturnIdsArray(T[] entities) {
149 | final SupportSQLiteStatement stmt = acquire();
150 | try {
151 | final long[] result = new long[entities.length];
152 | int index = 0;
153 | for (T entity : entities) {
154 | bind(stmt, entity);
155 | result[index] = stmt.executeInsert();
156 | index++;
157 | }
158 | return result;
159 | } finally {
160 | release(stmt);
161 | }
162 | }
163 |
164 | /**
165 | * Inserts the given entities into the database and returns the row ids.
166 | *
167 | * @param entities Entities to insert
168 | * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
169 | */
170 | public final Long[] insertAndReturnIdsArrayBox(Collection extends T> entities) {
171 | final SupportSQLiteStatement stmt = acquire();
172 | try {
173 | final Long[] result = new Long[entities.size()];
174 | int index = 0;
175 | for (T entity : entities) {
176 | bind(stmt, entity);
177 | result[index] = stmt.executeInsert();
178 | index++;
179 | }
180 | return result;
181 | } finally {
182 | release(stmt);
183 | }
184 | }
185 |
186 | /**
187 | * Inserts the given entities into the database and returns the row ids.
188 | *
189 | * @param entities Entities to insert
190 | * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
191 | */
192 | public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
193 | final SupportSQLiteStatement stmt = acquire();
194 | try {
195 | final Long[] result = new Long[entities.length];
196 | int index = 0;
197 | for (T entity : entities) {
198 | bind(stmt, entity);
199 | result[index] = stmt.executeInsert();
200 | index++;
201 | }
202 | return result;
203 | } finally {
204 | release(stmt);
205 | }
206 | }
207 |
208 | /**
209 | * Inserts the given entities into the database and returns the row ids.
210 | *
211 | * @param entities Entities to insert
212 | * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
213 | */
214 | public final List insertAndReturnIdsList(T[] entities) {
215 | final SupportSQLiteStatement stmt = acquire();
216 | try {
217 | final List result = new ArrayList<>(entities.length);
218 | int index = 0;
219 | for (T entity : entities) {
220 | bind(stmt, entity);
221 | result.add(index, stmt.executeInsert());
222 | index++;
223 | }
224 | return result;
225 | } finally {
226 | release(stmt);
227 | }
228 | }
229 |
230 | /**
231 | * Inserts the given entities into the database and returns the row ids.
232 | *
233 | * @param entities Entities to insert
234 | * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
235 | */
236 | public final List insertAndReturnIdsList(Collection extends T> entities) {
237 | final SupportSQLiteStatement stmt = acquire();
238 | try {
239 | final List result = new ArrayList<>(entities.size());
240 | int index = 0;
241 | for (T entity : entities) {
242 | bind(stmt, entity);
243 | result.add(index, stmt.executeInsert());
244 | index++;
245 | }
246 | return result;
247 | } finally {
248 | release(stmt);
249 | }
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.annotation.VisibleForTesting;
20 | import androidx.lifecycle.LiveData;
21 |
22 | import java.util.Collections;
23 | import java.util.IdentityHashMap;
24 | import java.util.Set;
25 | import java.util.concurrent.Callable;
26 |
27 | /**
28 | * A helper class that maintains {@link RoomTrackingLiveData} instances for an
29 | * {@link InvalidationTracker}.
30 | *
31 | * We keep a strong reference to active LiveData instances to avoid garbage collection in case
32 | * developer does not hold onto the returned LiveData.
33 | */
34 | class InvalidationLiveDataContainer {
35 | @SuppressWarnings("WeakerAccess")
36 | @VisibleForTesting
37 | final Set mLiveDataSet = Collections.newSetFromMap(
38 | new IdentityHashMap()
39 | );
40 | private final RoomDatabase mDatabase;
41 |
42 | InvalidationLiveDataContainer(RoomDatabase database) {
43 | mDatabase = database;
44 | }
45 |
46 | LiveData create(String[] tableNames, boolean inTransaction,
47 | Callable computeFunction) {
48 | return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
49 | tableNames);
50 | }
51 |
52 | void onActive(LiveData liveData) {
53 | mLiveDataSet.add(liveData);
54 | }
55 |
56 | void onInactive(LiveData liveData) {
57 | mLiveDataSet.remove(liveData);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import android.content.ComponentName;
20 | import android.content.Context;
21 | import android.content.Intent;
22 | import android.content.ServiceConnection;
23 | import android.os.IBinder;
24 | import android.os.RemoteException;
25 | import android.util.Log;
26 |
27 | import androidx.annotation.NonNull;
28 | import androidx.annotation.Nullable;
29 |
30 | import java.util.Set;
31 | import java.util.concurrent.Executor;
32 | import java.util.concurrent.atomic.AtomicBoolean;
33 |
34 | /**
35 | * Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
36 | * {@link MultiInstanceInvalidationService}.
37 | */
38 | class MultiInstanceInvalidationClient {
39 |
40 | /**
41 | * The application context.
42 | */
43 | // synthetic access
44 | @SuppressWarnings("WeakerAccess")
45 | final Context mAppContext;
46 |
47 | /**
48 | * The name of the database file.
49 | */
50 | // synthetic access
51 | @SuppressWarnings("WeakerAccess")
52 | final String mName;
53 |
54 | /**
55 | * The client ID assigned by {@link MultiInstanceInvalidationService}.
56 | */
57 | // synthetic access
58 | @SuppressWarnings("WeakerAccess")
59 | int mClientId;
60 |
61 | // synthetic access
62 | @SuppressWarnings("WeakerAccess")
63 | final InvalidationTracker mInvalidationTracker;
64 |
65 | // synthetic access
66 | @SuppressWarnings("WeakerAccess")
67 | final InvalidationTracker.Observer mObserver;
68 |
69 | // synthetic access
70 | @SuppressWarnings("WeakerAccess")
71 | @Nullable
72 | IMultiInstanceInvalidationService mService;
73 |
74 | // synthetic access
75 | @SuppressWarnings("WeakerAccess")
76 | final Executor mExecutor;
77 |
78 | // synthetic access
79 | @SuppressWarnings("WeakerAccess")
80 | final IMultiInstanceInvalidationCallback mCallback =
81 | new IMultiInstanceInvalidationCallback.Stub() {
82 | @Override
83 | public void onInvalidation(final String[] tables) {
84 | mExecutor.execute(new Runnable() {
85 | @Override
86 | public void run() {
87 | mInvalidationTracker.notifyObserversByTableNames(tables);
88 | }
89 | });
90 | }
91 | };
92 |
93 | // synthetic access
94 | @SuppressWarnings("WeakerAccess")
95 | final AtomicBoolean mStopped = new AtomicBoolean(false);
96 |
97 | // synthetic access
98 | @SuppressWarnings("WeakerAccess")
99 | final ServiceConnection mServiceConnection = new ServiceConnection() {
100 |
101 | @Override
102 | public void onServiceConnected(ComponentName name, IBinder service) {
103 | mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
104 | mExecutor.execute(mSetUpRunnable);
105 | }
106 |
107 | @Override
108 | public void onServiceDisconnected(ComponentName name) {
109 | mExecutor.execute(mRemoveObserverRunnable);
110 | mService = null;
111 | }
112 |
113 | };
114 |
115 | // synthetic access
116 | @SuppressWarnings("WeakerAccess")
117 | final Runnable mSetUpRunnable = new Runnable() {
118 | @Override
119 | public void run() {
120 | try {
121 | final IMultiInstanceInvalidationService service = mService;
122 | if (service != null) {
123 | mClientId = service.registerCallback(mCallback, mName);
124 | mInvalidationTracker.addObserver(mObserver);
125 | }
126 | } catch (RemoteException e) {
127 | Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
128 | }
129 | }
130 | };
131 |
132 | // synthetic access
133 | @SuppressWarnings("WeakerAccess")
134 | final Runnable mRemoveObserverRunnable = new Runnable() {
135 | @Override
136 | public void run() {
137 | mInvalidationTracker.removeObserver(mObserver);
138 | }
139 | };
140 |
141 | private final Runnable mTearDownRunnable = new Runnable() {
142 | @Override
143 | public void run() {
144 | mInvalidationTracker.removeObserver(mObserver);
145 | try {
146 | final IMultiInstanceInvalidationService service = mService;
147 | if (service != null) {
148 | service.unregisterCallback(mCallback, mClientId);
149 | }
150 | } catch (RemoteException e) {
151 | Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
152 | }
153 | mAppContext.unbindService(mServiceConnection);
154 | }
155 | };
156 |
157 | /**
158 | * @param context The Context to be used for binding
159 | * {@link IMultiInstanceInvalidationService}.
160 | * @param name The name of the database file.
161 | * @param invalidationTracker The {@link InvalidationTracker}
162 | * @param executor The background executor.
163 | */
164 | MultiInstanceInvalidationClient(Context context, String name,
165 | InvalidationTracker invalidationTracker, Executor executor) {
166 | mAppContext = context.getApplicationContext();
167 | mName = name;
168 | mInvalidationTracker = invalidationTracker;
169 | mExecutor = executor;
170 | mObserver = new InvalidationTracker.Observer(invalidationTracker.mTableNames) {
171 | @Override
172 | public void onInvalidated(@NonNull Set tables) {
173 | if (mStopped.get()) {
174 | return;
175 | }
176 | try {
177 | final IMultiInstanceInvalidationService service = mService;
178 | if (service != null) {
179 | service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
180 | }
181 | } catch (RemoteException e) {
182 | Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
183 | }
184 | }
185 |
186 | @Override
187 | boolean isRemote() {
188 | return true;
189 | }
190 | };
191 | Intent intent = new Intent(mAppContext, MultiInstanceInvalidationService.class);
192 | mAppContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
193 | }
194 |
195 | void stop() {
196 | if (mStopped.compareAndSet(false, true)) {
197 | mExecutor.execute(mTearDownRunnable);
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import android.app.Service;
20 | import android.content.Intent;
21 | import android.os.IBinder;
22 | import android.os.RemoteCallbackList;
23 | import android.os.RemoteException;
24 | import android.util.Log;
25 |
26 | import androidx.annotation.Nullable;
27 | import androidx.annotation.RestrictTo;
28 |
29 | import java.util.HashMap;
30 |
31 | /**
32 | * A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
33 | * This service runs in the main app process. All the instances of {@link InvalidationTracker}
34 | * (potentially in other processes) has to connect to this service.
35 | *
36 | * @hide
37 | */
38 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
39 | public class MultiInstanceInvalidationService extends Service {
40 |
41 | // synthetic access
42 | @SuppressWarnings("WeakerAccess")
43 | int mMaxClientId = 0;
44 |
45 | // synthetic access
46 | @SuppressWarnings("WeakerAccess")
47 | final HashMap mClientNames = new HashMap<>();
48 |
49 | // synthetic access
50 | @SuppressWarnings("WeakerAccess")
51 | final RemoteCallbackList mCallbackList =
52 | new RemoteCallbackList() {
53 | @Override
54 | public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
55 | Object cookie) {
56 | mClientNames.remove((int) cookie);
57 | }
58 | };
59 |
60 | private final IMultiInstanceInvalidationService.Stub mBinder =
61 | new IMultiInstanceInvalidationService.Stub() {
62 |
63 | // Assigns a client ID to the client.
64 | @Override
65 | public int registerCallback(IMultiInstanceInvalidationCallback callback,
66 | String name) {
67 | if (name == null) {
68 | return 0;
69 | }
70 | synchronized (mCallbackList) {
71 | int clientId = ++mMaxClientId;
72 | // Use the client ID as the RemoteCallbackList cookie.
73 | if (mCallbackList.register(callback, clientId)) {
74 | mClientNames.put(clientId, name);
75 | return clientId;
76 | } else {
77 | --mMaxClientId;
78 | return 0;
79 | }
80 | }
81 | }
82 |
83 | // Explicitly removes the client.
84 | // The client can die without calling this. In that case, mCallbackList
85 | // .onCallbackDied() can take care of removal.
86 | @Override
87 | public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
88 | int clientId) {
89 | synchronized (mCallbackList) {
90 | mCallbackList.unregister(callback);
91 | mClientNames.remove(clientId);
92 | }
93 | }
94 |
95 | // Broadcasts table invalidation to other instances of the same database file.
96 | // The broadcast is not sent to the caller itself.
97 | @Override
98 | public void broadcastInvalidation(int clientId, String[] tables) {
99 | synchronized (mCallbackList) {
100 | String name = mClientNames.get(clientId);
101 | if (name == null) {
102 | Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
103 | return;
104 | }
105 | int count = mCallbackList.beginBroadcast();
106 | try {
107 | for (int i = 0; i < count; i++) {
108 | int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
109 | String targetName = mClientNames.get(targetClientId);
110 | if (clientId == targetClientId // This is the caller itself.
111 | || !name.equals(targetName)) { // Not the same file.
112 | continue;
113 | }
114 | try {
115 | IMultiInstanceInvalidationCallback callback =
116 | mCallbackList.getBroadcastItem(i);
117 | callback.onInvalidation(tables);
118 | } catch (RemoteException e) {
119 | Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
120 | }
121 | }
122 | } finally {
123 | mCallbackList.finishBroadcast();
124 | }
125 | }
126 | }
127 | };
128 |
129 | @Nullable
130 | @Override
131 | public IBinder onBind(Intent intent) {
132 | return mBinder;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/Room.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import android.content.Context;
20 |
21 | import androidx.annotation.NonNull;
22 |
23 | /**
24 | * Utility class for Room.
25 | */
26 | @SuppressWarnings("unused")
27 | public class Room {
28 | static final String LOG_TAG = "ROOM";
29 | /**
30 | * The master table where room keeps its metadata information.
31 | */
32 | public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
33 | private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
34 |
35 | /**
36 | * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
37 | * should keep a reference to it and re-use it.
38 | *
39 | * @param context The context for the database. This is usually the Application context.
40 | * @param klass The abstract class which is annotated with {@link Database} and extends
41 | * {@link RoomDatabase}.
42 | * @param name The name of the database file.
43 | * @param The type of the database class.
44 | * @return A {@code RoomDatabaseBuilder} which you can use to create the database.
45 | */
46 | @SuppressWarnings("WeakerAccess")
47 | @NonNull
48 | public static RoomDatabase.Builder databaseBuilder(
49 | @NonNull Context context, @NonNull Class klass, @NonNull String name) {
50 | //noinspection ConstantConditions
51 | if (name == null || name.trim().length() == 0) {
52 | throw new IllegalArgumentException("Cannot build a database with null or empty name."
53 | + " If you are trying to create an in memory database, use Room"
54 | + ".inMemoryDatabaseBuilder");
55 | }
56 | return new RoomDatabase.Builder<>(context, klass, name);
57 | }
58 |
59 | /**
60 | * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
61 | * database disappears when the process is killed.
62 | * Once a database is built, you should keep a reference to it and re-use it.
63 | *
64 | * @param context The context for the database. This is usually the Application context.
65 | * @param klass The abstract class which is annotated with {@link Database} and extends
66 | * {@link RoomDatabase}.
67 | * @param The type of the database class.
68 | * @return A {@code RoomDatabaseBuilder} which you can use to create the database.
69 | */
70 | @NonNull
71 | public static RoomDatabase.Builder inMemoryDatabaseBuilder(
72 | @NonNull Context context, @NonNull Class klass) {
73 | return new RoomDatabase.Builder<>(context, klass, null);
74 | }
75 |
76 | @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
77 | @NonNull
78 | static T getGeneratedImplementation(Class klass, String suffix) {
79 | final String fullPackage = klass.getPackage().getName();
80 | String name = klass.getCanonicalName();
81 | final String postPackageName = fullPackage.isEmpty()
82 | ? name
83 | : (name.substring(fullPackage.length() + 1));
84 | final String implName = postPackageName.replace('.', '_') + suffix;
85 | //noinspection TryWithIdenticalCatches
86 | try {
87 |
88 | @SuppressWarnings("unchecked")
89 | final Class aClass = (Class) Class.forName(
90 | fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
91 | return aClass.newInstance();
92 | } catch (ClassNotFoundException e) {
93 | throw new RuntimeException("cannot find implementation for "
94 | + klass.getCanonicalName() + ". " + implName + " does not exist");
95 | } catch (IllegalAccessException e) {
96 | throw new RuntimeException("Cannot access the constructor"
97 | + klass.getCanonicalName());
98 | } catch (InstantiationException e) {
99 | throw new RuntimeException("Failed to create an instance of "
100 | + klass.getCanonicalName());
101 | }
102 | }
103 |
104 | /** @deprecated This type should not be instantiated as it contains only static methods. */
105 | @Deprecated
106 | @SuppressWarnings("PrivateConstructorForUtilityClass")
107 | public Room() {
108 | }
109 |
110 | static RoomDatabase.Factory databaseFactory = clazz -> null;
111 |
112 | public static void setFactory(@NonNull RoomDatabase.Factory factory) {
113 | databaseFactory = factory;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/RoomOpenHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import android.database.Cursor;
20 |
21 | import androidx.annotation.NonNull;
22 | import androidx.annotation.Nullable;
23 | import androidx.annotation.RestrictTo;
24 | import androidx.room.migration.Migration;
25 | import androidx.sqlite.db.SimpleSQLiteQuery;
26 | import androidx.sqlite.db.SupportSQLiteDatabase;
27 | import androidx.sqlite.db.SupportSQLiteOpenHelper;
28 |
29 | import java.util.List;
30 |
31 | /**
32 | * An open helper that holds a reference to the configuration until the database is opened.
33 | *
34 | * @hide
35 | */
36 | @SuppressWarnings("unused")
37 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
38 | public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
39 | @Nullable
40 | private DatabaseConfiguration mConfiguration;
41 | @NonNull
42 | private final Delegate mDelegate;
43 | @NonNull
44 | private final String mIdentityHash;
45 | /**
46 | * Room v1 had a bug where the hash was not consistent if fields are reordered.
47 | * The new has fixes it but we still need to accept the legacy hash.
48 | */
49 | @NonNull // b/64290754
50 | private final String mLegacyHash;
51 |
52 | public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
53 | @NonNull String identityHash, @NonNull String legacyHash) {
54 | super(delegate.version);
55 | mConfiguration = configuration;
56 | mDelegate = delegate;
57 | mIdentityHash = identityHash;
58 | mLegacyHash = legacyHash;
59 | }
60 |
61 | public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
62 | @NonNull String legacyHash) {
63 | this(configuration, delegate, "", legacyHash);
64 | }
65 |
66 | @Override
67 | public void onConfigure(SupportSQLiteDatabase db) {
68 | super.onConfigure(db);
69 | }
70 |
71 | @Override
72 | public void onCreate(SupportSQLiteDatabase db) {
73 | boolean isEmptyDatabase = hasEmptySchema(db);
74 | mDelegate.createAllTables(db);
75 | if (!isEmptyDatabase) {
76 | // A 0 version pre-populated database goes through the create path because the
77 | // framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
78 | // find the database not to be empty, then it is a pre-populated, we must validate it to
79 | // see if its suitable for usage.
80 | ValidationResult result = mDelegate.onValidateSchema(db);
81 | if (!result.isValid) {
82 | throw new IllegalStateException("Pre-packaged database has an invalid schema: "
83 | + result.expectedFoundMsg);
84 | }
85 | }
86 | updateIdentity(db);
87 | mDelegate.onCreate(db);
88 | }
89 |
90 | @Override
91 | public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
92 | boolean migrated = false;
93 | if (mConfiguration != null) {
94 | List migrations = mConfiguration.migrationContainer.findMigrationPath(
95 | oldVersion, newVersion);
96 | if (migrations != null) {
97 | mDelegate.onPreMigrate(db);
98 | for (Migration migration : migrations) {
99 | migration.migrate(db);
100 | }
101 | ValidationResult result = mDelegate.onValidateSchema(db);
102 | if (!result.isValid) {
103 | throw new IllegalStateException("Migration didn't properly handle: "
104 | + result.expectedFoundMsg);
105 | }
106 | mDelegate.onPostMigrate(db);
107 | updateIdentity(db);
108 | migrated = true;
109 | }
110 | }
111 | if (!migrated) {
112 | if (mConfiguration != null
113 | && !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
114 | mDelegate.dropAllTables(db);
115 | mDelegate.createAllTables(db);
116 | } else {
117 | throw new IllegalStateException("A migration from " + oldVersion + " to "
118 | + newVersion + " was required but not found. Please provide the "
119 | + "necessary Migration path via "
120 | + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
121 | + "destructive migrations via one of the "
122 | + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
123 | }
124 | }
125 | }
126 |
127 | @Override
128 | public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
129 | onUpgrade(db, oldVersion, newVersion);
130 | }
131 |
132 | @Override
133 | public void onOpen(SupportSQLiteDatabase db) {
134 | super.onOpen(db);
135 | checkIdentity(db);
136 | mDelegate.onOpen(db);
137 | // there might be too many configurations etc, just clear it.
138 | mConfiguration = null;
139 | }
140 |
141 | private void checkIdentity(SupportSQLiteDatabase db) {
142 | if (hasRoomMasterTable(db)) {
143 | String identityHash = null;
144 | Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
145 | //noinspection TryFinallyCanBeTryWithResources
146 | try {
147 | if (cursor.moveToFirst()) {
148 | identityHash = cursor.getString(0);
149 | }
150 | } finally {
151 | cursor.close();
152 | }
153 | if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
154 | throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
155 | + " you've changed schema but forgot to update the version number. You can"
156 | + " simply fix this by increasing the version number.");
157 | }
158 | } else {
159 | // No room_master_table, this might an a pre-populated DB, we must validate to see if
160 | // its suitable for usage.
161 | ValidationResult result = mDelegate.onValidateSchema(db);
162 | if (!result.isValid) {
163 | throw new IllegalStateException("Pre-packaged database has an invalid schema: "
164 | + result.expectedFoundMsg);
165 | }
166 | mDelegate.onPostMigrate(db);
167 | updateIdentity(db);
168 | }
169 | }
170 |
171 | private void updateIdentity(SupportSQLiteDatabase db) {
172 | createMasterTableIfNotExists(db);
173 | db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
174 | }
175 |
176 | private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
177 | db.execSQL(RoomMasterTable.CREATE_QUERY);
178 | }
179 |
180 | private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
181 | Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
182 | + RoomMasterTable.TABLE_NAME + "'");
183 | //noinspection TryFinallyCanBeTryWithResources
184 | try {
185 | return cursor.moveToFirst() && cursor.getInt(0) != 0;
186 | } finally {
187 | cursor.close();
188 | }
189 | }
190 |
191 | private static boolean hasEmptySchema(SupportSQLiteDatabase db) {
192 | Cursor cursor = db.query(
193 | "SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'");
194 | //noinspection TryFinallyCanBeTryWithResources
195 | try {
196 | return cursor.moveToFirst() && cursor.getInt(0) == 0;
197 | } finally {
198 | cursor.close();
199 | }
200 | }
201 |
202 | /**
203 | * @hide
204 | */
205 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
206 | public abstract static class Delegate {
207 | public final int version;
208 |
209 | public Delegate(int version) {
210 | this.version = version;
211 | }
212 |
213 | protected abstract void dropAllTables(SupportSQLiteDatabase database);
214 |
215 | protected abstract void createAllTables(SupportSQLiteDatabase database);
216 |
217 | protected abstract void onOpen(SupportSQLiteDatabase database);
218 |
219 | protected abstract void onCreate(SupportSQLiteDatabase database);
220 |
221 | /**
222 | * Called after a migration run to validate database integrity.
223 | *
224 | * @param db The SQLite database.
225 | *
226 | * @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)}
227 | */
228 | @Deprecated
229 | protected void validateMigration(SupportSQLiteDatabase db) {
230 | throw new UnsupportedOperationException("validateMigration is deprecated");
231 | }
232 |
233 | /**
234 | * Called after a migration run or pre-package database copy to validate database integrity.
235 | *
236 | * @param db The SQLite database.
237 | */
238 | @SuppressWarnings("deprecation")
239 | @NonNull
240 | protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) {
241 | validateMigration(db);
242 | return new ValidationResult(true, null);
243 | }
244 |
245 | /**
246 | * Called before migrations execute to perform preliminary work.
247 | * @param database The SQLite database.
248 | */
249 | protected void onPreMigrate(SupportSQLiteDatabase database) {
250 |
251 | }
252 |
253 | /**
254 | * Called after migrations execute to perform additional work.
255 | * @param database The SQLite database.
256 | */
257 | protected void onPostMigrate(SupportSQLiteDatabase database) {
258 |
259 | }
260 | }
261 |
262 | /**
263 | * @hide
264 | */
265 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
266 | public static class ValidationResult {
267 |
268 | public final boolean isValid;
269 | @Nullable
270 | public final String expectedFoundMsg;
271 |
272 | public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) {
273 | this.isValid = isValid;
274 | this.expectedFoundMsg = expectedFoundMsg;
275 | }
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/RoomSQLiteQuery.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.annotation.IntDef;
20 | import androidx.annotation.RestrictTo;
21 | import androidx.annotation.VisibleForTesting;
22 | import androidx.sqlite.db.SupportSQLiteProgram;
23 | import androidx.sqlite.db.SupportSQLiteQuery;
24 |
25 | import java.lang.annotation.Retention;
26 | import java.lang.annotation.RetentionPolicy;
27 | import java.util.Arrays;
28 | import java.util.Iterator;
29 | import java.util.Map;
30 | import java.util.TreeMap;
31 |
32 | /**
33 | * This class is used as an intermediate place to keep binding arguments so that we can run
34 | * Cursor queries with correct types rather than passing everything as a string.
35 | *
36 | * Because it is relatively a big object, they are pooled and must be released after each use.
37 | *
38 | * @hide
39 | */
40 | @SuppressWarnings("unused")
41 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
42 | public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
43 | @SuppressWarnings("WeakerAccess")
44 | @VisibleForTesting
45 | // Maximum number of queries we'll keep cached.
46 | static final int POOL_LIMIT = 15;
47 | @SuppressWarnings("WeakerAccess")
48 | @VisibleForTesting
49 | // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
50 | // clear the bigger queries (# of arguments).
51 | static final int DESIRED_POOL_SIZE = 10;
52 | private volatile String mQuery;
53 | @SuppressWarnings("WeakerAccess")
54 | @VisibleForTesting
55 | final long[] mLongBindings;
56 | @SuppressWarnings("WeakerAccess")
57 | @VisibleForTesting
58 | final double[] mDoubleBindings;
59 | @SuppressWarnings("WeakerAccess")
60 | @VisibleForTesting
61 | final String[] mStringBindings;
62 | @SuppressWarnings("WeakerAccess")
63 | @VisibleForTesting
64 | final byte[][] mBlobBindings;
65 |
66 | @Binding
67 | private final int[] mBindingTypes;
68 | @SuppressWarnings("WeakerAccess")
69 | @VisibleForTesting
70 | final int mCapacity;
71 | // number of arguments in the query
72 | @SuppressWarnings("WeakerAccess")
73 | @VisibleForTesting
74 | int mArgCount;
75 |
76 |
77 | @SuppressWarnings("WeakerAccess")
78 | @VisibleForTesting
79 | static final TreeMap sQueryPool = new TreeMap<>();
80 |
81 | /**
82 | * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
83 | *
84 | * @param supportSQLiteQuery The query to copy from
85 | * @return A new query copied from the provided one.
86 | */
87 | public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
88 | final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
89 | supportSQLiteQuery.getSql(),
90 | supportSQLiteQuery.getArgCount());
91 | supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
92 | @Override
93 | public void bindNull(int index) {
94 | query.bindNull(index);
95 | }
96 |
97 | @Override
98 | public void bindLong(int index, long value) {
99 | query.bindLong(index, value);
100 | }
101 |
102 | @Override
103 | public void bindDouble(int index, double value) {
104 | query.bindDouble(index, value);
105 | }
106 |
107 | @Override
108 | public void bindString(int index, String value) {
109 | query.bindString(index, value);
110 | }
111 |
112 | @Override
113 | public void bindBlob(int index, byte[] value) {
114 | query.bindBlob(index, value);
115 | }
116 |
117 | @Override
118 | public void clearBindings() {
119 | query.clearBindings();
120 | }
121 |
122 | @Override
123 | public void close() {
124 | // ignored.
125 | }
126 | });
127 | return query;
128 | }
129 |
130 | /**
131 | * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
132 | * given query.
133 | *
134 | * @param query The query to prepare
135 | * @param argumentCount The number of query arguments
136 | * @return A RoomSQLiteQuery that holds the given query and has space for the given number of
137 | * arguments.
138 | */
139 | @SuppressWarnings("WeakerAccess")
140 | public static RoomSQLiteQuery acquire(String query, int argumentCount) {
141 | synchronized (sQueryPool) {
142 | final Map.Entry entry =
143 | sQueryPool.ceilingEntry(argumentCount);
144 | if (entry != null) {
145 | sQueryPool.remove(entry.getKey());
146 | final RoomSQLiteQuery sqliteQuery = entry.getValue();
147 | sqliteQuery.init(query, argumentCount);
148 | return sqliteQuery;
149 | }
150 | }
151 | RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
152 | sqLiteQuery.init(query, argumentCount);
153 | return sqLiteQuery;
154 | }
155 |
156 | private RoomSQLiteQuery(int capacity) {
157 | mCapacity = capacity;
158 | // because, 1 based indices... we don't want to offsets everything with 1 all the time.
159 | int limit = capacity + 1;
160 | //noinspection WrongConstant
161 | mBindingTypes = new int[limit];
162 | mLongBindings = new long[limit];
163 | mDoubleBindings = new double[limit];
164 | mStringBindings = new String[limit];
165 | mBlobBindings = new byte[limit][];
166 | }
167 |
168 | @SuppressWarnings("WeakerAccess")
169 | void init(String query, int argCount) {
170 | mQuery = query;
171 | mArgCount = argCount;
172 | }
173 |
174 | /**
175 | * Releases the query back to the pool.
176 | *
177 | * After released, the statement might be returned when {@link #acquire(String, int)} is called
178 | * so you should never re-use it after releasing.
179 | */
180 | @SuppressWarnings("WeakerAccess")
181 | public void release() {
182 | synchronized (sQueryPool) {
183 | sQueryPool.put(mCapacity, this);
184 | prunePoolLocked();
185 | }
186 | }
187 |
188 | private static void prunePoolLocked() {
189 | if (sQueryPool.size() > POOL_LIMIT) {
190 | int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
191 | final Iterator iterator = sQueryPool.descendingKeySet().iterator();
192 | while (toBeRemoved-- > 0) {
193 | iterator.next();
194 | iterator.remove();
195 | }
196 | }
197 | }
198 |
199 | @Override
200 | public String getSql() {
201 | return mQuery;
202 | }
203 |
204 | @Override
205 | public int getArgCount() {
206 | return mArgCount;
207 | }
208 |
209 | @Override
210 | public void bindTo(SupportSQLiteProgram program) {
211 | for (int index = 1; index <= mArgCount; index++) {
212 | switch (mBindingTypes[index]) {
213 | case NULL:
214 | program.bindNull(index);
215 | break;
216 | case LONG:
217 | program.bindLong(index, mLongBindings[index]);
218 | break;
219 | case DOUBLE:
220 | program.bindDouble(index, mDoubleBindings[index]);
221 | break;
222 | case STRING:
223 | program.bindString(index, mStringBindings[index]);
224 | break;
225 | case BLOB:
226 | program.bindBlob(index, mBlobBindings[index]);
227 | break;
228 | }
229 | }
230 | }
231 |
232 | @Override
233 | public void bindNull(int index) {
234 | mBindingTypes[index] = NULL;
235 | }
236 |
237 | @Override
238 | public void bindLong(int index, long value) {
239 | mBindingTypes[index] = LONG;
240 | mLongBindings[index] = value;
241 | }
242 |
243 | @Override
244 | public void bindDouble(int index, double value) {
245 | mBindingTypes[index] = DOUBLE;
246 | mDoubleBindings[index] = value;
247 | }
248 |
249 | @Override
250 | public void bindString(int index, String value) {
251 | mBindingTypes[index] = STRING;
252 | mStringBindings[index] = value;
253 | }
254 |
255 | @Override
256 | public void bindBlob(int index, byte[] value) {
257 | mBindingTypes[index] = BLOB;
258 | mBlobBindings[index] = value;
259 | }
260 |
261 | @Override
262 | public void close() {
263 | // no-op. not calling release because it is internal API.
264 | }
265 |
266 | /**
267 | * Copies arguments from another RoomSQLiteQuery into this query.
268 | *
269 | * @param other The other query, which holds the arguments to be copied.
270 | */
271 | public void copyArgumentsFrom(RoomSQLiteQuery other) {
272 | int argCount = other.getArgCount() + 1; // +1 for the binding offsets
273 | System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
274 | System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
275 | System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
276 | System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
277 | System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
278 | }
279 |
280 | @Override
281 | public void clearBindings() {
282 | Arrays.fill(mBindingTypes, NULL);
283 | Arrays.fill(mStringBindings, null);
284 | Arrays.fill(mBlobBindings, null);
285 | mQuery = null;
286 | // no need to clear others
287 | }
288 |
289 | private static final int NULL = 1;
290 | private static final int LONG = 2;
291 | private static final int DOUBLE = 3;
292 | private static final int STRING = 4;
293 | private static final int BLOB = 5;
294 |
295 | @Retention(RetentionPolicy.SOURCE)
296 | @IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
297 | @interface Binding {
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 |
20 | import android.annotation.SuppressLint;
21 |
22 | import androidx.annotation.MainThread;
23 | import androidx.annotation.NonNull;
24 | import androidx.annotation.WorkerThread;
25 | import androidx.arch.core.executor.ArchTaskExecutor;
26 | import androidx.lifecycle.LiveData;
27 |
28 | import java.util.Set;
29 | import java.util.concurrent.Callable;
30 | import java.util.concurrent.Executor;
31 | import java.util.concurrent.atomic.AtomicBoolean;
32 |
33 | /**
34 | * A LiveData implementation that closely works with {@link InvalidationTracker} to implement
35 | * database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
36 | * as they are active.
37 | *
38 | * We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
39 | * observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
40 | * memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
41 | * {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
42 | * extra query on every UI rotation.
43 | *
44 | * This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
45 | * strongly by the {@link InvalidationTracker} as long as it is active.
46 | */
47 | class RoomTrackingLiveData extends LiveData {
48 | @SuppressWarnings("WeakerAccess")
49 | final RoomDatabase mDatabase;
50 |
51 | @SuppressWarnings("WeakerAccess")
52 | final boolean mInTransaction;
53 |
54 | @SuppressWarnings("WeakerAccess")
55 | final Callable mComputeFunction;
56 |
57 | private final InvalidationLiveDataContainer mContainer;
58 |
59 | @SuppressWarnings("WeakerAccess")
60 | final InvalidationTracker.Observer mObserver;
61 |
62 | @SuppressWarnings("WeakerAccess")
63 | final AtomicBoolean mInvalid = new AtomicBoolean(true);
64 |
65 | @SuppressWarnings("WeakerAccess")
66 | final AtomicBoolean mComputing = new AtomicBoolean(false);
67 |
68 | @SuppressWarnings("WeakerAccess")
69 | final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
70 |
71 | @SuppressWarnings("WeakerAccess")
72 | final Runnable mRefreshRunnable = new Runnable() {
73 | @WorkerThread
74 | @Override
75 | public void run() {
76 | if (mRegisteredObserver.compareAndSet(false, true)) {
77 | mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
78 | }
79 | boolean computed;
80 | do {
81 | computed = false;
82 | // compute can happen only in 1 thread but no reason to lock others.
83 | if (mComputing.compareAndSet(false, true)) {
84 | // as long as it is invalid, keep computing.
85 | try {
86 | T value = null;
87 | while (mInvalid.compareAndSet(true, false)) {
88 | computed = true;
89 | try {
90 | value = mComputeFunction.call();
91 | } catch (Exception e) {
92 | throw new RuntimeException("Exception while computing database"
93 | + " live data.", e);
94 | }
95 | }
96 | if (computed) {
97 | postValue(value);
98 | }
99 | } finally {
100 | // release compute lock
101 | mComputing.set(false);
102 | }
103 | }
104 | // check invalid after releasing compute lock to avoid the following scenario.
105 | // Thread A runs compute()
106 | // Thread A checks invalid, it is false
107 | // Main thread sets invalid to true
108 | // Thread B runs, fails to acquire compute lock and skips
109 | // Thread A releases compute lock
110 | // We've left invalid in set state. The check below recovers.
111 | } while (computed && mInvalid.get());
112 | }
113 | };
114 |
115 | @SuppressWarnings("WeakerAccess")
116 | final Runnable mInvalidationRunnable = new Runnable() {
117 | @MainThread
118 | @Override
119 | public void run() {
120 | boolean isActive = hasActiveObservers();
121 | if (mInvalid.compareAndSet(false, true)) {
122 | if (isActive) {
123 | getQueryExecutor().execute(mRefreshRunnable);
124 | }
125 | }
126 | }
127 | };
128 | @SuppressLint("RestrictedApi")
129 | RoomTrackingLiveData(
130 | RoomDatabase database,
131 | InvalidationLiveDataContainer container,
132 | boolean inTransaction,
133 | Callable computeFunction,
134 | String[] tableNames) {
135 | mDatabase = database;
136 | mInTransaction = inTransaction;
137 | mComputeFunction = computeFunction;
138 | mContainer = container;
139 | mObserver = new InvalidationTracker.Observer(tableNames) {
140 | @Override
141 | public void onInvalidated(@NonNull Set tables) {
142 | ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
143 | }
144 | };
145 | }
146 |
147 | @Override
148 | protected void onActive() {
149 | super.onActive();
150 | mContainer.onActive(this);
151 | getQueryExecutor().execute(mRefreshRunnable);
152 | }
153 |
154 | @Override
155 | protected void onInactive() {
156 | super.onInactive();
157 | mContainer.onInactive(this);
158 | }
159 |
160 | Executor getQueryExecutor() {
161 | if (mInTransaction) {
162 | return mDatabase.getTransactionExecutor();
163 | } else {
164 | return mDatabase.getQueryExecutor();
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import android.content.Context;
20 | import android.os.Build;
21 | import android.util.Log;
22 |
23 | import androidx.annotation.NonNull;
24 | import androidx.annotation.Nullable;
25 | import androidx.annotation.RequiresApi;
26 | import androidx.room.util.CopyLock;
27 | import androidx.room.util.DBUtil;
28 | import androidx.room.util.FileUtil;
29 | import androidx.sqlite.db.SupportSQLiteDatabase;
30 | import androidx.sqlite.db.SupportSQLiteOpenHelper;
31 |
32 | import java.io.File;
33 | import java.io.FileInputStream;
34 | import java.io.FileOutputStream;
35 | import java.io.IOException;
36 | import java.nio.channels.Channels;
37 | import java.nio.channels.FileChannel;
38 | import java.nio.channels.ReadableByteChannel;
39 |
40 | /**
41 | * An open helper that will copy & open a pre-populated database if it doesn't exists in internal
42 | * storage.
43 | */
44 | class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
45 |
46 | @NonNull
47 | private final Context mContext;
48 | @Nullable
49 | private final String mCopyFromAssetPath;
50 | @Nullable
51 | private final File mCopyFromFile;
52 | private final int mDatabaseVersion;
53 | @NonNull
54 | private final SupportSQLiteOpenHelper mDelegate;
55 | @Nullable
56 | private DatabaseConfiguration mDatabaseConfiguration;
57 |
58 | private boolean mVerified;
59 |
60 | SQLiteCopyOpenHelper(
61 | @NonNull Context context,
62 | @Nullable String copyFromAssetPath,
63 | @Nullable File copyFromFile,
64 | int databaseVersion,
65 | @NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
66 | mContext = context;
67 | mCopyFromAssetPath = copyFromAssetPath;
68 | mCopyFromFile = copyFromFile;
69 | mDatabaseVersion = databaseVersion;
70 | mDelegate = supportSQLiteOpenHelper;
71 | }
72 |
73 | @Override
74 | public String getDatabaseName() {
75 | return mDelegate.getDatabaseName();
76 | }
77 |
78 | @Override
79 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
80 | public void setWriteAheadLoggingEnabled(boolean enabled) {
81 | mDelegate.setWriteAheadLoggingEnabled(enabled);
82 | }
83 |
84 | @Override
85 | public synchronized SupportSQLiteDatabase getWritableDatabase() {
86 | if (!mVerified) {
87 | verifyDatabaseFile();
88 | mVerified = true;
89 | }
90 | return mDelegate.getWritableDatabase();
91 | }
92 |
93 | @Override
94 | public synchronized SupportSQLiteDatabase getReadableDatabase() {
95 | if (!mVerified) {
96 | verifyDatabaseFile();
97 | mVerified = true;
98 | }
99 | return mDelegate.getReadableDatabase();
100 | }
101 |
102 | @Override
103 | public synchronized void close() {
104 | mDelegate.close();
105 | mVerified = false;
106 | }
107 |
108 | // Can't be constructor param because the factory is needed by the database builder which in
109 | // turn is the one that actually builds the configuration.
110 | void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) {
111 | mDatabaseConfiguration = databaseConfiguration;
112 | }
113 |
114 | private void verifyDatabaseFile() {
115 | String databaseName = getDatabaseName();
116 | File databaseFile = mContext.getDatabasePath(databaseName);
117 | boolean processLevelLock = mDatabaseConfiguration == null
118 | || mDatabaseConfiguration.multiInstanceInvalidation;
119 | CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock);
120 | try {
121 | // Acquire a copy lock, this lock works across threads and processes, preventing
122 | // concurrent copy attempts from occurring.
123 | copyLock.lock();
124 |
125 | if (!databaseFile.exists()) {
126 | try {
127 | // No database file found, copy and be done.
128 | copyDatabaseFile(databaseFile);
129 | return;
130 | } catch (IOException e) {
131 | throw new RuntimeException("Unable to copy database file.", e);
132 | }
133 | }
134 |
135 | if (mDatabaseConfiguration == null) {
136 | return;
137 | }
138 |
139 | // A database file is present, check if we need to re-copy it.
140 | int currentVersion;
141 | try {
142 | currentVersion = DBUtil.readVersion(databaseFile);
143 | } catch (IOException e) {
144 | Log.w(Room.LOG_TAG, "Unable to read database version.", e);
145 | return;
146 | }
147 |
148 | if (currentVersion == mDatabaseVersion) {
149 | return;
150 | }
151 |
152 | if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) {
153 | // From the current version to the desired version a migration is required, i.e.
154 | // we won't be performing a copy destructive migration.
155 | return;
156 | }
157 |
158 | if (mContext.deleteDatabase(databaseName)) {
159 | try {
160 | copyDatabaseFile(databaseFile);
161 | } catch (IOException e) {
162 | // We are more forgiving copying a database on a destructive migration since
163 | // there is already a database file that can be opened.
164 | Log.w(Room.LOG_TAG, "Unable to copy database file.", e);
165 | }
166 | } else {
167 | Log.w(Room.LOG_TAG, "Failed to delete database file ("
168 | + databaseName + ") for a copy destructive migration.");
169 | }
170 | } finally {
171 | copyLock.unlock();
172 | }
173 | }
174 |
175 | private void copyDatabaseFile(File destinationFile) throws IOException {
176 | ReadableByteChannel input;
177 | if (mCopyFromAssetPath != null) {
178 | input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
179 | } else if (mCopyFromFile != null) {
180 | input = new FileInputStream(mCopyFromFile).getChannel();
181 | } else {
182 | throw new IllegalStateException("copyFromAssetPath and copyFromFile == null!");
183 | }
184 |
185 | // An intermediate file is used so that we never end up with a half-copied database file
186 | // in the internal directory.
187 | File intermediateFile = File.createTempFile(
188 | "room-copy-helper", ".tmp", mContext.getCacheDir());
189 | intermediateFile.deleteOnExit();
190 | FileChannel output = new FileOutputStream(intermediateFile).getChannel();
191 | FileUtil.copy(input, output);
192 |
193 | File parent = destinationFile.getParentFile();
194 | if (parent != null && !parent.exists() && !parent.mkdirs()) {
195 | throw new IOException("Failed to create directories for "
196 | + destinationFile.getAbsolutePath());
197 | }
198 |
199 | if (!intermediateFile.renameTo(destinationFile)) {
200 | throw new IOException("Failed to move intermediate file ("
201 | + intermediateFile.getAbsolutePath() + ") to destination ("
202 | + destinationFile.getAbsolutePath() + ").");
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.annotation.Nullable;
21 | import androidx.sqlite.db.SupportSQLiteOpenHelper;
22 |
23 | import java.io.File;
24 |
25 | /**
26 | * Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
27 | * {@link SQLiteCopyOpenHelper}.
28 | */
29 | class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
30 |
31 | @Nullable
32 | private final String mCopyFromAssetPath;
33 | @Nullable
34 | private final File mCopyFromFile;
35 | @NonNull
36 | private final SupportSQLiteOpenHelper.Factory mDelegate;
37 |
38 | SQLiteCopyOpenHelperFactory(
39 | @Nullable String copyFromAssetPath,
40 | @Nullable File copyFromFile,
41 | @NonNull SupportSQLiteOpenHelper.Factory factory) {
42 | mCopyFromAssetPath = copyFromAssetPath;
43 | mCopyFromFile = copyFromFile;
44 | mDelegate = factory;
45 | }
46 |
47 | @Override
48 | public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
49 | return new SQLiteCopyOpenHelper(
50 | configuration.context,
51 | mCopyFromAssetPath,
52 | mCopyFromFile,
53 | configuration.callback.version,
54 | mDelegate.create(configuration));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/SharedSQLiteStatement.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package androidx.room;
17 |
18 | import androidx.annotation.RestrictTo;
19 | import androidx.sqlite.db.SupportSQLiteStatement;
20 |
21 | import java.util.concurrent.atomic.AtomicBoolean;
22 |
23 | /**
24 | * Represents a prepared SQLite state that can be re-used multiple times.
25 | *
26 | * This class is used by generated code. After it is used, {@code release} must be called so that
27 | * it can be used by other threads.
28 | *
29 | * To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
30 | * statement until it is released.
31 | *
32 | * @hide
33 | */
34 | @SuppressWarnings({"WeakerAccess", "unused"})
35 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
36 | public abstract class SharedSQLiteStatement {
37 | private final AtomicBoolean mLock = new AtomicBoolean(false);
38 |
39 | private final RoomDatabase mDatabase;
40 | private volatile SupportSQLiteStatement mStmt;
41 |
42 | /**
43 | * Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
44 | * it automatically creates a new one.
45 | *
46 | * @param database The database to create the statement in.
47 | */
48 | public SharedSQLiteStatement(RoomDatabase database) {
49 | mDatabase = database;
50 | }
51 |
52 | /**
53 | * Create the query.
54 | *
55 | * @return The SQL query to prepare.
56 | */
57 | protected abstract String createQuery();
58 |
59 | protected void assertNotMainThread() {
60 | mDatabase.assertNotMainThread();
61 | }
62 |
63 | private SupportSQLiteStatement createNewStatement() {
64 | String query = createQuery();
65 | return mDatabase.compileStatement(query);
66 | }
67 |
68 | private SupportSQLiteStatement getStmt(boolean canUseCached) {
69 | final SupportSQLiteStatement stmt;
70 | if (canUseCached) {
71 | if (mStmt == null) {
72 | mStmt = createNewStatement();
73 | }
74 | stmt = mStmt;
75 | } else {
76 | // it is in use, create a one off statement
77 | stmt = createNewStatement();
78 | }
79 | return stmt;
80 | }
81 |
82 | /**
83 | * Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
84 | */
85 | public SupportSQLiteStatement acquire() {
86 | assertNotMainThread();
87 | return getStmt(mLock.compareAndSet(false, true));
88 | }
89 |
90 | /**
91 | * Must call this when statement will not be used anymore.
92 | *
93 | * @param statement The statement that was returned from acquire.
94 | */
95 | public void release(SupportSQLiteStatement statement) {
96 | if (statement == mStmt) {
97 | mLock.set(false);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/TransactionExecutor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room;
18 |
19 | import androidx.annotation.NonNull;
20 |
21 | import java.util.ArrayDeque;
22 | import java.util.concurrent.Executor;
23 |
24 | /**
25 | * Executor wrapper for performing database transactions serially.
26 | *
27 | * Since database transactions are exclusive, this executor ensures that transactions are performed
28 | * in-order and one at a time, preventing threads from blocking each other when multiple concurrent
29 | * transactions are attempted.
30 | */
31 | class TransactionExecutor implements Executor {
32 |
33 | private final Executor mExecutor;
34 | private final ArrayDeque mTasks = new ArrayDeque<>();
35 | private Runnable mActive;
36 |
37 | TransactionExecutor(@NonNull Executor executor) {
38 | mExecutor = executor;
39 | }
40 |
41 | public synchronized void execute(final Runnable command) {
42 | mTasks.offer(new Runnable() {
43 | public void run() {
44 | try {
45 | command.run();
46 | } finally {
47 | scheduleNext();
48 | }
49 | }
50 | });
51 | if (mActive == null) {
52 | scheduleNext();
53 | }
54 | }
55 |
56 | @SuppressWarnings("WeakerAccess")
57 | synchronized void scheduleNext() {
58 | if ((mActive = mTasks.poll()) != null) {
59 | mExecutor.execute(mActive);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/migration/Migration.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.migration;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.sqlite.db.SupportSQLiteDatabase;
21 |
22 | /**
23 | * Base class for a database migration.
24 | *
25 | * Each migration can move between 2 versions that are defined by {@link #startVersion} and
26 | * {@link #endVersion}.
27 | *
28 | * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
29 | * going version 3 to 5 without going to version 4). If Room opens a database at version
30 | * 3 and latest version is >= 5, Room will use the migration object that can migrate from
31 | * 3 to 5 instead of 3 to 4 and 4 to 5.
32 | *
33 | * If there are not enough migrations provided to move from the current version to the latest
34 | * version, Room will clear the database and recreate so even if you have no changes between 2
35 | * versions, you should still provide a Migration object to the builder.
36 | */
37 | public abstract class Migration {
38 | public final int startVersion;
39 | public final int endVersion;
40 |
41 | /**
42 | * Creates a new migration between {@code startVersion} and {@code endVersion}.
43 | *
44 | * @param startVersion The start version of the database.
45 | * @param endVersion The end version of the database after this migration is applied.
46 | */
47 | public Migration(int startVersion, int endVersion) {
48 | this.startVersion = startVersion;
49 | this.endVersion = endVersion;
50 | }
51 |
52 | /**
53 | * Should run the necessary migrations.
54 | *
55 | * This class cannot access any generated Dao in this method.
56 | *
57 | * This method is already called inside a transaction and that transaction might actually be a
58 | * composite transaction of all necessary {@code Migration}s.
59 | *
60 | * @param database The database instance
61 | */
62 | public abstract void migrate(@NonNull SupportSQLiteDatabase database);
63 | }
64 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /**
18 | * Room is a Database Object Mapping library that makes it easy to access database on Android
19 | * applications.
20 | *
21 | * Rather than hiding the details of SQLite, Room tries to embrace them by providing convenient APIs
22 | * to query the database and also verify such queries at compile time. This allows you to access
23 | * the full power of SQLite while having the type safety provided by Java SQL query builders.
24 | *
25 | * There are 3 major components in Room.
26 | *
27 | * - {@link androidx.room.Database Database}: This annotation marks a class as a database.
28 | * It should be an abstract class that extends {@link androidx.room.RoomDatabase RoomDatabase}.
29 | * At runtime, you can acquire an instance of it via {@link androidx.room.Room#databaseBuilder(
30 | * android.content.Context,java.lang.Class, java.lang.String) Room.databaseBuilder} or
31 | * {@link androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context, java.lang.Class)
32 | * Room.inMemoryDatabaseBuilder}.
33 | *
34 | * The database class defines the list of entities and data access objects in the database.
35 | * It is also the main access point for the underlying connection.
36 | *
37 | * - {@link androidx.room.Entity Entity}: This annotation marks a class as a database row.
38 | * For each {@link androidx.room.Entity Entity}, a database table is created to hold the items.
39 | * The Entity class must be referenced in the
40 | * {@link androidx.room.Database#entities() Database#entities} array. Each field of the Entity
41 | * (and its super class) is persisted in the database unless it is denoted otherwise
42 | * (see {@link androidx.room.Entity Entity} docs for details).
43 | *
44 | * - {@link androidx.room.Dao Dao}: This annotation marks a class or interface as a
45 | * Data Access Object. Data access objects are the main components of Room that are
46 | * responsible for defining the methods that access the database. The class that is annotated
47 | * with {@link androidx.room.Database Database} must have an abstract method that has 0
48 | * arguments and returns the class that is annotated with Dao. While generating the code at
49 | * compile time, Room will generate an implementation of this class.
50 | *
51 | * Using Dao classes for database access rather than query builders or direct queries allows you
52 | * to keep a separation between different components and easily mock the database access while
53 | * testing your application.
54 | *
55 | *
56 | * Below is a sample of a simple database.
57 | *
58 | * // File: Song.java
59 | * {@literal @}Entity
60 | * public class User {
61 | * {@literal @}PrimaryKey
62 | * private int id;
63 | * private String name;
64 | * {@literal @}ColumnInfo(name = "release_year")
65 | * private int releaseYear;
66 | * // getters and setters are ignored for brevity but they are required for Room to work.
67 | * }
68 | * // File: SongDao.java
69 | * {@literal @}Dao
70 | * public interface SongDao {
71 | * {@literal @}Query("SELECT * FROM song")
72 | * List<Song> loadAll();
73 | * {@literal @}Query("SELECT * FROM song WHERE id IN (:songIds)")
74 | * List<Song> loadAllBySongId(int... songIds);
75 | * {@literal @}Query("SELECT * FROM song WHERE name LIKE :name AND release_year = :year LIMIT 1")
76 | * Song loadOneByNameAndReleaseYear(String first, int year);
77 | * {@literal @}Insert
78 | * void insertAll(Song... songs);
79 | * {@literal @}Delete
80 | * void delete(Song song);
81 | * }
82 | * // File: MusicDatabase.java
83 | * {@literal @}Database(entities = {Song.java})
84 | * public abstract class MusicDatabase extends RoomDatabase {
85 | * public abstract SongDao userDao();
86 | * }
87 | *
88 | * You can create an instance of {@code MusicDatabase} as follows:
89 | *
90 | * MusicDatabase db = Room
91 | * .databaseBuilder(getApplicationContext(), MusicDatabase.class, "database-name")
92 | * .build();
93 | *
94 | * Since Room verifies your queries at compile time, it also detects information about which tables
95 | * are accessed by the query or what columns are present in the response.
96 | *
97 | * You can observe a particular table for changes using the
98 | * {@link androidx.room.InvalidationTracker InvalidationTracker} class which you can acquire via
99 | * {@link androidx.room.RoomDatabase#getInvalidationTracker()
100 | * RoomDatabase.getInvalidationTracker}.
101 | *
102 | * For convenience, Room allows you to return {@link androidx.lifecycle.LiveData LiveData} from
103 | * {@link androidx.room.Query Query} methods. It will automatically observe the related tables as
104 | * long as the {@code LiveData} has active observers.
105 | *
106 | * // This live data will automatically dispatch changes as the database changes.
107 | * {@literal @}Query("SELECT * FROM song ORDER BY name LIMIT 5")
108 | * LiveData<Song> loadFirstFiveSongs();
109 | *
110 | *
111 | * You can also return arbitrary data objects from your query results as long as the fields in the
112 | * object match the list of columns in the query response. This makes it very easy to write
113 | * applications that drive the UI from persistent storage.
114 | *
115 | * class IdAndSongHeader {
116 | * int id;
117 | * {@literal @}ColumnInfo(name = "header")
118 | * String header;
119 | * }
120 | * // DAO
121 | * {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM user")
122 | * public IdAndSongHeader[] loadSongHeaders();
123 | *
124 | * If there is a mismatch between the query result and the POJO, Room will print a warning during
125 | * compilation.
126 | *
127 | * Please see the documentation of individual classes for details.
128 | */
129 | package androidx.room;
130 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.paging;
18 |
19 | import android.database.Cursor;
20 |
21 | import androidx.annotation.NonNull;
22 | import androidx.annotation.RestrictTo;
23 | import androidx.paging.PositionalDataSource;
24 | import androidx.room.InvalidationTracker;
25 | import androidx.room.RoomDatabase;
26 | import androidx.room.RoomSQLiteQuery;
27 | import androidx.sqlite.db.SupportSQLiteQuery;
28 |
29 | import java.util.Collections;
30 | import java.util.List;
31 | import java.util.Set;
32 |
33 | /**
34 | * A simple data source implementation that uses Limit & Offset to page the query.
35 | *
36 | * This is NOT the most efficient way to do paging on SQLite. It is
37 | * recommended to use an indexed
38 | * ORDER BY statement but that requires a more complex API. This solution is technically equal to
39 | * receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
40 | * never returns inconsistent data if it is invalidated.
41 | *
42 | * @param Data type returned by the data source.
43 | *
44 | * @hide
45 | */
46 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
47 | public abstract class LimitOffsetDataSource extends PositionalDataSource {
48 | private final RoomSQLiteQuery mSourceQuery;
49 | private final String mCountQuery;
50 | private final String mLimitOffsetQuery;
51 | private final RoomDatabase mDb;
52 | @SuppressWarnings("FieldCanBeLocal")
53 | private final InvalidationTracker.Observer mObserver;
54 | private final boolean mInTransaction;
55 |
56 | protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
57 | boolean inTransaction, String... tables) {
58 | this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, tables);
59 | }
60 |
61 | protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
62 | boolean inTransaction, String... tables) {
63 | mDb = db;
64 | mSourceQuery = query;
65 | mInTransaction = inTransaction;
66 | mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
67 | mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
68 | mObserver = new InvalidationTracker.Observer(tables) {
69 | @Override
70 | public void onInvalidated(@NonNull Set tables) {
71 | invalidate();
72 | }
73 | };
74 | db.getInvalidationTracker().addWeakObserver(mObserver);
75 | }
76 |
77 | /**
78 | * Count number of rows query can return
79 | *
80 | * @hide
81 | */
82 | @SuppressWarnings("WeakerAccess")
83 | public int countItems() {
84 | final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
85 | mSourceQuery.getArgCount());
86 | sqLiteQuery.copyArgumentsFrom(mSourceQuery);
87 | Cursor cursor = mDb.query(sqLiteQuery);
88 | try {
89 | if (cursor.moveToFirst()) {
90 | return cursor.getInt(0);
91 | }
92 | return 0;
93 | } finally {
94 | cursor.close();
95 | sqLiteQuery.release();
96 | }
97 | }
98 |
99 | @Override
100 | public boolean isInvalid() {
101 | mDb.getInvalidationTracker().refreshVersionsSync();
102 | return super.isInvalid();
103 | }
104 |
105 | @SuppressWarnings("WeakerAccess")
106 | protected abstract List convertRows(Cursor cursor);
107 |
108 | @SuppressWarnings("deprecation")
109 | @Override
110 | public void loadInitial(@NonNull LoadInitialParams params,
111 | @NonNull LoadInitialCallback callback) {
112 | List list = Collections.emptyList();
113 | int totalCount = 0;
114 | int firstLoadPosition = 0;
115 | RoomSQLiteQuery sqLiteQuery = null;
116 | Cursor cursor = null;
117 | mDb.beginTransaction();
118 | try {
119 | totalCount = countItems();
120 | if (totalCount != 0) {
121 | // bound the size requested, based on known count
122 | firstLoadPosition = computeInitialLoadPosition(params, totalCount);
123 | int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
124 |
125 | sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
126 | cursor = mDb.query(sqLiteQuery);
127 | List rows = convertRows(cursor);
128 | mDb.setTransactionSuccessful();
129 | list = rows;
130 | }
131 | } finally {
132 | if (cursor != null) {
133 | cursor.close();
134 | }
135 | mDb.endTransaction();
136 | if (sqLiteQuery != null) {
137 | sqLiteQuery.release();
138 | }
139 | }
140 |
141 | callback.onResult(list, firstLoadPosition, totalCount);
142 | }
143 |
144 | @Override
145 | public void loadRange(@NonNull LoadRangeParams params,
146 | @NonNull LoadRangeCallback callback) {
147 | callback.onResult(loadRange(params.startPosition, params.loadSize));
148 | }
149 |
150 | /**
151 | * Return the rows from startPos to startPos + loadCount
152 | *
153 | * @hide
154 | */
155 | @SuppressWarnings("deprecation")
156 | @NonNull
157 | public List loadRange(int startPosition, int loadCount) {
158 | final RoomSQLiteQuery sqLiteQuery = getSQLiteQuery(startPosition, loadCount);
159 | if (mInTransaction) {
160 | mDb.beginTransaction();
161 | Cursor cursor = null;
162 | //noinspection TryFinallyCanBeTryWithResources
163 | try {
164 | cursor = mDb.query(sqLiteQuery);
165 | List rows = convertRows(cursor);
166 | mDb.setTransactionSuccessful();
167 | return rows;
168 | } finally {
169 | if (cursor != null) {
170 | cursor.close();
171 | }
172 | mDb.endTransaction();
173 | sqLiteQuery.release();
174 | }
175 | } else {
176 | Cursor cursor = mDb.query(sqLiteQuery);
177 | //noinspection TryFinallyCanBeTryWithResources
178 | try {
179 | return convertRows(cursor);
180 | } finally {
181 | cursor.close();
182 | sqLiteQuery.release();
183 | }
184 | }
185 | }
186 |
187 | private RoomSQLiteQuery getSQLiteQuery(int startPosition, int loadCount) {
188 | final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
189 | mSourceQuery.getArgCount() + 2);
190 | sqLiteQuery.copyArgumentsFrom(mSourceQuery);
191 | sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
192 | sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
193 | return sqLiteQuery;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/CopyLock.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.annotation.RestrictTo;
21 |
22 | import java.io.File;
23 | import java.io.FileOutputStream;
24 | import java.io.IOException;
25 | import java.nio.channels.FileChannel;
26 | import java.util.HashMap;
27 | import java.util.Map;
28 | import java.util.concurrent.locks.Lock;
29 | import java.util.concurrent.locks.ReentrantLock;
30 |
31 | /**
32 | * Utility class for in-process and multi-process key-based lock mechanism for safely copying
33 | * database files.
34 | *
35 | * Acquiring the lock will be quick if no other thread or process has a lock with the same key.
36 | * But if the lock is already held then acquiring it will block, until the other thread or process
37 | * releases the lock. Note that the key and lock directory must be the same to achieve
38 | * synchronization.
39 | *
40 | * Locking is done via two levels:
41 | *
42 | * -
43 | * Thread locking within the same JVM process is done via a map of String key to ReentrantLock
44 | * objects.
45 | *
-
46 | * Multi-process locking is done via a dummy file whose name contains the key and FileLock
47 | * objects.
48 | *
49 | * @hide
50 | */
51 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
52 | public class CopyLock {
53 |
54 | // in-process lock map
55 | private static final Map sThreadLocks = new HashMap<>();
56 |
57 | private final File mCopyLockFile;
58 | private final Lock mThreadLock;
59 | private final boolean mFileLevelLock;
60 | private FileChannel mLockChannel;
61 |
62 | /**
63 | * Creates a lock with {@code name} and using {@code lockDir} as the directory for the
64 | * lock files.
65 | * @param name the name of this lock.
66 | * @param lockDir the directory where the lock files will be located.
67 | * @param processLock whether to use file for process level locking or not.
68 | */
69 | public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) {
70 | mCopyLockFile = new File(lockDir, name + ".lck");
71 | mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath());
72 | mFileLevelLock = processLock;
73 | }
74 |
75 | /**
76 | * Attempts to grab the lock, blocking if already held by another thread or process.
77 | */
78 | public void lock() {
79 | mThreadLock.lock();
80 | if (mFileLevelLock) {
81 | try {
82 | mLockChannel = new FileOutputStream(mCopyLockFile).getChannel();
83 | mLockChannel.lock();
84 | } catch (IOException e) {
85 | throw new IllegalStateException("Unable to grab copy lock.", e);
86 | }
87 | }
88 | }
89 |
90 | /**
91 | * Releases the lock.
92 | */
93 | public void unlock() {
94 | if (mLockChannel != null) {
95 | try {
96 | mLockChannel.close();
97 | } catch (IOException ignored) { }
98 | }
99 | mThreadLock.unlock();
100 | }
101 |
102 | private static Lock getThreadLock(String key) {
103 | synchronized (sThreadLocks) {
104 | Lock threadLock = sThreadLocks.get(key);
105 | if (threadLock == null) {
106 | threadLock = new ReentrantLock();
107 | sThreadLocks.put(key, threadLock);
108 | }
109 | return threadLock;
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/CursorUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.database.Cursor;
20 | import android.database.MatrixCursor;
21 |
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.RestrictTo;
24 |
25 | /**
26 | * Cursor utilities for Room
27 | *
28 | * @hide
29 | */
30 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
31 | public class CursorUtil {
32 |
33 | /**
34 | * Copies the given cursor into a in-memory cursor and then closes it.
35 | *
36 | * This is useful for iterating over a cursor multiple times without the cost of JNI while
37 | * reading or IO while filling the window at the expense of memory consumption.
38 | *
39 | * @param c the cursor to copy.
40 | * @return a new cursor containing the same data as the given cursor.
41 | */
42 | @NonNull
43 | public static Cursor copyAndClose(@NonNull Cursor c) {
44 | final MatrixCursor matrixCursor;
45 | try {
46 | matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
47 | while (c.moveToNext()) {
48 | final Object[] row = new Object[c.getColumnCount()];
49 | for (int i = 0; i < c.getColumnCount(); i++) {
50 | switch (c.getType(i)) {
51 | case Cursor.FIELD_TYPE_NULL:
52 | row[i] = null;
53 | break;
54 | case Cursor.FIELD_TYPE_INTEGER:
55 | row[i] = c.getLong(i);
56 | break;
57 | case Cursor.FIELD_TYPE_FLOAT:
58 | row[i] = c.getDouble(i);
59 | break;
60 | case Cursor.FIELD_TYPE_STRING:
61 | row[i] = c.getString(i);
62 | break;
63 | case Cursor.FIELD_TYPE_BLOB:
64 | row[i] = c.getBlob(i);
65 | break;
66 | default:
67 | throw new IllegalStateException();
68 | }
69 | }
70 | matrixCursor.addRow(row);
71 | }
72 | } finally {
73 | c.close();
74 | }
75 | return matrixCursor;
76 | }
77 |
78 | /**
79 | * Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
80 | * If the column is not found, it retries with the specified name surrounded by backticks.
81 | *
82 | * @param c The cursor.
83 | * @param name The name of the target column.
84 | * @return The index of the column, or -1 if not found.
85 | */
86 | public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
87 | final int index = c.getColumnIndex(name);
88 | if (index >= 0) {
89 | return index;
90 | }
91 | return c.getColumnIndex("`" + name + "`");
92 | }
93 |
94 | /**
95 | * Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
96 | * If the column is not found, it retries with the specified name surrounded by backticks.
97 | *
98 | * @param c The cursor.
99 | * @param name The name of the target column.
100 | * @return The index of the column.
101 | * @throws IllegalArgumentException if the column does not exist.
102 | */
103 | public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
104 | final int index = c.getColumnIndex(name);
105 | if (index >= 0) {
106 | return index;
107 | }
108 | return c.getColumnIndexOrThrow("`" + name + "`");
109 | }
110 |
111 | private CursorUtil() {
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/DBUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.database.AbstractWindowedCursor;
20 | import android.database.Cursor;
21 | import android.os.Build;
22 | import android.os.CancellationSignal;
23 |
24 | import androidx.annotation.NonNull;
25 | import androidx.annotation.Nullable;
26 | import androidx.annotation.RestrictTo;
27 | import androidx.room.RoomDatabase;
28 | import androidx.sqlite.db.SupportSQLiteDatabase;
29 | import androidx.sqlite.db.SupportSQLiteQuery;
30 |
31 | import java.io.File;
32 | import java.io.FileInputStream;
33 | import java.io.IOException;
34 | import java.nio.ByteBuffer;
35 | import java.nio.channels.FileChannel;
36 | import java.util.ArrayList;
37 | import java.util.List;
38 |
39 | /**
40 | * Database utilities for Room
41 | *
42 | * @hide
43 | */
44 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
45 | public class DBUtil {
46 |
47 | /**
48 | * Performs the SQLiteQuery on the given database.
49 | *
50 | * This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
51 | * {@code true} and either the api level is below a certain threshold or the full result of the
52 | * query does not fit in a single window.
53 | *
54 | * @param db The database to perform the query on.
55 | * @param sqLiteQuery The query to perform.
56 | * @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
57 | * @return Result of the query.
58 | *
59 | * @deprecated This is only used in the generated code and shouldn't be called directly.
60 | */
61 | @Deprecated
62 | @NonNull
63 | public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) {
64 | return query(db, sqLiteQuery, maybeCopy, null);
65 | }
66 |
67 | /**
68 | * Performs the SQLiteQuery on the given database.
69 | *
70 | * This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
71 | * {@code true} and either the api level is below a certain threshold or the full result of the
72 | * query does not fit in a single window.
73 | *
74 | * @param db The database to perform the query on.
75 | * @param sqLiteQuery The query to perform.
76 | * @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
77 | * @param signal The cancellation signal to be attached to the query.
78 | * @return Result of the query.
79 | */
80 | @NonNull
81 | public static Cursor query(@NonNull RoomDatabase db, @NonNull SupportSQLiteQuery sqLiteQuery,
82 | boolean maybeCopy, @Nullable CancellationSignal signal) {
83 | final Cursor cursor = db.query(sqLiteQuery, signal);
84 | if (maybeCopy && cursor instanceof AbstractWindowedCursor) {
85 | AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
86 | int rowsInCursor = windowedCursor.getCount(); // Should fill the window.
87 | int rowsInWindow;
88 | if (windowedCursor.hasWindow()) {
89 | rowsInWindow = windowedCursor.getWindow().getNumRows();
90 | } else {
91 | rowsInWindow = rowsInCursor;
92 | }
93 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
94 | return CursorUtil.copyAndClose(windowedCursor);
95 | }
96 | }
97 |
98 | return cursor;
99 | }
100 |
101 | /**
102 | * Drops all FTS content sync triggers created by Room.
103 | *
104 | * FTS content sync triggers created by Room are those that are found in the sqlite_master table
105 | * who's names start with 'room_fts_content_sync_'.
106 | *
107 | * @param db The database.
108 | */
109 | public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) {
110 | List existingTriggers = new ArrayList<>();
111 | Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'");
112 | //noinspection TryFinallyCanBeTryWithResources
113 | try {
114 | while (cursor.moveToNext()) {
115 | existingTriggers.add(cursor.getString(0));
116 | }
117 | } finally {
118 | cursor.close();
119 | }
120 |
121 | for (String triggerName : existingTriggers) {
122 | if (triggerName.startsWith("room_fts_content_sync_")) {
123 | db.execSQL("DROP TRIGGER IF EXISTS " + triggerName);
124 | }
125 | }
126 | }
127 |
128 | /**
129 | * Reads the user version number out of the database header from the given file.
130 | *
131 | * @param databaseFile the database file.
132 | * @return the database version
133 | * @throws IOException if something goes wrong reading the file, such as bad database header or
134 | * missing permissions.
135 | *
136 | * @see User Version
137 | * Number.
138 | */
139 | public static int readVersion(@NonNull File databaseFile) throws IOException {
140 | FileChannel input = null;
141 | try {
142 | ByteBuffer buffer = ByteBuffer.allocate(4);
143 | input = new FileInputStream(databaseFile).getChannel();
144 | input.tryLock(60, 4, true);
145 | input.position(60);
146 | int read = input.read(buffer);
147 | if (read != 4) {
148 | throw new IOException("Bad database header, unable to read 4 bytes at offset 60");
149 | }
150 | buffer.rewind();
151 | return buffer.getInt(); // ByteBuffer is big-endian by default
152 | } finally {
153 | if (input != null) {
154 | input.close();
155 | }
156 | }
157 | }
158 |
159 | /**
160 | * CancellationSignal is only available from API 16 on. This function will create a new
161 | * instance of the Cancellation signal only if the current API > 16.
162 | *
163 | * @return A new instance of CancellationSignal or null.
164 | */
165 | @Nullable
166 | public static CancellationSignal createCancellationSignal() {
167 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
168 | return new CancellationSignal();
169 | }
170 | return null;
171 | }
172 |
173 | private DBUtil() {
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/FileUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.annotation.SuppressLint;
20 | import android.os.Build;
21 |
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.RestrictTo;
24 |
25 | import java.io.IOException;
26 | import java.io.InputStream;
27 | import java.io.OutputStream;
28 | import java.nio.channels.Channels;
29 | import java.nio.channels.FileChannel;
30 | import java.nio.channels.ReadableByteChannel;
31 |
32 | /**
33 | * File utilities for Room
34 | *
35 | * @hide
36 | */
37 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
38 | public class FileUtil {
39 |
40 | /**
41 | * Copies data from the input channel to the output file channel.
42 | *
43 | * @param input the input channel to copy.
44 | * @param output the output channel to copy.
45 | * @throws IOException if there is an I/O error.
46 | */
47 | @SuppressLint("LambdaLast")
48 | public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
49 | throws IOException {
50 | try {
51 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
52 | output.transferFrom(input, 0, Long.MAX_VALUE);
53 | } else {
54 | InputStream inputStream = Channels.newInputStream(input);
55 | OutputStream outputStream = Channels.newOutputStream(output);
56 | int length;
57 | byte[] buffer = new byte[1024 * 4];
58 | while ((length = inputStream.read(buffer)) > 0) {
59 | outputStream.write(buffer, 0, length);
60 | }
61 | }
62 | output.force(false);
63 | } finally {
64 | input.close();
65 | output.close();
66 | }
67 | }
68 |
69 | private FileUtil() {
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/FtsTableInfo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.database.Cursor;
20 |
21 | import androidx.annotation.RestrictTo;
22 | import androidx.annotation.VisibleForTesting;
23 | import androidx.sqlite.db.SupportSQLiteDatabase;
24 |
25 | import java.util.ArrayDeque;
26 | import java.util.ArrayList;
27 | import java.util.HashSet;
28 | import java.util.List;
29 | import java.util.Set;
30 |
31 | /**
32 | * A data class that holds the information about an FTS table.
33 | *
34 | * @hide
35 | */
36 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
37 | public class FtsTableInfo {
38 |
39 | // A set of valid FTS Options
40 | private static final String[] FTS_OPTIONS = new String[] {
41 | "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
42 | "order=", "prefix=", "uncompress="};
43 |
44 | /**
45 | * The table name
46 | */
47 | public final String name;
48 |
49 | /**
50 | * The column names
51 | */
52 | public final Set columns;
53 |
54 | /**
55 | * The set of options. Each value in the set contains the option in the following format:
56 | * <key>=<value>.
57 | */
58 | public final Set options;
59 |
60 | public FtsTableInfo(String name, Set columns, Set options) {
61 | this.name = name;
62 | this.columns = columns;
63 | this.options = options;
64 | }
65 |
66 | public FtsTableInfo(String name, Set columns, String createSql) {
67 | this.name = name;
68 | this.columns = columns;
69 | this.options = parseOptions(createSql);
70 | }
71 |
72 | /**
73 | * Reads the table information from the given database.
74 | *
75 | * @param database The database to read the information from.
76 | * @param tableName The table name.
77 | * @return A FtsTableInfo containing the columns and options for the provided table name.
78 | */
79 | public static FtsTableInfo read(SupportSQLiteDatabase database, String tableName) {
80 | Set columns = readColumns(database, tableName);
81 | Set options = readOptions(database, tableName);
82 |
83 | return new FtsTableInfo(tableName, columns, options);
84 | }
85 |
86 | @SuppressWarnings("TryFinallyCanBeTryWithResources")
87 | private static Set readColumns(SupportSQLiteDatabase database, String tableName) {
88 | Cursor cursor = database.query("PRAGMA table_info(`" + tableName + "`)");
89 | Set columns = new HashSet<>();
90 | try {
91 | if (cursor.getColumnCount() > 0) {
92 | int nameIndex = cursor.getColumnIndex("name");
93 | while (cursor.moveToNext()) {
94 | columns.add(cursor.getString(nameIndex));
95 | }
96 | }
97 | } finally {
98 | cursor.close();
99 | }
100 | return columns;
101 | }
102 |
103 | @SuppressWarnings("TryFinallyCanBeTryWithResources")
104 | private static Set readOptions(SupportSQLiteDatabase database, String tableName) {
105 | String sql = "";
106 | Cursor cursor = database.query(
107 | "SELECT * FROM sqlite_master WHERE `name` = '" + tableName + "'");
108 | try {
109 | if (cursor.moveToFirst()) {
110 | sql = cursor.getString(cursor.getColumnIndexOrThrow("sql"));
111 | }
112 | } finally {
113 | cursor.close();
114 | }
115 | return parseOptions(sql);
116 | }
117 |
118 | /**
119 | * Parses FTS options from the create statement of an FTS table.
120 | *
121 | * This method assumes the given create statement is a valid well-formed SQLite statement as
122 | * defined in the CREATE VIRTUAL TABLE
123 | * syntax diagram.
124 | *
125 | * @param createStatement the "CREATE VIRTUAL TABLE" statement.
126 | * @return the set of FTS option key and values in the create statement.
127 | */
128 | @VisibleForTesting
129 | @SuppressWarnings("WeakerAccess") /* synthetic access */
130 | static Set parseOptions(String createStatement) {
131 | if (createStatement.isEmpty()) {
132 | return new HashSet<>();
133 | }
134 |
135 | // Module arguments are within the parenthesis followed by the module name.
136 | String argsString = createStatement.substring(
137 | createStatement.indexOf('(') + 1,
138 | createStatement.lastIndexOf(')'));
139 |
140 | // Split the module argument string by the comma delimiter, keeping track of quotation so
141 | // so that if the delimiter is found within a string literal we don't substring at the wrong
142 | // index. SQLite supports four ways of quoting keywords, see:
143 | // https://www.sqlite.org/lang_keywords.html
144 | List args = new ArrayList<>();
145 | ArrayDeque quoteStack = new ArrayDeque<>();
146 | int lastDelimiterIndex = -1;
147 | for (int i = 0; i < argsString.length(); i++) {
148 | char c = argsString.charAt(i);
149 | switch (c) {
150 | case '\'':
151 | case '"':
152 | case '`':
153 | if (quoteStack.isEmpty()) {
154 | quoteStack.push(c);
155 | } else if (quoteStack.peek() == c) {
156 | quoteStack.pop();
157 | }
158 | break;
159 | case '[':
160 | if (quoteStack.isEmpty()) {
161 | quoteStack.push(c);
162 | }
163 | break;
164 | case ']':
165 | if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
166 | quoteStack.pop();
167 | }
168 | break;
169 | case ',':
170 | if (quoteStack.isEmpty()) {
171 | args.add(argsString.substring(lastDelimiterIndex + 1, i).trim());
172 | lastDelimiterIndex = i;
173 | }
174 | break;
175 | }
176 | }
177 | args.add(argsString.substring(lastDelimiterIndex + 1).trim()); // Add final argument.
178 |
179 | // Match args against valid options, otherwise they are column definitions.
180 | HashSet options = new HashSet<>();
181 | for (String arg : args) {
182 | for (String validOption : FTS_OPTIONS) {
183 | if (arg.startsWith(validOption)) {
184 | options.add(arg);
185 | }
186 | }
187 | }
188 |
189 | return options;
190 | }
191 |
192 | @Override
193 | public boolean equals(Object o) {
194 | if (this == o) return true;
195 | if (o == null || getClass() != o.getClass()) return false;
196 |
197 | FtsTableInfo that = (FtsTableInfo) o;
198 |
199 | if (name != null ? !name.equals(that.name) : that.name != null) return false;
200 | if (columns != null ? !columns.equals(that.columns) : that.columns != null) return false;
201 | return options != null ? options.equals(that.options) : that.options == null;
202 | }
203 |
204 | @Override
205 | public int hashCode() {
206 | int result = name != null ? name.hashCode() : 0;
207 | result = 31 * result + (columns != null ? columns.hashCode() : 0);
208 | result = 31 * result + (options != null ? options.hashCode() : 0);
209 | return result;
210 | }
211 |
212 | @Override
213 | public String toString() {
214 | return "FtsTableInfo{"
215 | + "name='" + name + '\''
216 | + ", columns=" + columns
217 | + ", options=" + options
218 | + '}';
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/SneakyThrow.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.annotation.RestrictTo;
21 |
22 | /**
23 | * Java 8 Sneaky Throw technique.
24 | *
25 | * @hide
26 | */
27 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
28 | public class SneakyThrow {
29 |
30 | /**
31 | * Re-throws a checked exception as if it was a runtime exception without wrapping it.
32 | *
33 | * @param e the exception to re-throw.
34 | */
35 | public static void reThrow(@NonNull Exception e) {
36 | sneakyThrow(e);
37 | }
38 |
39 | @SuppressWarnings("unchecked")
40 | private static void sneakyThrow(@NonNull Throwable e) throws E {
41 | throw (E) e;
42 | }
43 |
44 | private SneakyThrow() {
45 |
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/StringUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.util.Log;
20 |
21 | import androidx.annotation.Nullable;
22 | import androidx.annotation.RestrictTo;
23 |
24 | import java.util.ArrayList;
25 | import java.util.List;
26 | import java.util.StringTokenizer;
27 |
28 | /**
29 | * @hide
30 | *
31 | * String utilities for Room
32 | */
33 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
34 | public class StringUtil {
35 |
36 | @SuppressWarnings("unused")
37 | public static final String[] EMPTY_STRING_ARRAY = new String[0];
38 | /**
39 | * Returns a new StringBuilder to be used while producing SQL queries.
40 | *
41 | * @return A new or recycled StringBuilder
42 | */
43 | public static StringBuilder newStringBuilder() {
44 | // TODO pool:
45 | return new StringBuilder();
46 | }
47 |
48 | /**
49 | * Adds bind variable placeholders (?) to the given string. Each placeholder is separated
50 | * by a comma.
51 | *
52 | * @param builder The StringBuilder for the query
53 | * @param count Number of placeholders
54 | */
55 | public static void appendPlaceholders(StringBuilder builder, int count) {
56 | for (int i = 0; i < count; i++) {
57 | builder.append("?");
58 | if (i < count - 1) {
59 | builder.append(",");
60 | }
61 | }
62 | }
63 | /**
64 | * Splits a comma separated list of integers to integer list.
65 | *
66 | * If an input is malformed, it is omitted from the result.
67 | *
68 | * @param input Comma separated list of integers.
69 | * @return A List containing the integers or null if the input is null.
70 | */
71 | @Nullable
72 | public static List splitToIntList(@Nullable String input) {
73 | if (input == null) {
74 | return null;
75 | }
76 | List result = new ArrayList<>();
77 | StringTokenizer tokenizer = new StringTokenizer(input, ",");
78 | while (tokenizer.hasMoreElements()) {
79 | final String item = tokenizer.nextToken();
80 | try {
81 | result.add(Integer.parseInt(item));
82 | } catch (NumberFormatException ex) {
83 | Log.e("ROOM", "Malformed integer list", ex);
84 | }
85 | }
86 | return result;
87 | }
88 |
89 | /**
90 | * Joins the given list of integers into a comma separated list.
91 | *
92 | * @param input The list of integers.
93 | * @return Comma separated string composed of integers in the list. If the list is null, return
94 | * value is null.
95 | */
96 | @Nullable
97 | public static String joinIntoString(@Nullable List input) {
98 | if (input == null) {
99 | return null;
100 | }
101 |
102 | final int size = input.size();
103 | if (size == 0) {
104 | return "";
105 | }
106 | StringBuilder sb = new StringBuilder();
107 | for (int i = 0; i < size; i++) {
108 | sb.append(Integer.toString(input.get(i)));
109 | if (i < size - 1) {
110 | sb.append(",");
111 | }
112 | }
113 | return sb.toString();
114 | }
115 |
116 | private StringUtil() {
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/TableInfo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.database.Cursor;
20 | import android.os.Build;
21 |
22 | import androidx.annotation.IntDef;
23 | import androidx.annotation.NonNull;
24 | import androidx.annotation.Nullable;
25 | import androidx.annotation.RestrictTo;
26 | import androidx.room.ColumnInfo;
27 | import androidx.sqlite.db.SupportSQLiteDatabase;
28 |
29 | import java.lang.annotation.Retention;
30 | import java.lang.annotation.RetentionPolicy;
31 | import java.util.ArrayList;
32 | import java.util.Collections;
33 | import java.util.HashMap;
34 | import java.util.HashSet;
35 | import java.util.List;
36 | import java.util.Locale;
37 | import java.util.Map;
38 | import java.util.Set;
39 | import java.util.TreeMap;
40 |
41 | /**
42 | * A data class that holds the information about a table.
43 | *
44 | * It directly maps to the result of {@code PRAGMA table_info()}. Check the
45 | * PRAGMA table_info
46 | * documentation for more details.
47 | *
48 | * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
49 | *
50 | * @hide
51 | */
52 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
53 | @SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
54 | "SimplifiableIfStatement"})
55 | // if you change this class, you must change TableInfoWriter.kt
56 | public class TableInfo {
57 |
58 | /**
59 | * Identifies from where the info object was created.
60 | */
61 | @Retention(RetentionPolicy.SOURCE)
62 | @IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
63 | @interface CreatedFrom {
64 | }
65 |
66 | /**
67 | * Identifier for when the info is created from an unknown source.
68 | */
69 | public static final int CREATED_FROM_UNKNOWN = 0;
70 |
71 | /**
72 | * Identifier for when the info is created from an entity definition, such as generated code
73 | * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
74 | */
75 | public static final int CREATED_FROM_ENTITY = 1;
76 |
77 | /**
78 | * Identifier for when the info is created from the database itself, reading information from a
79 | * PRAGMA, such as table_info.
80 | */
81 | public static final int CREATED_FROM_DATABASE = 2;
82 |
83 | /**
84 | * The table name.
85 | */
86 | public final String name;
87 | /**
88 | * Unmodifiable map of columns keyed by column name.
89 | */
90 | public final Map columns;
91 |
92 | public final Set foreignKeys;
93 |
94 | /**
95 | * Sometimes, Index information is not available (older versions). If so, we skip their
96 | * verification.
97 | */
98 | @Nullable
99 | public final Set indices;
100 |
101 | @SuppressWarnings("unused")
102 | public TableInfo(String name, Map columns, Set foreignKeys,
103 | Set indices) {
104 | this.name = name;
105 | this.columns = Collections.unmodifiableMap(columns);
106 | this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
107 | this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
108 | }
109 |
110 | /**
111 | * For backward compatibility with dbs created with older versions.
112 | */
113 | @SuppressWarnings("unused")
114 | public TableInfo(String name, Map columns, Set foreignKeys) {
115 | this(name, columns, foreignKeys, Collections.emptySet());
116 | }
117 |
118 | @Override
119 | public boolean equals(Object o) {
120 | if (this == o) return true;
121 | if (o == null || getClass() != o.getClass()) return false;
122 |
123 | TableInfo tableInfo = (TableInfo) o;
124 |
125 | if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
126 | if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
127 | return false;
128 | }
129 | if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
130 | : tableInfo.foreignKeys != null) {
131 | return false;
132 | }
133 | if (indices == null || tableInfo.indices == null) {
134 | // if one us is missing index information, seems like we couldn't acquire the
135 | // information so we better skip.
136 | return true;
137 | }
138 | return indices.equals(tableInfo.indices);
139 | }
140 |
141 | @Override
142 | public int hashCode() {
143 | int result = name != null ? name.hashCode() : 0;
144 | result = 31 * result + (columns != null ? columns.hashCode() : 0);
145 | result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
146 | // skip index, it is not reliable for comparison.
147 | return result;
148 | }
149 |
150 | @Override
151 | public String toString() {
152 | return "TableInfo{"
153 | + "name='" + name + '\''
154 | + ", columns=" + columns
155 | + ", foreignKeys=" + foreignKeys
156 | + ", indices=" + indices
157 | + '}';
158 | }
159 |
160 | /**
161 | * Reads the table information from the given database.
162 | *
163 | * @param database The database to read the information from.
164 | * @param tableName The table name.
165 | * @return A TableInfo containing the schema information for the provided table name.
166 | */
167 | @SuppressWarnings("SameParameterValue")
168 | public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
169 | Map columns = readColumns(database, tableName);
170 | Set foreignKeys = readForeignKeys(database, tableName);
171 | Set indices = readIndices(database, tableName);
172 | return new TableInfo(tableName, columns, foreignKeys, indices);
173 | }
174 |
175 | private static Set readForeignKeys(SupportSQLiteDatabase database,
176 | String tableName) {
177 | Set foreignKeys = new HashSet<>();
178 | // this seems to return everything in order but it is not documented so better be safe
179 | Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
180 | try {
181 | final int idColumnIndex = cursor.getColumnIndex("id");
182 | final int seqColumnIndex = cursor.getColumnIndex("seq");
183 | final int tableColumnIndex = cursor.getColumnIndex("table");
184 | final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
185 | final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
186 |
187 | final List ordered = readForeignKeyFieldMappings(cursor);
188 | final int count = cursor.getCount();
189 | for (int position = 0; position < count; position++) {
190 | cursor.moveToPosition(position);
191 | final int seq = cursor.getInt(seqColumnIndex);
192 | if (seq != 0) {
193 | continue;
194 | }
195 | final int id = cursor.getInt(idColumnIndex);
196 | List myColumns = new ArrayList<>();
197 | List refColumns = new ArrayList<>();
198 | for (ForeignKeyWithSequence key : ordered) {
199 | if (key.mId == id) {
200 | myColumns.add(key.mFrom);
201 | refColumns.add(key.mTo);
202 | }
203 | }
204 | foreignKeys.add(new ForeignKey(
205 | cursor.getString(tableColumnIndex),
206 | cursor.getString(onDeleteColumnIndex),
207 | cursor.getString(onUpdateColumnIndex),
208 | myColumns,
209 | refColumns
210 | ));
211 | }
212 | } finally {
213 | cursor.close();
214 | }
215 | return foreignKeys;
216 | }
217 |
218 | private static List readForeignKeyFieldMappings(Cursor cursor) {
219 | final int idColumnIndex = cursor.getColumnIndex("id");
220 | final int seqColumnIndex = cursor.getColumnIndex("seq");
221 | final int fromColumnIndex = cursor.getColumnIndex("from");
222 | final int toColumnIndex = cursor.getColumnIndex("to");
223 | final int count = cursor.getCount();
224 | List result = new ArrayList<>();
225 | for (int i = 0; i < count; i++) {
226 | cursor.moveToPosition(i);
227 | result.add(new ForeignKeyWithSequence(
228 | cursor.getInt(idColumnIndex),
229 | cursor.getInt(seqColumnIndex),
230 | cursor.getString(fromColumnIndex),
231 | cursor.getString(toColumnIndex)
232 | ));
233 | }
234 | Collections.sort(result);
235 | return result;
236 | }
237 |
238 | private static Map readColumns(SupportSQLiteDatabase database,
239 | String tableName) {
240 | Cursor cursor = database
241 | .query("PRAGMA table_info(`" + tableName + "`)");
242 | //noinspection TryFinallyCanBeTryWithResources
243 | Map columns = new HashMap<>();
244 | try {
245 | if (cursor.getColumnCount() > 0) {
246 | int nameIndex = cursor.getColumnIndex("name");
247 | int typeIndex = cursor.getColumnIndex("type");
248 | int notNullIndex = cursor.getColumnIndex("notnull");
249 | int pkIndex = cursor.getColumnIndex("pk");
250 | int defaultValueIndex = cursor.getColumnIndex("dflt_value");
251 |
252 | while (cursor.moveToNext()) {
253 | final String name = cursor.getString(nameIndex);
254 | final String type = cursor.getString(typeIndex);
255 | final boolean notNull = 0 != cursor.getInt(notNullIndex);
256 | final int primaryKeyPosition = cursor.getInt(pkIndex);
257 | final String defaultValue = cursor.getString(defaultValueIndex);
258 | columns.put(name,
259 | new Column(name, type, notNull, primaryKeyPosition, defaultValue,
260 | CREATED_FROM_DATABASE));
261 | }
262 | }
263 | } finally {
264 | cursor.close();
265 | }
266 | return columns;
267 | }
268 |
269 | /**
270 | * @return null if we cannot read the indices due to older sqlite implementations.
271 | */
272 | @Nullable
273 | private static Set readIndices(SupportSQLiteDatabase database, String tableName) {
274 | Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
275 | try {
276 | final int nameColumnIndex = cursor.getColumnIndex("name");
277 | final int originColumnIndex = cursor.getColumnIndex("origin");
278 | final int uniqueIndex = cursor.getColumnIndex("unique");
279 | if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
280 | // we cannot read them so better not validate any index.
281 | return null;
282 | }
283 | HashSet indices = new HashSet<>();
284 | while (cursor.moveToNext()) {
285 | String origin = cursor.getString(originColumnIndex);
286 | if (!"c".equals(origin)) {
287 | // Ignore auto-created indices
288 | continue;
289 | }
290 | String name = cursor.getString(nameColumnIndex);
291 | boolean unique = cursor.getInt(uniqueIndex) == 1;
292 | Index index = readIndex(database, name, unique);
293 | if (index == null) {
294 | // we cannot read it properly so better not read it
295 | return null;
296 | }
297 | indices.add(index);
298 | }
299 | return indices;
300 | } finally {
301 | cursor.close();
302 | }
303 | }
304 |
305 | /**
306 | * @return null if we cannot read the index due to older sqlite implementations.
307 | */
308 | @Nullable
309 | private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
310 | Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
311 | try {
312 | final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
313 | final int cidColumnIndex = cursor.getColumnIndex("cid");
314 | final int nameColumnIndex = cursor.getColumnIndex("name");
315 | if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) {
316 | // we cannot read them so better not validate any index.
317 | return null;
318 | }
319 | final TreeMap results = new TreeMap<>();
320 |
321 | while (cursor.moveToNext()) {
322 | int cid = cursor.getInt(cidColumnIndex);
323 | if (cid < 0) {
324 | // Ignore SQLite row ID
325 | continue;
326 | }
327 | int seq = cursor.getInt(seqnoColumnIndex);
328 | String columnName = cursor.getString(nameColumnIndex);
329 | results.put(seq, columnName);
330 | }
331 | final List columns = new ArrayList<>(results.size());
332 | columns.addAll(results.values());
333 | return new Index(name, unique, columns);
334 | } finally {
335 | cursor.close();
336 | }
337 | }
338 |
339 | /**
340 | * Holds the information about a database column.
341 | */
342 | @SuppressWarnings("WeakerAccess")
343 | public static class Column {
344 | /**
345 | * The column name.
346 | */
347 | public final String name;
348 | /**
349 | * The column type affinity.
350 | */
351 | public final String type;
352 | /**
353 | * The column type after it is normalized to one of the basic types according to
354 | * https://www.sqlite.org/datatype3.html Section 3.1.
355 | *
356 | * This is the value Room uses for equality check.
357 | */
358 | @ColumnInfo.SQLiteTypeAffinity
359 | public final int affinity;
360 | /**
361 | * Whether or not the column can be NULL.
362 | */
363 | public final boolean notNull;
364 | /**
365 | * The position of the column in the list of primary keys, 0 if the column is not part
366 | * of the primary key.
367 | *
368 | * This information is only available in API 20+.
369 | * (SQLite version 3.7.16.2)
370 | * On older platforms, it will be 1 if the column is part of the primary key and 0
371 | * otherwise.
372 | *
373 | * The {@link #equals(Object)} implementation handles this inconsistency based on
374 | * API levels os if you are using a custom SQLite deployment, it may return false
375 | * positives.
376 | */
377 | public final int primaryKeyPosition;
378 | /**
379 | * The default value of this column.
380 | */
381 | public final String defaultValue;
382 |
383 | @CreatedFrom
384 | private final int mCreatedFrom;
385 |
386 | /**
387 | * @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
388 | */
389 | @Deprecated
390 | public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
391 | this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
392 | }
393 |
394 | // if you change this constructor, you must change TableInfoWriter.kt
395 | public Column(String name, String type, boolean notNull, int primaryKeyPosition,
396 | String defaultValue, @CreatedFrom int createdFrom) {
397 | this.name = name;
398 | this.type = type;
399 | this.notNull = notNull;
400 | this.primaryKeyPosition = primaryKeyPosition;
401 | this.affinity = findAffinity(type);
402 | this.defaultValue = defaultValue;
403 | this.mCreatedFrom = createdFrom;
404 | }
405 |
406 | /**
407 | * Implements https://www.sqlite.org/datatype3.html section 3.1
408 | *
409 | * @param type The type that was given to the sqlite
410 | * @return The normalized type which is one of the 5 known affinities
411 | */
412 | @ColumnInfo.SQLiteTypeAffinity
413 | private static int findAffinity(@Nullable String type) {
414 | if (type == null) {
415 | return ColumnInfo.BLOB;
416 | }
417 | String uppercaseType = type.toUpperCase(Locale.US);
418 | if (uppercaseType.contains("INT")) {
419 | return ColumnInfo.INTEGER;
420 | }
421 | if (uppercaseType.contains("CHAR")
422 | || uppercaseType.contains("CLOB")
423 | || uppercaseType.contains("TEXT")) {
424 | return ColumnInfo.TEXT;
425 | }
426 | if (uppercaseType.contains("BLOB")) {
427 | return ColumnInfo.BLOB;
428 | }
429 | if (uppercaseType.contains("REAL")
430 | || uppercaseType.contains("FLOA")
431 | || uppercaseType.contains("DOUB")) {
432 | return ColumnInfo.REAL;
433 | }
434 | // sqlite returns NUMERIC here but it is like a catch all. We already
435 | // have UNDEFINED so it is better to use UNDEFINED for consistency.
436 | return ColumnInfo.UNDEFINED;
437 | }
438 |
439 | @Override
440 | public boolean equals(Object o) {
441 | if (this == o) return true;
442 | if (o == null || getClass() != o.getClass()) return false;
443 |
444 | Column column = (Column) o;
445 | if (Build.VERSION.SDK_INT >= 20) {
446 | if (primaryKeyPosition != column.primaryKeyPosition) return false;
447 | } else {
448 | if (isPrimaryKey() != column.isPrimaryKey()) return false;
449 | }
450 |
451 | if (!name.equals(column.name)) return false;
452 | //noinspection SimplifiableIfStatement
453 | if (notNull != column.notNull) return false;
454 |
455 | // Only validate default value if it was defined in an entity, i.e. if the info
456 | // from the compiler itself has it. b/136019383
457 | if (mCreatedFrom == CREATED_FROM_ENTITY
458 | && column.mCreatedFrom == CREATED_FROM_DATABASE
459 | && (defaultValue != null && !defaultValue.equals(column.defaultValue))) {
460 | return false;
461 | } else if (mCreatedFrom == CREATED_FROM_DATABASE
462 | && column.mCreatedFrom == CREATED_FROM_ENTITY
463 | && (column.defaultValue != null && !column.defaultValue.equals(defaultValue))) {
464 | return false;
465 | } else if (mCreatedFrom != CREATED_FROM_UNKNOWN
466 | && mCreatedFrom == column.mCreatedFrom
467 | && (defaultValue != null ? !defaultValue.equals(column.defaultValue)
468 | : column.defaultValue != null)) {
469 | return false;
470 | }
471 |
472 | return affinity == column.affinity;
473 | }
474 |
475 | /**
476 | * Returns whether this column is part of the primary key or not.
477 | *
478 | * @return True if this column is part of the primary key, false otherwise.
479 | */
480 | public boolean isPrimaryKey() {
481 | return primaryKeyPosition > 0;
482 | }
483 |
484 | @Override
485 | public int hashCode() {
486 | int result = name.hashCode();
487 | result = 31 * result + affinity;
488 | result = 31 * result + (notNull ? 1231 : 1237);
489 | result = 31 * result + primaryKeyPosition;
490 | // Default value is not part of the hashcode since we conditionally check it for
491 | // equality which would break the equals + hashcode contract.
492 | // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
493 | return result;
494 | }
495 |
496 | @Override
497 | public String toString() {
498 | return "Column{"
499 | + "name='" + name + '\''
500 | + ", type='" + type + '\''
501 | + ", affinity='" + affinity + '\''
502 | + ", notNull=" + notNull
503 | + ", primaryKeyPosition=" + primaryKeyPosition
504 | + ", defaultValue='" + defaultValue + '\''
505 | + '}';
506 | }
507 | }
508 |
509 | /**
510 | * Holds the information about an SQLite foreign key
511 | *
512 | * @hide
513 | */
514 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
515 | public static class ForeignKey {
516 | @NonNull
517 | public final String referenceTable;
518 | @NonNull
519 | public final String onDelete;
520 | @NonNull
521 | public final String onUpdate;
522 | @NonNull
523 | public final List columnNames;
524 | @NonNull
525 | public final List referenceColumnNames;
526 |
527 | public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
528 | @NonNull String onUpdate,
529 | @NonNull List columnNames, @NonNull List referenceColumnNames) {
530 | this.referenceTable = referenceTable;
531 | this.onDelete = onDelete;
532 | this.onUpdate = onUpdate;
533 | this.columnNames = Collections.unmodifiableList(columnNames);
534 | this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
535 | }
536 |
537 | @Override
538 | public boolean equals(Object o) {
539 | if (this == o) return true;
540 | if (o == null || getClass() != o.getClass()) return false;
541 |
542 | ForeignKey that = (ForeignKey) o;
543 |
544 | if (!referenceTable.equals(that.referenceTable)) return false;
545 | if (!onDelete.equals(that.onDelete)) return false;
546 | if (!onUpdate.equals(that.onUpdate)) return false;
547 | //noinspection SimplifiableIfStatement
548 | if (!columnNames.equals(that.columnNames)) return false;
549 | return referenceColumnNames.equals(that.referenceColumnNames);
550 | }
551 |
552 | @Override
553 | public int hashCode() {
554 | int result = referenceTable.hashCode();
555 | result = 31 * result + onDelete.hashCode();
556 | result = 31 * result + onUpdate.hashCode();
557 | result = 31 * result + columnNames.hashCode();
558 | result = 31 * result + referenceColumnNames.hashCode();
559 | return result;
560 | }
561 |
562 | @Override
563 | public String toString() {
564 | return "ForeignKey{"
565 | + "referenceTable='" + referenceTable + '\''
566 | + ", onDelete='" + onDelete + '\''
567 | + ", onUpdate='" + onUpdate + '\''
568 | + ", columnNames=" + columnNames
569 | + ", referenceColumnNames=" + referenceColumnNames
570 | + '}';
571 | }
572 | }
573 |
574 | /**
575 | * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
576 | * sorting in the generated foreign key object.
577 | *
578 | * @hide
579 | */
580 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
581 | static class ForeignKeyWithSequence implements Comparable {
582 | final int mId;
583 | final int mSequence;
584 | final String mFrom;
585 | final String mTo;
586 |
587 | ForeignKeyWithSequence(int id, int sequence, String from, String to) {
588 | mId = id;
589 | mSequence = sequence;
590 | mFrom = from;
591 | mTo = to;
592 | }
593 |
594 | @Override
595 | public int compareTo(@NonNull ForeignKeyWithSequence o) {
596 | final int idCmp = mId - o.mId;
597 | if (idCmp == 0) {
598 | return mSequence - o.mSequence;
599 | } else {
600 | return idCmp;
601 | }
602 | }
603 | }
604 |
605 | /**
606 | * Holds the information about an SQLite index
607 | *
608 | * @hide
609 | */
610 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
611 | public static class Index {
612 | // should match the value in Index.kt
613 | public static final String DEFAULT_PREFIX = "index_";
614 | public final String name;
615 | public final boolean unique;
616 | public final List columns;
617 |
618 | public Index(String name, boolean unique, List columns) {
619 | this.name = name;
620 | this.unique = unique;
621 | this.columns = columns;
622 | }
623 |
624 | @Override
625 | public boolean equals(Object o) {
626 | if (this == o) return true;
627 | if (o == null || getClass() != o.getClass()) return false;
628 |
629 | Index index = (Index) o;
630 | if (unique != index.unique) {
631 | return false;
632 | }
633 | if (!columns.equals(index.columns)) {
634 | return false;
635 | }
636 | if (name.startsWith(Index.DEFAULT_PREFIX)) {
637 | return index.name.startsWith(Index.DEFAULT_PREFIX);
638 | } else {
639 | return name.equals(index.name);
640 | }
641 | }
642 |
643 | @Override
644 | public int hashCode() {
645 | int result;
646 | if (name.startsWith(DEFAULT_PREFIX)) {
647 | result = DEFAULT_PREFIX.hashCode();
648 | } else {
649 | result = name.hashCode();
650 | }
651 | result = 31 * result + (unique ? 1 : 0);
652 | result = 31 * result + columns.hashCode();
653 | return result;
654 | }
655 |
656 | @Override
657 | public String toString() {
658 | return "Index{"
659 | + "name='" + name + '\''
660 | + ", unique=" + unique
661 | + ", columns=" + columns
662 | + '}';
663 | }
664 | }
665 | }
666 |
--------------------------------------------------------------------------------
/runtime/src/main/java/androidx/room/util/ViewInfo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.room.util;
18 |
19 | import android.database.Cursor;
20 |
21 | import androidx.annotation.RestrictTo;
22 | import androidx.sqlite.db.SupportSQLiteDatabase;
23 |
24 | /**
25 | * A data class that holds the information about a view.
26 | *
27 | * This derives information from sqlite_master.
28 | *
29 | * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
30 | *
31 | * @hide
32 | */
33 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
34 | public class ViewInfo {
35 |
36 | /**
37 | * The view name
38 | */
39 | public final String name;
40 |
41 | /**
42 | * The SQL of CREATE VIEW.
43 | */
44 | public final String sql;
45 |
46 | public ViewInfo(String name, String sql) {
47 | this.name = name;
48 | this.sql = sql;
49 | }
50 |
51 | @Override
52 | public boolean equals(Object o) {
53 | if (this == o) return true;
54 | if (o == null || getClass() != o.getClass()) return false;
55 | ViewInfo viewInfo = (ViewInfo) o;
56 | return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null)
57 | && (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null);
58 | }
59 |
60 | @Override
61 | public int hashCode() {
62 | int result = name != null ? name.hashCode() : 0;
63 | result = 31 * result + (sql != null ? sql.hashCode() : 0);
64 | return result;
65 | }
66 |
67 | @Override
68 | public String toString() {
69 | return "ViewInfo{"
70 | + "name='" + name + '\''
71 | + ", sql='" + sql + '\''
72 | + '}';
73 | }
74 |
75 | /**
76 | * Reads the view information from the given database.
77 | *
78 | * @param database The database to read the information from.
79 | * @param viewName The view name.
80 | * @return A ViewInfo containing the schema information for the provided view name.
81 | */
82 | @SuppressWarnings("SameParameterValue")
83 | public static ViewInfo read(SupportSQLiteDatabase database, String viewName) {
84 | Cursor cursor = database.query("SELECT name, sql FROM sqlite_master "
85 | + "WHERE type = 'view' AND name = '" + viewName + "'");
86 | //noinspection TryFinallyCanBeTryWithResources
87 | try {
88 | if (cursor.moveToFirst()) {
89 | return new ViewInfo(cursor.getString(0), cursor.getString(1));
90 | } else {
91 | return new ViewInfo(viewName, null);
92 | }
93 | } finally {
94 | cursor.close();
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':runtime'
2 | rootProject.name='room-runtime'
3 |
--------------------------------------------------------------------------------