├── .buildscript └── deploy_snapshot.sh ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── gradle.properties ├── gradle ├── gradle-mvn-push.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── sample ├── build.gradle ├── debug.keystore └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── sqlbrite │ │ └── todo │ │ ├── TodoApp.java │ │ ├── TodoComponent.java │ │ ├── TodoModule.java │ │ ├── db │ │ ├── Db.java │ │ ├── DbCallback.java │ │ ├── DbModule.java │ │ ├── TodoItem.java │ │ └── TodoList.java │ │ └── ui │ │ ├── ItemsAdapter.java │ │ ├── ItemsFragment.java │ │ ├── ListsAdapter.java │ │ ├── ListsFragment.java │ │ ├── ListsItem.java │ │ ├── MainActivity.java │ │ ├── NewItemFragment.java │ │ └── NewListFragment.java │ └── res │ ├── anim │ ├── slide_in_left.xml │ ├── slide_in_right.xml │ ├── slide_out_left.xml │ └── slide_out_right.xml │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_launcher.png │ ├── layout │ ├── items.xml │ ├── lists.xml │ ├── new_item.xml │ └── new_list.xml │ └── values │ └── strings.xml ├── settings.gradle ├── sqlbrite-kotlin ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── squareup │ └── sqlbrite3 │ └── extensions.kt ├── sqlbrite-lint ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── squareup │ │ └── sqlbrite3 │ │ ├── BriteIssueRegistry.kt │ │ └── SqlBriteArgCountDetector.kt │ └── test │ └── java │ └── com │ └── squareup │ └── sqlbrite3 │ └── SqlBriteArgCountDetectorTest.kt └── sqlbrite ├── build.gradle ├── gradle.properties └── src ├── androidTest └── java │ └── com │ └── squareup │ └── sqlbrite3 │ ├── BlockingRecordingObserver.java │ ├── BriteContentResolverTest.java │ ├── BriteDatabaseTest.java │ ├── QueryObservableTest.java │ ├── QueryTest.java │ ├── RecordingObserver.java │ ├── SqlBriteTest.java │ ├── TestDb.java │ └── TestScheduler.java └── main ├── AndroidManifest.xml └── java └── com └── squareup └── sqlbrite3 ├── BriteContentResolver.java ├── BriteDatabase.java ├── QueryObservable.java ├── QueryToListOperator.java ├── QueryToOneOperator.java ├── QueryToOptionalOperator.java └── SqlBrite.java /.buildscript/deploy_snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. 4 | # 5 | # Adapted from https://coderwall.com/p/9b_lfq and 6 | # http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ 7 | 8 | SLUG="square/sqlbrite" 9 | JDK="oraclejdk8" 10 | BRANCH="master" 11 | 12 | set -e 13 | 14 | if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then 15 | echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'." 16 | elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then 17 | echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'." 18 | elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 19 | echo "Skipping snapshot deployment: was pull request." 20 | elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then 21 | echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'." 22 | else 23 | echo "Deploying snapshot..." 24 | ./gradlew clean uploadArchives 25 | echo "Snapshot deployed!" 26 | fi 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | .idea 3 | *.iml 4 | 5 | # Gradle 6 | .gradle 7 | gradlew.bat 8 | build 9 | local.properties 10 | reports 11 | 12 | # Apple 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - tools 6 | - platform-tools 7 | 8 | jdk: 9 | - oraclejdk8 10 | 11 | before_install: 12 | # Install SDK license so Android Gradle plugin can install deps. 13 | - mkdir "$ANDROID_HOME/licenses" || true 14 | - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license" 15 | # Install the rest of tools (e.g., avdmanager) 16 | - sdkmanager tools 17 | # Install the system image 18 | - sdkmanager "system-images;android-18;default;armeabi-v7a" 19 | # Create and start emulator for the script. Meant to race the install task. 20 | - echo no | avdmanager create avd --force -n test -k "system-images;android-18;default;armeabi-v7a" 21 | - $ANDROID_HOME/emulator/emulator -avd test -no-audio -no-window & 22 | 23 | install: ./gradlew clean assemble assembleAndroidTest --stacktrace 24 | 25 | before_script: 26 | - android-wait-for-emulator 27 | - adb shell input keyevent 82 28 | 29 | script: ./gradlew check connectedCheck --stacktrace 30 | 31 | after_success: 32 | - .buildscript/deploy_snapshot.sh 33 | 34 | env: 35 | global: 36 | - secure: "NIWC0zkThskXn7uduTJ1yT78voqEgzEfw8tOImGNBjZ/NDU6yxM4bh+tq+fnkn5ENjELV6fgcYd2DUJSWmkFD2k9ZMRNLm//AqlQihl8aT+DpWhDdCkQjnolHnjm1O7+ys7Q/vswBZEzkBxzIgivajZEzvjarQItJjbpBftQ0Cs=" 37 | - secure: "ahPT9EzJVpkM4q2HA/VBxUzgicvfdOOZaEvOiQKJofy1FrLjrBS2LFxqCbyffg0sjGUyvBMLg767CSt/0xRRFWIpsjxCfmvEmAURi89zdZ8MUNXIwe7x/0lXCdQIt8eueq3Qh5qFwJUy4aFbzVvcmMXKswWzw1O0+IcvYX00/xc=" 38 | 39 | branches: 40 | except: 41 | - gh-pages 42 | 43 | notifications: 44 | email: false 45 | 46 | sudo: false 47 | 48 | cache: 49 | directories: 50 | - $HOME/.gradle 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 3.2.0 *(2018-03-05)* 5 | ---------------------------- 6 | 7 | * New: Add `query(SupportSQLiteQuery)` method for one-off queries. 8 | 9 | 10 | Version 3.1.1 *(2018-02-12)* 11 | ---------------------------- 12 | 13 | * Fix: Useless `BuildConfig` classes are no longer included. 14 | * Fix: Eliminate Java interop checks for Kotlin extensions as they're only for Kotlin consumers and the checks exist in the Java code they delegate to anyway. 15 | 16 | 17 | Version 3.1.0 *(2017-12-18)* 18 | ---------------------------- 19 | 20 | * New: `inTransaction` Kotlin extension function which handles starting, marking successful, and ending 21 | a transaction. 22 | * New: Embedded lint check which validates the number of arguments passed to `query` and `createQuery` 23 | match the number of expected arguments of the SQL statement. 24 | * Fix: Properly indent multi-line SQL statements in the logs for `query`. 25 | 26 | 27 | Version 3.0.0 *(2017-11-28)* 28 | ---------------------------- 29 | 30 | Group ID has changed to `com.squareup.sqlbrite3`. 31 | 32 | * New: Build on top of the Android architecture components Sqlite support library. This allows swapping 33 | out the underlying Sqlite implementation to that of your choosing. 34 | 35 | Because of the way the Sqlite support library works, there is no interop bridge between 1.x or 2.x to 36 | this new version. If you haven't fully migrated to 2.x, complete that migration first and then upgrade 37 | to 3.x all at once. 38 | 39 | 40 | Version 2.0.0 *(2017-07-07)* 41 | ---------------------------- 42 | 43 | Group ID has changed to `com.squareup.sqlbrite2`. 44 | 45 | * New: RxJava 2.x support. Backpressure is no longer supported as evidenced by the use of 46 | `Observable`. If you want to slow down query notifications based on backpressure or another metric 47 | like time then you should apply those operators yourself. 48 | * New: `mapToOptional` for queries that return 0 or 1 rows. 49 | * New: `sqlbrite-kotlin` module provides `mapTo*` extension functions for `Observable`. 50 | * New: `sqlbrite-interop` module allows bridging 1.x and 2.x libraries together so that notifications 51 | from each trigger queries from the other. 52 | 53 | Note: This version only supports RxJava 2. 54 | 55 | 56 | Version 1.1.2 *(2017-06-30)* 57 | ---------------------------- 58 | 59 | * Internal architecture changes to support the upcoming 2.0 release and a bridge allowing both 1.x 60 | and 2.x to be used at the same time. 61 | 62 | 63 | Version 1.1.1 *(2016-12-20)* 64 | ---------------------------- 65 | 66 | * Fix: Correct spelling of `getWritableDatabase()` to match `SQLiteOpenHelper`. 67 | 68 | 69 | Version 1.1.0 *(2016-12-16)* 70 | ---------------------------- 71 | 72 | * New: Expose `getReadableDatabase()` and `getWriteableDatabase()` convenience methods. 73 | * Fix: Do not cache instances of the readable and writable database internally as the framework 74 | does this by default. 75 | 76 | 77 | Version 1.0.0 *(2016-12-02)* 78 | ---------------------------- 79 | 80 | * RxJava dependency updated to 1.2.3. 81 | * Restore `@WorkerThread` annotations to methods which do I/O. If you're using Java 8 with 82 | Retrolambda or Jack you need to use version 2.3 or newer of the Android Gradle plugin to have 83 | these annotations correctly handled by lint. 84 | 85 | 86 | Version 0.8.0 *(2016-10-21)* 87 | ---------------------------- 88 | 89 | * New: A `Transformer` can be supplied which is applied to each returned observable. 90 | * New: `newNonExclusiveTransaction()` starts transactions in `IMMEDIATE` mode. See the platform 91 | or SQLite documentation for more information. 92 | * New: APIs for insert/update/delete which allow providing a compiled `SQLiteStatement`. 93 | 94 | 95 | Version 0.7.0 *(2016-07-06)* 96 | ---------------------------- 97 | 98 | * New: Allow `mapTo*` mappers to return `null` values. This is useful when querying on a single, 99 | nullable column for which `null` is a valid value. 100 | * Fix: When `mapToOne` does not emit a value downstream, request another value from upstream to 101 | ensure fixed-item requests (such as `take(1)`) as properly honored. 102 | * Fix: Add logging to synchronous `execute` methods. 103 | 104 | 105 | Version 0.6.3 *(2016-04-13)* 106 | ---------------------------- 107 | 108 | * `QueryObservable` constructor is now public allow instances to be created for tests. 109 | 110 | 111 | Version 0.6.2 *(2016-03-01)* 112 | ---------------------------- 113 | 114 | * Fix: Document explicitly and correctly handle the fact that `Query.run()` can return `null` in 115 | some situations. The `mapToOne`, `mapToOneOrDefault`, `mapToList`, and `asRows` helpers have all 116 | been updated to handle this case and each is documented with their respective behavior. 117 | 118 | 119 | Version 0.6.1 *(2016-02-29)* 120 | ---------------------------- 121 | 122 | * Fix: Apply backpressure strategy between database/content provider and the supplied `Scheduler`. 123 | This guards against backpressure exceptions when the scheduler is unable to keep up with the rate 124 | at which queries are being triggered. 125 | * Fix: Indent the subsequent lines of a multi-line queries when logging. 126 | 127 | 128 | Version 0.6.0 *(2016-02-17)* 129 | ---------------------------- 130 | 131 | * New: Require a `Scheduler` when wrapping a database or content provider which will be used when 132 | sending query triggers. This allows the query to be run in subsequent operators without needing an 133 | additional `observeOn`. It also eliminates the need to use `subscribeOn` since the supplied 134 | `Scheduler` will be used for all emissions (similar to RxJava's `timer`, `interval`, etc.). 135 | 136 | This also corrects a potential violation of the RxJava contract and potential source of bugs in that 137 | all triggers will occur on the supplied `Scheduler`. Previously the initial value would trigger 138 | synchronously (on the subscribing thread) while subsequent ones trigger on the thread which 139 | performed the transaction. The new behavior puts the initial trigger on the same thread as all 140 | subsequent triggers and also does not force transactions to block while sending triggers. 141 | 142 | 143 | Version 0.5.1 *(2016-02-03)* 144 | ---------------------------- 145 | 146 | * New: Query logs now contain timing information on how long they took to execute. This only covers 147 | the time until a `Cursor` was made available, not object mapping or delivering to subscribers. 148 | * Fix: Switch query logging to happen when `Query.run` is called, not when a query is triggered. 149 | * Fix: Check for subscribing inside a transaction using a more accurate primitive. 150 | 151 | 152 | Version 0.5.0 *(2015-12-09)* 153 | ---------------------------- 154 | 155 | * New: Expose `mapToOne`, `mapToOneOrDefault`, and `mapToList` as static methods on `Query`. These 156 | mirror the behavior of the methods of the same name on `QueryObservable` but can be used later in 157 | a stream by passing the returned `Operator` instances to `lift()` (e.g., 158 | `take(1).lift(Query.mapToOne(..))`). 159 | * Requires RxJava 1.1.0 or newer. 160 | 161 | 162 | Version 0.4.1 *(2015-10-19)* 163 | ---------------------------- 164 | 165 | * New: `execute` method provides the ability to execute arbitrary SQL statements. 166 | * New: `executeAndTrigger` method provides the ability to execute arbitrary SQL statements and 167 | notifying any queries to update on the specified table. 168 | * Fix: `Query.asRows` no longer calls `onCompleted` when the downstream subscriber has unsubscribed. 169 | 170 | 171 | Version 0.4.0 *(2015-09-22)* 172 | ---------------------------- 173 | 174 | * New: `mapToOneOrDefault` replaces `mapToOneOrNull` for more flexibility. 175 | * Fix: Notifications of table updates as the result of a transaction now occur after the transaction 176 | has been applied. Previous the notification would happen during the commit at which time it was 177 | invalid to create a new transaction in a subscriber. 178 | 179 | 180 | Version 0.3.1 *(2015-09-02)* 181 | ---------------------------- 182 | 183 | * New: `mapToOne` and `mapToOneOrNull` operators on `QueryObservable`. These work on queries which 184 | return 0 or 1 rows and are a convenience for turning them into a type `T` given a mapper of type 185 | `Func1` (the same which can be used for `mapToList`). 186 | * Fix: Remove `@WorkerThread` annotations for now. Various combinations of lint, RxJava, and 187 | retrolambda can cause false-positives. 188 | 189 | 190 | Version 0.3.0 *(2015-08-31)* 191 | ---------------------------- 192 | 193 | * Transactions are now exposed as objects instead of methods. Call `newTransaction()` to start a 194 | transaction. On the `Transaction` instance, call `markSuccessful()` to indicate success and 195 | `end()` to commit or rollback the transaction. The `Transaction` instance implements `Closeable` 196 | to allow its use in a try-with-resources construct. See the `newTransaction()` Javadoc for more 197 | information. 198 | * `Query` instances can now be turned directly into an `Observable` by calling `asRows` with a 199 | `Func1` that maps rows to a type `T`. This allows easy filtering and limiting in 200 | memory rather than in the query. See the `asRows` Javadoc for more information. 201 | * `createQuery` now returns a `QueryObservable` which offers a `mapToList` operator. This operator 202 | also takes a `Func1` for mapping rows to a type `T`, but instead of individual rows it 203 | collects all the rows into a list. For large query results or frequently updated tables this can 204 | create a lot of objects. See the `mapToList` Javadoc for more information. 205 | * New: Nullability, `@CheckResult`, and `@WorkerThread` annotations on all APIs allow a more useful 206 | interaction with lint in consuming projects. 207 | 208 | 209 | Version 0.2.1 *(2015-07-14)* 210 | ---------------------------- 211 | 212 | * Fix: Add support for backpressure. 213 | 214 | 215 | Version 0.2.0 *(2015-06-30)* 216 | ---------------------------- 217 | 218 | * An `Observable` can now be created from wrapping a `ContentResolver` in order to observe 219 | queries from another app's content provider. 220 | * `SqlBrite` class is now a factory for both a `BriteDatabase` (the `SQLiteOpenHelper` wrapper) 221 | and `BriteContentResolver` (the `ContentResolver` wrapper). 222 | 223 | 224 | Version 0.1.0 *(2015-02-21)* 225 | ---------------------------- 226 | 227 | Initial release. 228 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code you can do so through GitHub by forking 5 | the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles by running `./gradlew clean build`. 10 | 11 | Before your code can be accepted into the project you must also sign the 12 | [Individual Contributor License Agreement (CLA)][1]. 13 | 14 | 15 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SQL Brite 2 | ========= 3 | 4 | A lightweight wrapper around `SupportSQLiteOpenHelper` and `ContentResolver` which introduces reactive 5 | stream semantics to queries. 6 | 7 | # Deprecated 8 | 9 | This library is no longer actively developed and is considered complete. 10 | 11 | Its database features (and far, far more) are now offered by [SQLDelight](https://github.com/cashapp/sqldelight/) 12 | and its [upgrading guide](https://github.com/cashapp/sqldelight/blob/1.0.0/UPGRADING.md) offers some 13 | migration help. 14 | 15 | For content provider monitoring please use [Copper](https://github.com/cashapp/copper) instead. 16 | 17 | 18 | 19 | Usage 20 | ----- 21 | 22 | Create a `SqlBrite` instance which is an adapter for the library functionality. 23 | 24 | ```java 25 | SqlBrite sqlBrite = new SqlBrite.Builder().build(); 26 | ``` 27 | 28 | Pass a `SupportSQLiteOpenHelper` instance and a `Scheduler` to create a `BriteDatabase`. 29 | 30 | ```java 31 | BriteDatabase db = sqlBrite.wrapDatabaseHelper(openHelper, Schedulers.io()); 32 | ``` 33 | 34 | A `Scheduler` is required for a few reasons, but the most important is that query notifications can 35 | trigger on the thread of your choice. The query can then be run without blocking the main thread or 36 | the thread which caused the trigger. 37 | 38 | The `BriteDatabase.createQuery` method is similar to `SupportSQLiteDatabase.query` except it takes an 39 | additional parameter of table(s) on which to listen for changes. Subscribe to the returned 40 | `Observable` which will immediately notify with a `Query` to run. 41 | 42 | ```java 43 | Observable users = db.createQuery("users", "SELECT * FROM users"); 44 | users.subscribe(new Consumer() { 45 | @Override public void accept(Query query) { 46 | Cursor cursor = query.run(); 47 | // TODO parse data... 48 | } 49 | }); 50 | ``` 51 | 52 | Unlike a traditional `query`, updates to the specified table(s) will trigger additional 53 | notifications for as long as you remain subscribed to the observable. This means that when you 54 | insert, update, or delete data, any subscribed queries will update with the new data instantly. 55 | 56 | ```java 57 | final AtomicInteger queries = new AtomicInteger(); 58 | users.subscribe(new Consumer() { 59 | @Override public void accept(Query query) { 60 | queries.getAndIncrement(); 61 | } 62 | }); 63 | System.out.println("Queries: " + queries.get()); // Prints 1 64 | 65 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("jw", "Jake Wharton")); 66 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("mattp", "Matt Precious")); 67 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("strong", "Alec Strong")); 68 | 69 | System.out.println("Queries: " + queries.get()); // Prints 4 70 | ``` 71 | 72 | In the previous example we re-used the `BriteDatabase` object "db" for inserts. All insert, update, 73 | or delete operations must go through this object in order to correctly notify subscribers. 74 | 75 | Unsubscribe from the returned `Subscription` to stop getting updates. 76 | 77 | ```java 78 | final AtomicInteger queries = new AtomicInteger(); 79 | Subscription s = users.subscribe(new Consumer() { 80 | @Override public void accept(Query query) { 81 | queries.getAndIncrement(); 82 | } 83 | }); 84 | System.out.println("Queries: " + queries.get()); // Prints 1 85 | 86 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("jw", "Jake Wharton")); 87 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("mattp", "Matt Precious")); 88 | s.unsubscribe(); 89 | 90 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("strong", "Alec Strong")); 91 | 92 | System.out.println("Queries: " + queries.get()); // Prints 3 93 | ``` 94 | 95 | Use transactions to prevent large changes to the data from spamming your subscribers. 96 | 97 | ```java 98 | final AtomicInteger queries = new AtomicInteger(); 99 | users.subscribe(new Consumer() { 100 | @Override public void accept(Query query) { 101 | queries.getAndIncrement(); 102 | } 103 | }); 104 | System.out.println("Queries: " + queries.get()); // Prints 1 105 | 106 | Transaction transaction = db.newTransaction(); 107 | try { 108 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("jw", "Jake Wharton")); 109 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("mattp", "Matt Precious")); 110 | db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("strong", "Alec Strong")); 111 | transaction.markSuccessful(); 112 | } finally { 113 | transaction.end(); 114 | } 115 | 116 | System.out.println("Queries: " + queries.get()); // Prints 2 117 | ``` 118 | *Note: You can also use try-with-resources with a `Transaction` instance.* 119 | 120 | Since queries are just regular RxJava `Observable` objects, operators can also be used to 121 | control the frequency of notifications to subscribers. 122 | 123 | ```java 124 | users.debounce(500, MILLISECONDS).subscribe(new Consumer() { 125 | @Override public void accept(Query query) { 126 | // TODO... 127 | } 128 | }); 129 | ``` 130 | 131 | The `SqlBrite` object can also wrap a `ContentResolver` for observing a query on another app's 132 | content provider. 133 | 134 | ```java 135 | BriteContentResolver resolver = sqlBrite.wrapContentProvider(contentResolver, Schedulers.io()); 136 | Observable query = resolver.createQuery(/*...*/); 137 | ``` 138 | 139 | The full power of RxJava's operators are available for combining, filtering, and triggering any 140 | number of queries and data changes. 141 | 142 | 143 | 144 | Philosophy 145 | ---------- 146 | 147 | SQL Brite's only responsibility is to be a mechanism for coordinating and composing the notification 148 | of updates to tables such that you can update queries as soon as data changes. 149 | 150 | This library is not an ORM. It is not a type-safe query mechanism. It won't serialize the same POJOs 151 | you use for Gson. It's not going to perform database migrations for you. 152 | 153 | Some of these features are offered by [SQL Delight][sqldelight] which can be used with SQL Brite. 154 | 155 | 156 | 157 | Download 158 | -------- 159 | 160 | ```groovy 161 | implementation 'com.squareup.sqlbrite3:sqlbrite:3.2.0' 162 | ``` 163 | 164 | For the 'kotlin' module that adds extension functions to `Observable`: 165 | ```groovy 166 | implementation 'com.squareup.sqlbrite3:sqlbrite-kotlin:3.2.0' 167 | ``` 168 | 169 | 170 | Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. 171 | 172 | 173 | 174 | License 175 | ------- 176 | 177 | Copyright 2015 Square, Inc. 178 | 179 | Licensed under the Apache License, Version 2.0 (the "License"); 180 | you may not use this file except in compliance with the License. 181 | You may obtain a copy of the License at 182 | 183 | http://www.apache.org/licenses/LICENSE-2.0 184 | 185 | Unless required by applicable law or agreed to in writing, software 186 | distributed under the License is distributed on an "AS IS" BASIS, 187 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 188 | See the License for the specific language governing permissions and 189 | limitations under the License. 190 | 191 | 192 | 193 | 194 | 195 | [snap]: https://oss.sonatype.org/content/repositories/snapshots/ 196 | [sqldelight]: https://github.com/square/sqldelight/ 197 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ======== 3 | 4 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. 5 | 2. Update the `CHANGELOG.md` for the impending release. 6 | 3. Update the `README.md` with the new version. 7 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 8 | 5. `./gradlew clean uploadArchives`. 9 | 6. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 10 | 7. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version) 11 | 8. Update the `gradle.properties` to the next SNAPSHOT version. 12 | 9. `git commit -am "Prepare next development version."` 13 | 10. `git push && git push --tags` 14 | 15 | If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5. 16 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.versions = [ 3 | 'minSdk': 14, 4 | 'compileSdk': 27, 5 | 'kotlin': '1.1.60', 6 | 'lint': '26.0.1' 7 | ] 8 | 9 | repositories { 10 | mavenCentral() 11 | google() 12 | jcenter() 13 | } 14 | 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:3.0.1' 17 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | mavenCentral() 24 | google() 25 | jcenter() 26 | } 27 | 28 | group = GROUP 29 | version = VERSION_NAME 30 | } 31 | 32 | ext { 33 | // Android dependencies. 34 | supportV4 = 'com.android.support:support-v4:27.0.0' 35 | supportAnnotations = 'com.android.support:support-annotations:27.0.0' 36 | supportTestRunner = 'com.android.support.test:runner:0.5' 37 | 38 | supportSqlite = 'android.arch.persistence:db:1.0.0' 39 | supportSqliteFramework = 'android.arch.persistence:db-framework:1.0.0' 40 | 41 | // Third-party dependencies. 42 | kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" 43 | dagger = 'com.google.dagger:dagger:2.13' 44 | daggerCompiler = 'com.google.dagger:dagger-compiler:2.13' 45 | butterKnifeRuntime = 'com.jakewharton:butterknife:8.8.1' 46 | butterKnifeCompiler = 'com.jakewharton:butterknife-compiler:8.8.1' 47 | timber = 'com.jakewharton.timber:timber:4.6.0' 48 | autoValue = 'com.google.auto.value:auto-value:1.5' 49 | autoValueParcel = 'com.ryanharter.auto.value:auto-value-parcel:0.2.5' 50 | rxJava = 'io.reactivex.rxjava2:rxjava:2.1.3' 51 | rxAndroid = 'io.reactivex.rxjava2:rxandroid:2.0.1' 52 | rxBinding = 'com.jakewharton.rxbinding2:rxbinding:2.0.0' 53 | junit = 'junit:junit:4.12' 54 | truth = 'com.google.truth:truth:0.36' 55 | 56 | // Lint dependencies. 57 | lintApi = "com.android.tools.lint:lint-api:${versions.lint}" 58 | lint = "com.android.tools.lint:lint:${versions.lint}" 59 | lintTests = "com.android.tools.lint:lint-tests:${versions.lint}" 60 | } 61 | 62 | configurations { 63 | osstrich 64 | } 65 | dependencies { 66 | osstrich 'com.squareup.osstrich:osstrich:1.2.0' 67 | } 68 | task publishV1Javadoc(type: JavaExec) { 69 | classpath = configurations.osstrich 70 | main = 'com.squareup.osstrich.JavadocPublisher' 71 | args = [ 72 | 'build/javadoc', 73 | 'https://github.com/square/sqlbrite', 74 | 'com.squareup.sqlbrite' 75 | ] 76 | } 77 | task publishV2Javadoc(type: JavaExec) { 78 | classpath = configurations.osstrich 79 | main = 'com.squareup.osstrich.JavadocPublisher' 80 | args = [ 81 | 'build/javadoc', 82 | 'https://github.com/square/sqlbrite', 83 | 'com.squareup.sqlbrite2' 84 | ] 85 | } 86 | task publishV3Javadoc(type: JavaExec) { 87 | classpath = configurations.osstrich 88 | main = 'com.squareup.osstrich.JavadocPublisher' 89 | args = [ 90 | 'build/javadoc', 91 | 'https://github.com/square/sqlbrite', 92 | 'com.squareup.sqlbrite3' 93 | ] 94 | } 95 | task publishJavadoc(dependsOn: [publishV1Javadoc, publishV2Javadoc, publishV3Javadoc]) 96 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.squareup.sqlbrite3 2 | VERSION_NAME=3.2.1-SNAPSHOT 3 | 4 | POM_DESCRIPTION=A lightweight wrapper around SQLiteOpenHelper which introduces reactive stream semantics to SQL operations. 5 | 6 | POM_URL=http://github.com/square/sqlbrite/ 7 | POM_SCM_URL=http://github.com/square/sqlbrite/ 8 | POM_SCM_CONNECTION=scm:git:git://github.com/square/sqlbrite.git 9 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/sqlbrite.git 10 | 11 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 12 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 13 | POM_LICENCE_DIST=repo 14 | 15 | POM_DEVELOPER_ID=square 16 | POM_DEVELOPER_NAME=Square, Inc. 17 | -------------------------------------------------------------------------------- /gradle/gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 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 | apply plugin: 'maven' 18 | apply plugin: 'signing' 19 | 20 | def isReleaseBuild() { 21 | return VERSION_NAME.contains("SNAPSHOT") == false 22 | } 23 | 24 | def getRepositoryUsername() { 25 | return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" 26 | } 27 | 28 | def getRepositoryPassword() { 29 | return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" 30 | } 31 | 32 | afterEvaluate { project -> 33 | uploadArchives { 34 | repositories { 35 | mavenDeployer { 36 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 37 | 38 | pom.groupId = GROUP 39 | pom.artifactId = POM_ARTIFACT_ID 40 | pom.version = VERSION_NAME 41 | 42 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 43 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 44 | } 45 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 46 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 47 | } 48 | 49 | pom.project { 50 | name POM_NAME 51 | packaging POM_PACKAGING 52 | description POM_DESCRIPTION 53 | url POM_URL 54 | 55 | scm { 56 | url POM_SCM_URL 57 | connection POM_SCM_CONNECTION 58 | developerConnection POM_SCM_DEV_CONNECTION 59 | } 60 | 61 | licenses { 62 | license { 63 | name POM_LICENCE_NAME 64 | url POM_LICENCE_URL 65 | distribution POM_LICENCE_DIST 66 | } 67 | } 68 | 69 | developers { 70 | developer { 71 | id POM_DEVELOPER_ID 72 | name POM_DEVELOPER_NAME 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | signing { 81 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 82 | sign configurations.archives 83 | } 84 | 85 | task androidJavadocs(type: Javadoc) { 86 | if (!project.plugins.hasPlugin('kotlin-android')) { 87 | source = android.sourceSets.main.java.srcDirs 88 | } 89 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 90 | 91 | if (JavaVersion.current().isJava8Compatible()) { 92 | options.addStringOption('Xdoclint:none', '-quiet') 93 | } 94 | } 95 | 96 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 97 | classifier = 'javadoc' 98 | from androidJavadocs.destinationDir 99 | } 100 | 101 | task androidSourcesJar(type: Jar) { 102 | classifier = 'sources' 103 | from android.sourceSets.main.java.sourceFiles 104 | } 105 | 106 | artifacts { 107 | archives androidSourcesJar 108 | archives androidJavadocsJar 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sqlbrite/d4cf6ef967452f19a508e14b7c46f64964f8052e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | dependencies { 4 | implementation rootProject.ext.supportV4 5 | implementation rootProject.ext.supportAnnotations 6 | 7 | implementation rootProject.ext.dagger 8 | annotationProcessor rootProject.ext.daggerCompiler 9 | 10 | implementation rootProject.ext.butterKnifeRuntime 11 | annotationProcessor rootProject.ext.butterKnifeCompiler 12 | implementation rootProject.ext.timber 13 | implementation rootProject.ext.rxJava 14 | implementation rootProject.ext.rxAndroid 15 | implementation rootProject.ext.rxBinding 16 | 17 | compileOnly rootProject.ext.autoValue 18 | annotationProcessor rootProject.ext.autoValue 19 | annotationProcessor rootProject.ext.autoValueParcel 20 | 21 | implementation project(':sqlbrite') 22 | implementation rootProject.ext.supportSqliteFramework 23 | } 24 | 25 | android { 26 | compileSdkVersion versions.compileSdk 27 | 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_7 30 | targetCompatibility JavaVersion.VERSION_1_7 31 | } 32 | 33 | lintOptions { 34 | textOutput 'stdout' 35 | textReport true 36 | ignore 'InvalidPackage' // Provided AutoValue pulls in Guava and friends. Doesn't end up in APK. 37 | } 38 | 39 | defaultConfig { 40 | minSdkVersion versions.minSdk 41 | targetSdkVersion versions.compileSdk 42 | applicationId 'com.example.sqlbrite.todo' 43 | 44 | versionCode 1 45 | versionName '1.0' 46 | } 47 | 48 | signingConfigs { 49 | debug { 50 | storeFile file('debug.keystore') 51 | storePassword 'android' 52 | keyAlias 'android' 53 | keyPassword 'android' 54 | } 55 | } 56 | 57 | buildTypes { 58 | debug { 59 | applicationIdSuffix '.development' 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sample/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sqlbrite/d4cf6ef967452f19a508e14b7c46f64964f8052e/sample/debug.keystore -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/TodoApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo; 17 | 18 | import android.app.Application; 19 | import android.content.Context; 20 | import timber.log.Timber; 21 | 22 | public final class TodoApp extends Application { 23 | private TodoComponent mainComponent; 24 | 25 | @Override public void onCreate() { 26 | super.onCreate(); 27 | 28 | if (BuildConfig.DEBUG) { 29 | Timber.plant(new Timber.DebugTree()); 30 | } 31 | 32 | mainComponent = DaggerTodoComponent.builder().todoModule(new TodoModule(this)).build(); 33 | } 34 | 35 | public static TodoComponent getComponent(Context context) { 36 | return ((TodoApp) context.getApplicationContext()).mainComponent; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/TodoComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo; 17 | 18 | import com.example.sqlbrite.todo.ui.ItemsFragment; 19 | import com.example.sqlbrite.todo.ui.ListsFragment; 20 | import com.example.sqlbrite.todo.ui.NewItemFragment; 21 | import com.example.sqlbrite.todo.ui.NewListFragment; 22 | import dagger.Component; 23 | import javax.inject.Singleton; 24 | 25 | @Singleton 26 | @Component(modules = TodoModule.class) 27 | public interface TodoComponent { 28 | 29 | void inject(ListsFragment fragment); 30 | 31 | void inject(ItemsFragment fragment); 32 | 33 | void inject(NewItemFragment fragment); 34 | 35 | void inject(NewListFragment fragment); 36 | } 37 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/TodoModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo; 17 | 18 | import android.app.Application; 19 | import com.example.sqlbrite.todo.db.DbModule; 20 | import dagger.Module; 21 | import dagger.Provides; 22 | import javax.inject.Singleton; 23 | 24 | @Module( 25 | includes = { 26 | DbModule.class, 27 | } 28 | ) 29 | public final class TodoModule { 30 | private final Application application; 31 | 32 | TodoModule(Application application) { 33 | this.application = application; 34 | } 35 | 36 | @Provides @Singleton Application provideApplication() { 37 | return application; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/db/Db.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.db; 17 | 18 | import android.database.Cursor; 19 | 20 | public final class Db { 21 | public static final int BOOLEAN_FALSE = 0; 22 | public static final int BOOLEAN_TRUE = 1; 23 | 24 | public static String getString(Cursor cursor, String columnName) { 25 | return cursor.getString(cursor.getColumnIndexOrThrow(columnName)); 26 | } 27 | 28 | public static boolean getBoolean(Cursor cursor, String columnName) { 29 | return getInt(cursor, columnName) == BOOLEAN_TRUE; 30 | } 31 | 32 | public static long getLong(Cursor cursor, String columnName) { 33 | return cursor.getLong(cursor.getColumnIndexOrThrow(columnName)); 34 | } 35 | 36 | public static int getInt(Cursor cursor, String columnName) { 37 | return cursor.getInt(cursor.getColumnIndexOrThrow(columnName)); 38 | } 39 | 40 | private Db() { 41 | throw new AssertionError("No instances."); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/db/DbCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.db; 17 | 18 | import android.arch.persistence.db.SupportSQLiteDatabase; 19 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 20 | import android.content.Context; 21 | import android.database.sqlite.SQLiteDatabase; 22 | import android.database.sqlite.SQLiteOpenHelper; 23 | 24 | import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL; 25 | 26 | final class DbCallback extends SupportSQLiteOpenHelper.Callback { 27 | private static final int VERSION = 1; 28 | 29 | private static final String CREATE_LIST = "" 30 | + "CREATE TABLE " + TodoList.TABLE + "(" 31 | + TodoList.ID + " INTEGER NOT NULL PRIMARY KEY," 32 | + TodoList.NAME + " TEXT NOT NULL," 33 | + TodoList.ARCHIVED + " INTEGER NOT NULL DEFAULT 0" 34 | + ")"; 35 | private static final String CREATE_ITEM = "" 36 | + "CREATE TABLE " + TodoItem.TABLE + "(" 37 | + TodoItem.ID + " INTEGER NOT NULL PRIMARY KEY," 38 | + TodoItem.LIST_ID + " INTEGER NOT NULL REFERENCES " + TodoList.TABLE + "(" + TodoList.ID + ")," 39 | + TodoItem.DESCRIPTION + " TEXT NOT NULL," 40 | + TodoItem.COMPLETE + " INTEGER NOT NULL DEFAULT 0" 41 | + ")"; 42 | private static final String CREATE_ITEM_LIST_ID_INDEX = 43 | "CREATE INDEX item_list_id ON " + TodoItem.TABLE + " (" + TodoItem.LIST_ID + ")"; 44 | 45 | DbCallback() { 46 | super(VERSION); 47 | } 48 | 49 | @Override public void onCreate(SupportSQLiteDatabase db) { 50 | db.execSQL(CREATE_LIST); 51 | db.execSQL(CREATE_ITEM); 52 | db.execSQL(CREATE_ITEM_LIST_ID_INDEX); 53 | 54 | long groceryListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() 55 | .name("Grocery List") 56 | .build()); 57 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 58 | .listId(groceryListId) 59 | .description("Beer") 60 | .build()); 61 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 62 | .listId(groceryListId) 63 | .description("Point Break on DVD") 64 | .build()); 65 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 66 | .listId(groceryListId) 67 | .description("Bad Boys 2 on DVD") 68 | .build()); 69 | 70 | long holidayPresentsListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() 71 | .name("Holiday Presents") 72 | .build()); 73 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 74 | .listId(holidayPresentsListId) 75 | .description("Pogo Stick for Jake W.") 76 | .build()); 77 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 78 | .listId(holidayPresentsListId) 79 | .description("Jack-in-the-box for Alec S.") 80 | .build()); 81 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 82 | .listId(holidayPresentsListId) 83 | .description("Pogs for Matt P.") 84 | .build()); 85 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 86 | .listId(holidayPresentsListId) 87 | .description("Cola for Jesse W.") 88 | .build()); 89 | 90 | long workListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() 91 | .name("Work Items") 92 | .build()); 93 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 94 | .listId(workListId) 95 | .description("Finish SqlBrite library") 96 | .complete(true) 97 | .build()); 98 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 99 | .listId(workListId) 100 | .description("Finish SqlBrite sample app") 101 | .build()); 102 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() 103 | .listId(workListId) 104 | .description("Publish SqlBrite to GitHub") 105 | .build()); 106 | 107 | long birthdayPresentsListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() 108 | .name("Birthday Presents") 109 | .archived(true) 110 | .build()); 111 | db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder().listId(birthdayPresentsListId) 112 | .description("New car") 113 | .complete(true) 114 | .build()); 115 | } 116 | 117 | @Override public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/db/DbModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.db; 17 | 18 | import android.app.Application; 19 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 20 | import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration; 21 | import android.arch.persistence.db.SupportSQLiteOpenHelper.Factory; 22 | import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; 23 | import com.squareup.sqlbrite3.BriteDatabase; 24 | import com.squareup.sqlbrite3.SqlBrite; 25 | import dagger.Module; 26 | import dagger.Provides; 27 | import io.reactivex.schedulers.Schedulers; 28 | import javax.inject.Singleton; 29 | import timber.log.Timber; 30 | 31 | @Module 32 | public final class DbModule { 33 | @Provides @Singleton SqlBrite provideSqlBrite() { 34 | return new SqlBrite.Builder() 35 | .logger(new SqlBrite.Logger() { 36 | @Override public void log(String message) { 37 | Timber.tag("Database").v(message); 38 | } 39 | }) 40 | .build(); 41 | } 42 | 43 | @Provides @Singleton BriteDatabase provideDatabase(SqlBrite sqlBrite, Application application) { 44 | Configuration configuration = Configuration.builder(application) 45 | .name("todo.db") 46 | .callback(new DbCallback()) 47 | .build(); 48 | Factory factory = new FrameworkSQLiteOpenHelperFactory(); 49 | SupportSQLiteOpenHelper helper = factory.create(configuration); 50 | BriteDatabase db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io()); 51 | db.setLoggingEnabled(true); 52 | return db; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/db/TodoItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.db; 17 | 18 | import android.content.ContentValues; 19 | import android.database.Cursor; 20 | import android.os.Parcelable; 21 | import com.google.auto.value.AutoValue; 22 | import io.reactivex.functions.Function; 23 | 24 | @AutoValue 25 | public abstract class TodoItem implements Parcelable { 26 | public static final String TABLE = "todo_item"; 27 | 28 | public static final String ID = "_id"; 29 | public static final String LIST_ID = "todo_list_id"; 30 | public static final String DESCRIPTION = "description"; 31 | public static final String COMPLETE = "complete"; 32 | 33 | public abstract long id(); 34 | public abstract long listId(); 35 | public abstract String description(); 36 | public abstract boolean complete(); 37 | 38 | public static final Function MAPPER = new Function() { 39 | @Override public TodoItem apply(Cursor cursor) { 40 | long id = Db.getLong(cursor, ID); 41 | long listId = Db.getLong(cursor, LIST_ID); 42 | String description = Db.getString(cursor, DESCRIPTION); 43 | boolean complete = Db.getBoolean(cursor, COMPLETE); 44 | return new AutoValue_TodoItem(id, listId, description, complete); 45 | } 46 | }; 47 | 48 | public static final class Builder { 49 | private final ContentValues values = new ContentValues(); 50 | 51 | public Builder id(long id) { 52 | values.put(ID, id); 53 | return this; 54 | } 55 | 56 | public Builder listId(long listId) { 57 | values.put(LIST_ID, listId); 58 | return this; 59 | } 60 | 61 | public Builder description(String description) { 62 | values.put(DESCRIPTION, description); 63 | return this; 64 | } 65 | 66 | public Builder complete(boolean complete) { 67 | values.put(COMPLETE, complete ? Db.BOOLEAN_TRUE : Db.BOOLEAN_FALSE); 68 | return this; 69 | } 70 | 71 | public ContentValues build() { 72 | return values; // TODO defensive copy? 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/db/TodoList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.db; 17 | 18 | import android.content.ContentValues; 19 | import android.os.Parcelable; 20 | import com.google.auto.value.AutoValue; 21 | 22 | // Note: normally I wouldn't prefix table classes but I didn't want 'List' to be overloaded. 23 | @AutoValue 24 | public abstract class TodoList implements Parcelable { 25 | public static final String TABLE = "todo_list"; 26 | 27 | public static final String ID = "_id"; 28 | public static final String NAME = "name"; 29 | public static final String ARCHIVED = "archived"; 30 | 31 | public abstract long id(); 32 | public abstract String name(); 33 | public abstract boolean archived(); 34 | 35 | public static final class Builder { 36 | private final ContentValues values = new ContentValues(); 37 | 38 | public Builder id(long id) { 39 | values.put(ID, id); 40 | return this; 41 | } 42 | 43 | public Builder name(String name) { 44 | values.put(NAME, name); 45 | return this; 46 | } 47 | 48 | public Builder archived(boolean archived) { 49 | values.put(ARCHIVED, archived); 50 | return this; 51 | } 52 | 53 | public ContentValues build() { 54 | return values; // TODO defensive copy? 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/ItemsAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.content.Context; 19 | import android.text.SpannableString; 20 | import android.text.style.StrikethroughSpan; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.BaseAdapter; 25 | import android.widget.CheckedTextView; 26 | import com.example.sqlbrite.todo.db.TodoItem; 27 | import io.reactivex.functions.Consumer; 28 | import java.util.Collections; 29 | import java.util.List; 30 | 31 | final class ItemsAdapter extends BaseAdapter implements Consumer> { 32 | private final LayoutInflater inflater; 33 | 34 | private List items = Collections.emptyList(); 35 | 36 | public ItemsAdapter(Context context) { 37 | inflater = LayoutInflater.from(context); 38 | } 39 | 40 | @Override public void accept(List items) { 41 | this.items = items; 42 | notifyDataSetChanged(); 43 | } 44 | 45 | @Override public int getCount() { 46 | return items.size(); 47 | } 48 | 49 | @Override public TodoItem getItem(int position) { 50 | return items.get(position); 51 | } 52 | 53 | @Override public long getItemId(int position) { 54 | return getItem(position).id(); 55 | } 56 | 57 | @Override public boolean hasStableIds() { 58 | return true; 59 | } 60 | 61 | @Override public View getView(int position, View convertView, ViewGroup parent) { 62 | if (convertView == null) { 63 | convertView = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, parent, false); 64 | } 65 | 66 | TodoItem item = getItem(position); 67 | CheckedTextView textView = (CheckedTextView) convertView; 68 | textView.setChecked(item.complete()); 69 | 70 | CharSequence description = item.description(); 71 | if (item.complete()) { 72 | SpannableString spannable = new SpannableString(description); 73 | spannable.setSpan(new StrikethroughSpan(), 0, description.length(), 0); 74 | description = spannable; 75 | } 76 | 77 | textView.setText(description); 78 | 79 | return convertView; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/ItemsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.app.Activity; 19 | import android.database.Cursor; 20 | import android.os.Bundle; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.app.Fragment; 23 | import android.support.v4.view.MenuItemCompat; 24 | import android.view.LayoutInflater; 25 | import android.view.Menu; 26 | import android.view.MenuInflater; 27 | import android.view.MenuItem; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | import android.widget.ListView; 31 | import butterknife.BindView; 32 | import butterknife.ButterKnife; 33 | import com.example.sqlbrite.todo.R; 34 | import com.example.sqlbrite.todo.TodoApp; 35 | import com.example.sqlbrite.todo.db.Db; 36 | import com.example.sqlbrite.todo.db.TodoItem; 37 | import com.example.sqlbrite.todo.db.TodoList; 38 | import com.jakewharton.rxbinding2.widget.AdapterViewItemClickEvent; 39 | import com.jakewharton.rxbinding2.widget.RxAdapterView; 40 | import com.squareup.sqlbrite3.BriteDatabase; 41 | import io.reactivex.Observable; 42 | import io.reactivex.android.schedulers.AndroidSchedulers; 43 | import io.reactivex.disposables.CompositeDisposable; 44 | import io.reactivex.functions.BiFunction; 45 | import io.reactivex.functions.Consumer; 46 | import io.reactivex.functions.Function; 47 | import io.reactivex.schedulers.Schedulers; 48 | import javax.inject.Inject; 49 | 50 | import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; 51 | import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_IF_ROOM; 52 | import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; 53 | import static com.squareup.sqlbrite3.SqlBrite.Query; 54 | 55 | public final class ItemsFragment extends Fragment { 56 | private static final String KEY_LIST_ID = "list_id"; 57 | private static final String LIST_QUERY = "SELECT * FROM " 58 | + TodoItem.TABLE 59 | + " WHERE " 60 | + TodoItem.LIST_ID 61 | + " = ? ORDER BY " 62 | + TodoItem.COMPLETE 63 | + " ASC"; 64 | private static final String COUNT_QUERY = "SELECT COUNT(*) FROM " 65 | + TodoItem.TABLE 66 | + " WHERE " 67 | + TodoItem.COMPLETE 68 | + " = " 69 | + Db.BOOLEAN_FALSE 70 | + " AND " 71 | + TodoItem.LIST_ID 72 | + " = ?"; 73 | private static final String TITLE_QUERY = 74 | "SELECT " + TodoList.NAME + " FROM " + TodoList.TABLE + " WHERE " + TodoList.ID + " = ?"; 75 | 76 | public interface Listener { 77 | void onNewItemClicked(long listId); 78 | } 79 | 80 | public static ItemsFragment newInstance(long listId) { 81 | Bundle arguments = new Bundle(); 82 | arguments.putLong(KEY_LIST_ID, listId); 83 | 84 | ItemsFragment fragment = new ItemsFragment(); 85 | fragment.setArguments(arguments); 86 | return fragment; 87 | } 88 | 89 | @Inject BriteDatabase db; 90 | 91 | @BindView(android.R.id.list) ListView listView; 92 | @BindView(android.R.id.empty) View emptyView; 93 | 94 | private Listener listener; 95 | private ItemsAdapter adapter; 96 | private CompositeDisposable disposables; 97 | 98 | private long getListId() { 99 | return getArguments().getLong(KEY_LIST_ID); 100 | } 101 | 102 | @Override public void onAttach(Activity activity) { 103 | if (!(activity instanceof Listener)) { 104 | throw new IllegalStateException("Activity must implement fragment Listener."); 105 | } 106 | 107 | super.onAttach(activity); 108 | TodoApp.getComponent(activity).inject(this); 109 | setHasOptionsMenu(true); 110 | 111 | listener = (Listener) activity; 112 | adapter = new ItemsAdapter(activity); 113 | } 114 | 115 | @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 116 | super.onCreateOptionsMenu(menu, inflater); 117 | 118 | MenuItem item = menu.add(R.string.new_item) 119 | .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { 120 | @Override public boolean onMenuItemClick(MenuItem item) { 121 | listener.onNewItemClicked(getListId()); 122 | return true; 123 | } 124 | }); 125 | MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT); 126 | } 127 | 128 | @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 129 | @Nullable Bundle savedInstanceState) { 130 | return inflater.inflate(R.layout.items, container, false); 131 | } 132 | 133 | @Override 134 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 135 | super.onViewCreated(view, savedInstanceState); 136 | ButterKnife.bind(this, view); 137 | listView.setEmptyView(emptyView); 138 | listView.setAdapter(adapter); 139 | 140 | RxAdapterView.itemClickEvents(listView) // 141 | .observeOn(Schedulers.io()) 142 | .subscribe(new Consumer() { 143 | @Override public void accept(AdapterViewItemClickEvent event) { 144 | boolean newValue = !adapter.getItem(event.position()).complete(); 145 | db.update(TodoItem.TABLE, CONFLICT_NONE, 146 | new TodoItem.Builder().complete(newValue).build(), TodoItem.ID + " = ?", 147 | String.valueOf(event.id())); 148 | } 149 | }); 150 | } 151 | 152 | @Override public void onResume() { 153 | super.onResume(); 154 | String listId = String.valueOf(getListId()); 155 | 156 | disposables = new CompositeDisposable(); 157 | 158 | Observable itemCount = db.createQuery(TodoItem.TABLE, COUNT_QUERY, listId) // 159 | .map(new Function() { 160 | @Override public Integer apply(Query query) { 161 | Cursor cursor = query.run(); 162 | try { 163 | if (!cursor.moveToNext()) { 164 | throw new AssertionError("No rows"); 165 | } 166 | return cursor.getInt(0); 167 | } finally { 168 | cursor.close(); 169 | } 170 | } 171 | }); 172 | Observable listName = 173 | db.createQuery(TodoList.TABLE, TITLE_QUERY, listId).map(new Function() { 174 | @Override public String apply(Query query) { 175 | Cursor cursor = query.run(); 176 | try { 177 | if (!cursor.moveToNext()) { 178 | throw new AssertionError("No rows"); 179 | } 180 | return cursor.getString(0); 181 | } finally { 182 | cursor.close(); 183 | } 184 | } 185 | }); 186 | disposables.add( 187 | Observable.combineLatest(listName, itemCount, new BiFunction() { 188 | @Override public String apply(String listName, Integer itemCount) { 189 | return listName + " (" + itemCount + ")"; 190 | } 191 | }) 192 | .observeOn(AndroidSchedulers.mainThread()) 193 | .subscribe(new Consumer() { 194 | @Override public void accept(String title) throws Exception { 195 | getActivity().setTitle(title); 196 | } 197 | })); 198 | 199 | disposables.add(db.createQuery(TodoItem.TABLE, LIST_QUERY, listId) 200 | .mapToList(TodoItem.MAPPER) 201 | .observeOn(AndroidSchedulers.mainThread()) 202 | .subscribe(adapter)); 203 | } 204 | 205 | @Override public void onPause() { 206 | super.onPause(); 207 | disposables.dispose(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/ListsAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.content.Context; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.BaseAdapter; 23 | import android.widget.TextView; 24 | import io.reactivex.functions.Consumer; 25 | import java.util.Collections; 26 | import java.util.List; 27 | 28 | final class ListsAdapter extends BaseAdapter implements Consumer> { 29 | private final LayoutInflater inflater; 30 | 31 | private List items = Collections.emptyList(); 32 | 33 | public ListsAdapter(Context context) { 34 | this.inflater = LayoutInflater.from(context); 35 | } 36 | 37 | @Override public void accept(List items) { 38 | this.items = items; 39 | notifyDataSetChanged(); 40 | } 41 | 42 | @Override public int getCount() { 43 | return items.size(); 44 | } 45 | 46 | @Override public ListsItem getItem(int position) { 47 | return items.get(position); 48 | } 49 | 50 | @Override public long getItemId(int position) { 51 | return getItem(position).id(); 52 | } 53 | 54 | @Override public boolean hasStableIds() { 55 | return true; 56 | } 57 | 58 | @Override public View getView(int position, View convertView, ViewGroup parent) { 59 | if (convertView == null) { 60 | convertView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); 61 | } 62 | 63 | ListsItem item = getItem(position); 64 | ((TextView) convertView).setText(item.name() + " (" + item.itemCount() + ")"); 65 | 66 | return convertView; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/ListsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.app.Activity; 19 | import android.os.Bundle; 20 | import android.support.annotation.Nullable; 21 | import android.support.v4.app.Fragment; 22 | import android.support.v4.view.MenuItemCompat; 23 | import android.view.LayoutInflater; 24 | import android.view.Menu; 25 | import android.view.MenuInflater; 26 | import android.view.MenuItem; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | import android.widget.ListView; 30 | import butterknife.BindView; 31 | import butterknife.ButterKnife; 32 | import butterknife.OnItemClick; 33 | import com.example.sqlbrite.todo.R; 34 | import com.example.sqlbrite.todo.TodoApp; 35 | import com.squareup.sqlbrite3.BriteDatabase; 36 | import io.reactivex.android.schedulers.AndroidSchedulers; 37 | import io.reactivex.disposables.Disposable; 38 | import javax.inject.Inject; 39 | 40 | import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_IF_ROOM; 41 | import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; 42 | 43 | public final class ListsFragment extends Fragment { 44 | interface Listener { 45 | void onListClicked(long id); 46 | void onNewListClicked(); 47 | } 48 | 49 | static ListsFragment newInstance() { 50 | return new ListsFragment(); 51 | } 52 | 53 | @Inject BriteDatabase db; 54 | 55 | @BindView(android.R.id.list) ListView listView; 56 | @BindView(android.R.id.empty) View emptyView; 57 | 58 | private Listener listener; 59 | private ListsAdapter adapter; 60 | private Disposable disposable; 61 | 62 | @Override public void onAttach(Activity activity) { 63 | if (!(activity instanceof Listener)) { 64 | throw new IllegalStateException("Activity must implement fragment Listener."); 65 | } 66 | 67 | super.onAttach(activity); 68 | TodoApp.getComponent(activity).inject(this); 69 | setHasOptionsMenu(true); 70 | 71 | listener = (Listener) activity; 72 | adapter = new ListsAdapter(activity); 73 | } 74 | 75 | @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 76 | super.onCreateOptionsMenu(menu, inflater); 77 | 78 | MenuItem item = menu.add(R.string.new_list) 79 | .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { 80 | @Override public boolean onMenuItemClick(MenuItem item) { 81 | listener.onNewListClicked(); 82 | return true; 83 | } 84 | }); 85 | MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT); 86 | } 87 | 88 | @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 89 | @Nullable Bundle savedInstanceState) { 90 | return inflater.inflate(R.layout.lists, container, false); 91 | } 92 | 93 | @Override 94 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 95 | super.onViewCreated(view, savedInstanceState); 96 | ButterKnife.bind(this, view); 97 | listView.setEmptyView(emptyView); 98 | listView.setAdapter(adapter); 99 | } 100 | 101 | @OnItemClick(android.R.id.list) void listClicked(long listId) { 102 | listener.onListClicked(listId); 103 | } 104 | 105 | @Override public void onResume() { 106 | super.onResume(); 107 | 108 | getActivity().setTitle("To-Do"); 109 | 110 | disposable = db.createQuery(ListsItem.TABLES, ListsItem.QUERY) 111 | .mapToList(ListsItem.MAPPER) 112 | .observeOn(AndroidSchedulers.mainThread()) 113 | .subscribe(adapter); 114 | } 115 | 116 | @Override public void onPause() { 117 | super.onPause(); 118 | disposable.dispose(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/ListsItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.database.Cursor; 19 | import android.os.Parcelable; 20 | import com.example.sqlbrite.todo.db.Db; 21 | import com.example.sqlbrite.todo.db.TodoItem; 22 | import com.example.sqlbrite.todo.db.TodoList; 23 | import com.google.auto.value.AutoValue; 24 | import io.reactivex.functions.Function; 25 | import java.util.Arrays; 26 | import java.util.Collection; 27 | 28 | @AutoValue 29 | abstract class ListsItem implements Parcelable { 30 | private static String ALIAS_LIST = "list"; 31 | private static String ALIAS_ITEM = "item"; 32 | 33 | private static String LIST_ID = ALIAS_LIST + "." + TodoList.ID; 34 | private static String LIST_NAME = ALIAS_LIST + "." + TodoList.NAME; 35 | private static String ITEM_COUNT = "item_count"; 36 | private static String ITEM_ID = ALIAS_ITEM + "." + TodoItem.ID; 37 | private static String ITEM_LIST_ID = ALIAS_ITEM + "." + TodoItem.LIST_ID; 38 | 39 | public static Collection TABLES = Arrays.asList(TodoList.TABLE, TodoItem.TABLE); 40 | public static String QUERY = "" 41 | + "SELECT " + LIST_ID + ", " + LIST_NAME + ", COUNT(" + ITEM_ID + ") as " + ITEM_COUNT 42 | + " FROM " + TodoList.TABLE + " AS " + ALIAS_LIST 43 | + " LEFT OUTER JOIN " + TodoItem.TABLE + " AS " + ALIAS_ITEM + " ON " + LIST_ID + " = " + ITEM_LIST_ID 44 | + " GROUP BY " + LIST_ID; 45 | 46 | abstract long id(); 47 | abstract String name(); 48 | abstract int itemCount(); 49 | 50 | static Function MAPPER = new Function() { 51 | @Override public ListsItem apply(Cursor cursor) { 52 | long id = Db.getLong(cursor, TodoList.ID); 53 | String name = Db.getString(cursor, TodoList.NAME); 54 | int itemCount = Db.getInt(cursor, ITEM_COUNT); 55 | return new AutoValue_ListsItem(id, name, itemCount); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.os.Bundle; 19 | import android.support.v4.app.FragmentActivity; 20 | import com.example.sqlbrite.todo.R; 21 | 22 | public final class MainActivity extends FragmentActivity 23 | implements ListsFragment.Listener, ItemsFragment.Listener { 24 | 25 | @Override protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | if (savedInstanceState == null) { 28 | getSupportFragmentManager().beginTransaction() 29 | .add(android.R.id.content, ListsFragment.newInstance()) 30 | .commit(); 31 | } 32 | } 33 | 34 | @Override public void onListClicked(long id) { 35 | getSupportFragmentManager().beginTransaction() 36 | .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, 37 | R.anim.slide_out_right) 38 | .replace(android.R.id.content, ItemsFragment.newInstance(id)) 39 | .addToBackStack(null) 40 | .commit(); 41 | } 42 | 43 | @Override public void onNewListClicked() { 44 | NewListFragment.newInstance().show(getSupportFragmentManager(), "new-list"); 45 | } 46 | 47 | @Override public void onNewItemClicked(long listId) { 48 | NewItemFragment.newInstance(listId).show(getSupportFragmentManager(), "new-item"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/NewItemFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.app.Activity; 19 | import android.app.AlertDialog; 20 | import android.app.Dialog; 21 | import android.content.Context; 22 | import android.content.DialogInterface; 23 | import android.os.Bundle; 24 | import android.support.annotation.NonNull; 25 | import android.support.v4.app.DialogFragment; 26 | import android.view.LayoutInflater; 27 | import android.view.View; 28 | import android.widget.EditText; 29 | import com.example.sqlbrite.todo.R; 30 | import com.example.sqlbrite.todo.TodoApp; 31 | import com.example.sqlbrite.todo.db.TodoItem; 32 | import com.jakewharton.rxbinding2.widget.RxTextView; 33 | import com.squareup.sqlbrite3.BriteDatabase; 34 | import io.reactivex.Observable; 35 | import io.reactivex.functions.BiFunction; 36 | import io.reactivex.functions.Consumer; 37 | import io.reactivex.schedulers.Schedulers; 38 | import io.reactivex.subjects.PublishSubject; 39 | import javax.inject.Inject; 40 | 41 | import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; 42 | import static butterknife.ButterKnife.findById; 43 | 44 | public final class NewItemFragment extends DialogFragment { 45 | private static final String KEY_LIST_ID = "list_id"; 46 | 47 | public static NewItemFragment newInstance(long listId) { 48 | Bundle arguments = new Bundle(); 49 | arguments.putLong(KEY_LIST_ID, listId); 50 | 51 | NewItemFragment fragment = new NewItemFragment(); 52 | fragment.setArguments(arguments); 53 | return fragment; 54 | } 55 | 56 | private final PublishSubject createClicked = PublishSubject.create(); 57 | 58 | @Inject BriteDatabase db; 59 | 60 | private long getListId() { 61 | return getArguments().getLong(KEY_LIST_ID); 62 | } 63 | 64 | @Override public void onAttach(Activity activity) { 65 | super.onAttach(activity); 66 | TodoApp.getComponent(activity).inject(this); 67 | } 68 | 69 | @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { 70 | final Context context = getActivity(); 71 | View view = LayoutInflater.from(context).inflate(R.layout.new_item, null); 72 | 73 | EditText name = findById(view, android.R.id.input); 74 | Observable.combineLatest(createClicked, RxTextView.textChanges(name), 75 | new BiFunction() { 76 | @Override public String apply(String ignored, CharSequence text) { 77 | return text.toString(); 78 | } 79 | }) // 80 | .observeOn(Schedulers.io()) 81 | .subscribe(new Consumer() { 82 | @Override public void accept(String description) { 83 | db.insert(TodoItem.TABLE, CONFLICT_NONE, 84 | new TodoItem.Builder().listId(getListId()).description(description).build()); 85 | } 86 | }); 87 | 88 | return new AlertDialog.Builder(context) // 89 | .setTitle(R.string.new_item) 90 | .setView(view) 91 | .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() { 92 | @Override public void onClick(DialogInterface dialog, int which) { 93 | createClicked.onNext("clicked"); 94 | } 95 | }) 96 | .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 97 | @Override public void onClick(@NonNull DialogInterface dialog, int which) { 98 | } 99 | }) 100 | .create(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/sqlbrite/todo/ui/NewListFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.example.sqlbrite.todo.ui; 17 | 18 | import android.app.Activity; 19 | import android.app.AlertDialog; 20 | import android.app.Dialog; 21 | import android.content.Context; 22 | import android.content.DialogInterface; 23 | import android.os.Bundle; 24 | import android.support.annotation.NonNull; 25 | import android.support.v4.app.DialogFragment; 26 | import android.view.LayoutInflater; 27 | import android.view.View; 28 | import android.widget.EditText; 29 | import com.example.sqlbrite.todo.R; 30 | import com.example.sqlbrite.todo.TodoApp; 31 | import com.example.sqlbrite.todo.db.TodoList; 32 | import com.jakewharton.rxbinding2.widget.RxTextView; 33 | import com.squareup.sqlbrite3.BriteDatabase; 34 | import io.reactivex.Observable; 35 | import io.reactivex.functions.BiFunction; 36 | import io.reactivex.functions.Consumer; 37 | import io.reactivex.schedulers.Schedulers; 38 | import io.reactivex.subjects.PublishSubject; 39 | import javax.inject.Inject; 40 | 41 | import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; 42 | import static butterknife.ButterKnife.findById; 43 | 44 | public final class NewListFragment extends DialogFragment { 45 | public static NewListFragment newInstance() { 46 | return new NewListFragment(); 47 | } 48 | 49 | private final PublishSubject createClicked = PublishSubject.create(); 50 | 51 | @Inject BriteDatabase db; 52 | 53 | @Override public void onAttach(Activity activity) { 54 | super.onAttach(activity); 55 | TodoApp.getComponent(activity).inject(this); 56 | } 57 | 58 | @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { 59 | final Context context = getActivity(); 60 | View view = LayoutInflater.from(context).inflate(R.layout.new_list, null); 61 | 62 | EditText name = findById(view, android.R.id.input); 63 | Observable.combineLatest(createClicked, RxTextView.textChanges(name), 64 | new BiFunction() { 65 | @Override public String apply(String ignored, CharSequence text) { 66 | return text.toString(); 67 | } 68 | }) // 69 | .observeOn(Schedulers.io()) 70 | .subscribe(new Consumer() { 71 | @Override public void accept(String name) { 72 | db.insert(TodoList.TABLE, CONFLICT_NONE, new TodoList.Builder().name(name).build()); 73 | } 74 | }); 75 | 76 | return new AlertDialog.Builder(context) // 77 | .setTitle(R.string.new_list) 78 | .setView(view) 79 | .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() { 80 | @Override public void onClick(DialogInterface dialog, int which) { 81 | createClicked.onNext("clicked"); 82 | } 83 | }) 84 | .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 85 | @Override public void onClick(@NonNull DialogInterface dialog, int which) { 86 | } 87 | }) 88 | .create(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sqlbrite/d4cf6ef967452f19a508e14b7c46f64964f8052e/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sqlbrite/d4cf6ef967452f19a508e14b7c46f64964f8052e/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sqlbrite/d4cf6ef967452f19a508e14b7c46f64964f8052e/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sqlbrite/d4cf6ef967452f19a508e14b7c46f64964f8052e/sample/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/items.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/lists.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/new_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/new_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SqlBrite To-Do 4 | 5 | Create 6 | Cancel 7 | New List 8 | Name 9 | New Item 10 | Description 11 | 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sqlbrite' 2 | include ':sqlbrite-kotlin' 3 | include ':sqlbrite-lint' 4 | include ':sample' 5 | 6 | rootProject.name = 'sqlbrite-root' 7 | -------------------------------------------------------------------------------- /sqlbrite-kotlin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'org.jetbrains.kotlin.android' 3 | 4 | dependencies { 5 | api project(':sqlbrite') 6 | api rootProject.ext.kotlinStdLib 7 | } 8 | 9 | android { 10 | compileSdkVersion versions.compileSdk 11 | 12 | defaultConfig { 13 | minSdkVersion versions.minSdk 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_7 18 | targetCompatibility JavaVersion.VERSION_1_7 19 | } 20 | 21 | lintOptions { 22 | textOutput 'stdout' 23 | textReport true 24 | } 25 | 26 | // TODO replace with https://issuetracker.google.com/issues/72050365 once released. 27 | libraryVariants.all { 28 | it.generateBuildConfig.enabled = false 29 | } 30 | } 31 | 32 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 33 | kotlinOptions { 34 | freeCompilerArgs = ['-Xno-param-assertions'] 35 | } 36 | } 37 | 38 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') 39 | -------------------------------------------------------------------------------- /sqlbrite-kotlin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=sqlbrite-kotlin 2 | POM_NAME=SqlBrite (Kotlin Extensions) 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /sqlbrite-kotlin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sqlbrite-kotlin/src/main/java/com/squareup/sqlbrite3/extensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 | @file:Suppress("NOTHING_TO_INLINE") // Extensions provided for intentional convenience. 17 | 18 | package com.squareup.sqlbrite3 19 | 20 | import android.database.Cursor 21 | import android.support.annotation.RequiresApi 22 | import com.squareup.sqlbrite3.BriteDatabase.Transaction 23 | import com.squareup.sqlbrite3.SqlBrite.Query 24 | import io.reactivex.Observable 25 | import java.util.Optional 26 | 27 | typealias Mapper = (Cursor) -> T 28 | 29 | /** 30 | * Transforms an observable of single-row [Query] to an observable of `T` using `mapper`. 31 | * 32 | * It is an error for a query to pass through this operator with more than 1 row in its result set. 33 | * Use `LIMIT 1` on the underlying SQL query to prevent this. Result sets with 0 rows do not emit 34 | * an item. 35 | * 36 | * This operator ignores null cursors returned from [Query.run]. 37 | * 38 | * @param mapper Maps the current [Cursor] row to `T`. May not return null. 39 | */ 40 | inline fun Observable.mapToOne(noinline mapper: Mapper): Observable 41 | = lift(Query.mapToOne(mapper)) 42 | 43 | /** 44 | * Transforms an observable of single-row [Query] to an observable of `T` using `mapper` 45 | * 46 | * It is an error for a query to pass through this operator with more than 1 row in its result set. 47 | * Use `LIMIT 1` on the underlying SQL query to prevent this. Result sets with 0 rows emit 48 | * `default`. 49 | * 50 | * This operator emits `defaultValue` if null is returned from [Query.run]. 51 | * 52 | * @param mapper Maps the current [Cursor] row to `T`. May not return null. 53 | * @param default Value returned if result set is empty 54 | */ 55 | inline fun Observable.mapToOneOrDefault(default: T, noinline mapper: Mapper): Observable 56 | = lift(Query.mapToOneOrDefault(mapper, default)) 57 | 58 | /** 59 | * Transforms an observable of single-row [Query] to an observable of `T` using `mapper. 60 | * 61 | * It is an error for a query to pass through this operator with more than 1 row in its result set. 62 | * Use `LIMIT 1` on the underlying SQL query to prevent this. Result sets with 0 rows emit 63 | * `default`. 64 | * 65 | * This operator ignores null cursors returned from [Query.run]. 66 | * 67 | * @param mapper Maps the current [Cursor] row to `T`. May not return null. 68 | */ 69 | @RequiresApi(24) 70 | inline fun Observable.mapToOptional(noinline mapper: Mapper): Observable> 71 | = lift(Query.mapToOptional(mapper)) 72 | 73 | /** 74 | * Transforms an observable of [Query] to `List` using `mapper` for each row. 75 | * 76 | * Be careful using this operator as it will always consume the entire cursor and create objects 77 | * for each row, every time this observable emits a new query. On tables whose queries update 78 | * frequently or very large result sets this can result in the creation of many objects. 79 | * 80 | * This operator ignores null cursors returned from [Query.run]. 81 | * 82 | * @param mapper Maps the current [Cursor] row to `T`. May not return null. 83 | */ 84 | inline fun Observable.mapToList(noinline mapper: Mapper): Observable> 85 | = lift(Query.mapToList(mapper)) 86 | 87 | /** 88 | * Run the database interactions in `body` inside of a transaction. 89 | * 90 | * @param exclusive Uses [BriteDatabase.newTransaction] if true, otherwise 91 | * [BriteDatabase.newNonExclusiveTransaction]. 92 | */ 93 | inline fun BriteDatabase.inTransaction( 94 | exclusive: Boolean = true, 95 | body: BriteDatabase.(Transaction) -> T 96 | ): T { 97 | val transaction = if (exclusive) newTransaction() else newNonExclusiveTransaction() 98 | try { 99 | val result = body(transaction) 100 | transaction.markSuccessful() 101 | return result 102 | } finally { 103 | transaction.end() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sqlbrite-lint/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | dependencies { 4 | compileOnly rootProject.ext.kotlinStdLib 5 | compileOnly rootProject.ext.lintApi 6 | 7 | testImplementation rootProject.ext.junit 8 | testImplementation rootProject.ext.lint 9 | testImplementation rootProject.ext.lintTests 10 | } 11 | 12 | jar { 13 | manifest { 14 | attributes("Lint-Registry-v2": "com.squareup.sqlbrite3.BriteIssueRegistry") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sqlbrite-lint/src/main/java/com/squareup/sqlbrite3/BriteIssueRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3 17 | 18 | import com.android.tools.lint.client.api.IssueRegistry 19 | 20 | class BriteIssueRegistry : IssueRegistry() { 21 | 22 | override fun getIssues() = listOf(SqlBriteArgCountDetector.ISSUE) 23 | } 24 | -------------------------------------------------------------------------------- /sqlbrite-lint/src/main/java/com/squareup/sqlbrite3/SqlBriteArgCountDetector.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3 17 | 18 | import com.android.tools.lint.detector.api.Category 19 | import com.android.tools.lint.detector.api.ConstantEvaluator.evaluateString 20 | import com.android.tools.lint.detector.api.Detector 21 | import com.android.tools.lint.detector.api.Implementation 22 | import com.android.tools.lint.detector.api.Issue 23 | import com.android.tools.lint.detector.api.JavaContext 24 | import com.android.tools.lint.detector.api.Scope.JAVA_FILE 25 | import com.android.tools.lint.detector.api.Scope.TEST_SOURCES 26 | import com.android.tools.lint.detector.api.Severity 27 | import com.intellij.psi.PsiMethod 28 | import org.jetbrains.uast.UCallExpression 29 | import java.util.EnumSet 30 | 31 | private const val BRITE_DATABASE = "com.squareup.sqlbrite3.BriteDatabase" 32 | private const val QUERY_METHOD_NAME = "query" 33 | private const val CREATE_QUERY_METHOD_NAME = "createQuery" 34 | 35 | class SqlBriteArgCountDetector : Detector(), Detector.UastScanner { 36 | 37 | companion object { 38 | 39 | val ISSUE: Issue = Issue.create( 40 | "SqlBriteArgCount", 41 | "Number of provided arguments doesn't match number " + 42 | "of arguments specified in query", 43 | "When providing arguments to query you need to provide the same amount of " + 44 | "arguments that is specified in query.", 45 | Category.MESSAGES, 46 | 9, 47 | Severity.ERROR, 48 | Implementation(SqlBriteArgCountDetector::class.java, EnumSet.of(JAVA_FILE, TEST_SOURCES))) 49 | } 50 | 51 | override fun getApplicableMethodNames() = listOf(CREATE_QUERY_METHOD_NAME, QUERY_METHOD_NAME) 52 | 53 | override fun visitMethod(context: JavaContext, call: UCallExpression, method: PsiMethod) { 54 | val evaluator = context.evaluator 55 | 56 | if (evaluator.isMemberInClass(method, BRITE_DATABASE)) { 57 | // Skip non varargs overloads. 58 | if (!method.isVarArgs) return 59 | 60 | // Position of sql parameter depends on method. 61 | val sql = evaluateString(context, 62 | call.valueArguments[if (call.isQueryMethod()) 0 else 1], true) ?: return 63 | 64 | // Count only vararg arguments. 65 | val argumentsCount = call.valueArgumentCount - if (call.isQueryMethod()) 1 else 2 66 | val questionMarksCount = sql.count { it == '?' } 67 | if (argumentsCount != questionMarksCount) { 68 | val requiredArguments = "$questionMarksCount ${"argument".pluralize(questionMarksCount)}" 69 | val actualArguments = "$argumentsCount ${"argument".pluralize(argumentsCount)}" 70 | context.report(ISSUE, call, context.getLocation(call), "Wrong argument count, " + 71 | "query $sql requires $requiredArguments, but was provided $actualArguments") 72 | } 73 | } 74 | } 75 | 76 | private fun UCallExpression.isQueryMethod() = methodName == QUERY_METHOD_NAME 77 | 78 | private fun String.pluralize(count: Int) = if (count == 1) this else this + "s" 79 | } -------------------------------------------------------------------------------- /sqlbrite-lint/src/test/java/com/squareup/sqlbrite3/SqlBriteArgCountDetectorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3 17 | 18 | import com.android.tools.lint.checks.infrastructure.TestFiles.java 19 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint 20 | import org.junit.Test 21 | 22 | class SqlBriteArgCountDetectorTest { 23 | 24 | companion object { 25 | private val BRITE_DATABASE_STUB = java( 26 | """ 27 | package com.squareup.sqlbrite3; 28 | 29 | public final class BriteDatabase { 30 | 31 | public void query(String sql, Object... args) { 32 | } 33 | 34 | public void createQuery(String table, String sql, Object... args) { 35 | } 36 | 37 | // simulate createQuery with SupportSQLiteQuery query parameter 38 | public void createQuery(String table, int something) { 39 | } 40 | } 41 | """.trimIndent() 42 | ) 43 | } 44 | 45 | @Test 46 | fun cleanCaseWithWithQueryAsLiteral() { 47 | lint().files( 48 | BRITE_DATABASE_STUB, 49 | java( 50 | """ 51 | package test.pkg; 52 | 53 | import com.squareup.sqlbrite3.BriteDatabase; 54 | 55 | public class Test { 56 | private static final String QUERY = "SELECT name FROM table WHERE id = ?"; 57 | 58 | public void test() { 59 | BriteDatabase db = new BriteDatabase(); 60 | db.query(QUERY, "id"); 61 | } 62 | 63 | } 64 | """.trimIndent())) 65 | .issues(SqlBriteArgCountDetector.ISSUE) 66 | .run() 67 | .expectClean() 68 | } 69 | 70 | @Test 71 | fun cleanCaseWithQueryAsBinaryExpression() { 72 | lint().files( 73 | BRITE_DATABASE_STUB, 74 | java( 75 | """ 76 | package test.pkg; 77 | 78 | import com.squareup.sqlbrite3.BriteDatabase; 79 | 80 | public class Test { 81 | private static final String QUERY = "SELECT name FROM table WHERE "; 82 | 83 | public void test() { 84 | BriteDatabase db = new BriteDatabase(); 85 | db.query(QUERY + "id = ?", "id"); 86 | } 87 | 88 | } 89 | """.trimIndent())) 90 | .issues(SqlBriteArgCountDetector.ISSUE) 91 | .run() 92 | .expectClean() 93 | } 94 | 95 | @Test 96 | fun cleanCaseWithQueryThatCantBeEvaluated() { 97 | lint().files( 98 | BRITE_DATABASE_STUB, 99 | java( 100 | """ 101 | package test.pkg; 102 | 103 | import com.squareup.sqlbrite3.BriteDatabase; 104 | 105 | public class Test { 106 | private static final String QUERY = "SELECT name FROM table WHERE id = ?"; 107 | 108 | public void test() { 109 | BriteDatabase db = new BriteDatabase(); 110 | db.query(query(), "id"); 111 | } 112 | 113 | private String query() { 114 | return QUERY + " age = ?"; 115 | } 116 | 117 | } 118 | """.trimIndent())) 119 | .issues(SqlBriteArgCountDetector.ISSUE) 120 | .run() 121 | .expectClean() 122 | } 123 | 124 | @Test 125 | fun cleanCaseWithNonVarargMethodCall() { 126 | lint().files( 127 | BRITE_DATABASE_STUB, 128 | java( 129 | """ 130 | package test.pkg; 131 | 132 | import com.squareup.sqlbrite3.BriteDatabase; 133 | 134 | public class Test { 135 | 136 | public void test() { 137 | BriteDatabase db = new BriteDatabase(); 138 | db.createQuery("table", 42); 139 | } 140 | 141 | } 142 | """.trimIndent())) 143 | .issues(SqlBriteArgCountDetector.ISSUE) 144 | .run() 145 | .expectClean() 146 | } 147 | 148 | @Test 149 | fun queryMethodWithWrongNumberOfArguments() { 150 | lint().files( 151 | BRITE_DATABASE_STUB, 152 | java( 153 | """ 154 | package test.pkg; 155 | 156 | import com.squareup.sqlbrite3.BriteDatabase; 157 | 158 | public class Test { 159 | private static final String QUERY = "SELECT name FROM table WHERE id = ?"; 160 | 161 | public void test() { 162 | BriteDatabase db = new BriteDatabase(); 163 | db.query(QUERY); 164 | } 165 | 166 | } 167 | """.trimIndent())) 168 | .issues(SqlBriteArgCountDetector.ISSUE) 169 | .run() 170 | .expect("src/test/pkg/Test.java:10: " + 171 | "Error: Wrong argument count, query SELECT name FROM table WHERE id = ?" + 172 | " requires 1 argument, but was provided 0 arguments [SqlBriteArgCount]\n" + 173 | " db.query(QUERY);\n" + 174 | " ~~~~~~~~~~~~~~~\n" + 175 | "1 errors, 0 warnings") 176 | } 177 | 178 | @Test 179 | fun createQueryMethodWithWrongNumberOfArguments() { 180 | lint().files( 181 | BRITE_DATABASE_STUB, 182 | java( 183 | """ 184 | package test.pkg; 185 | 186 | import com.squareup.sqlbrite3.BriteDatabase; 187 | 188 | public class Test { 189 | private static final String QUERY = "SELECT name FROM table WHERE id = ?"; 190 | 191 | public void test() { 192 | BriteDatabase db = new BriteDatabase(); 193 | db.createQuery("table", QUERY); 194 | } 195 | 196 | } 197 | """.trimIndent())) 198 | .issues(SqlBriteArgCountDetector.ISSUE) 199 | .run() 200 | .expect("src/test/pkg/Test.java:10: " + 201 | "Error: Wrong argument count, query SELECT name FROM table WHERE id = ?" + 202 | " requires 1 argument, but was provided 0 arguments [SqlBriteArgCount]\n" + 203 | " db.createQuery(\"table\", QUERY);\n" + 204 | " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + 205 | "1 errors, 0 warnings") 206 | } 207 | } -------------------------------------------------------------------------------- /sqlbrite/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | dependencies { 4 | api rootProject.ext.rxJava 5 | api rootProject.ext.supportSqlite 6 | implementation rootProject.ext.supportAnnotations 7 | 8 | androidTestImplementation rootProject.ext.supportTestRunner 9 | androidTestImplementation rootProject.ext.truth 10 | androidTestImplementation rootProject.ext.supportSqliteFramework 11 | 12 | lintChecks project(':sqlbrite-lint') 13 | } 14 | 15 | android { 16 | compileSdkVersion versions.compileSdk 17 | 18 | defaultConfig { 19 | minSdkVersion versions.minSdk 20 | 21 | testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_7 26 | targetCompatibility JavaVersion.VERSION_1_7 27 | } 28 | 29 | lintOptions { 30 | textOutput 'stdout' 31 | textReport true 32 | } 33 | 34 | // TODO replace with https://issuetracker.google.com/issues/72050365 once released. 35 | libraryVariants.all { 36 | it.generateBuildConfig.enabled = false 37 | } 38 | } 39 | 40 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') 41 | -------------------------------------------------------------------------------- /sqlbrite/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=sqlbrite 2 | POM_NAME=SqlBrite 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/BlockingRecordingObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import static com.google.common.truth.Truth.assertThat; 19 | import static java.util.concurrent.TimeUnit.SECONDS; 20 | 21 | final class BlockingRecordingObserver extends RecordingObserver { 22 | protected Object takeEvent() { 23 | try { 24 | Object item = events.pollFirst(1, SECONDS); 25 | if (item == null) { 26 | throw new AssertionError("No items."); 27 | } 28 | return item; 29 | } catch (InterruptedException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | @Override public void assertNoMoreEvents() { 35 | try { 36 | assertThat(events.pollFirst(1, SECONDS)).isNull(); 37 | } catch (InterruptedException e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/BriteContentResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.content.ContentResolver; 19 | import android.content.ContentValues; 20 | import android.database.Cursor; 21 | import android.database.MatrixCursor; 22 | import android.net.Uri; 23 | import android.test.ProviderTestCase2; 24 | import android.test.mock.MockContentProvider; 25 | import com.squareup.sqlbrite3.SqlBrite.Query; 26 | import io.reactivex.Observable; 27 | import io.reactivex.ObservableSource; 28 | import io.reactivex.ObservableTransformer; 29 | import io.reactivex.subjects.PublishSubject; 30 | import java.util.ArrayList; 31 | import java.util.LinkedHashMap; 32 | import java.util.List; 33 | import java.util.Map; 34 | 35 | import static com.google.common.truth.Truth.assertThat; 36 | 37 | public final class BriteContentResolverTest 38 | extends ProviderTestCase2 { 39 | private static final Uri AUTHORITY = Uri.parse("content://test_authority"); 40 | private static final Uri TABLE = AUTHORITY.buildUpon().appendPath("test_table").build(); 41 | private static final String KEY = "test_key"; 42 | private static final String VALUE = "test_value"; 43 | 44 | private final List logs = new ArrayList<>(); 45 | private final RecordingObserver o = new BlockingRecordingObserver(); 46 | private final TestScheduler scheduler = new TestScheduler(); 47 | private final PublishSubject killSwitch = PublishSubject.create(); 48 | 49 | private ContentResolver contentResolver; 50 | private BriteContentResolver db; 51 | 52 | public BriteContentResolverTest() { 53 | super(TestContentProvider.class, AUTHORITY.getAuthority()); 54 | } 55 | 56 | @Override protected void setUp() throws Exception { 57 | super.setUp(); 58 | contentResolver = getMockContentResolver(); 59 | 60 | SqlBrite.Logger logger = new SqlBrite.Logger() { 61 | @Override public void log(String message) { 62 | logs.add(message); 63 | } 64 | }; 65 | ObservableTransformer queryTransformer = 66 | new ObservableTransformer() { 67 | @Override public ObservableSource apply(Observable upstream) { 68 | return upstream.takeUntil(killSwitch); 69 | } 70 | }; 71 | db = new BriteContentResolver(contentResolver, logger, scheduler, queryTransformer); 72 | 73 | getProvider().init(getContext().getContentResolver()); 74 | } 75 | 76 | @Override public void tearDown() { 77 | o.assertNoMoreEvents(); 78 | o.dispose(); 79 | } 80 | 81 | public void testLoggerEnabled() { 82 | db.setLoggingEnabled(true); 83 | 84 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 85 | o.assertCursor().isExhausted(); 86 | 87 | contentResolver.insert(TABLE, values("key1", "value1")); 88 | o.assertCursor().hasRow("key1", "value1").isExhausted(); 89 | assertThat(logs).isNotEmpty(); 90 | } 91 | 92 | public void testLoggerDisabled() { 93 | db.setLoggingEnabled(false); 94 | 95 | contentResolver.insert(TABLE, values("key1", "value1")); 96 | assertThat(logs).isEmpty(); 97 | } 98 | 99 | public void testCreateQueryObservesInsert() { 100 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 101 | o.assertCursor().isExhausted(); 102 | 103 | contentResolver.insert(TABLE, values("key1", "val1")); 104 | o.assertCursor().hasRow("key1", "val1").isExhausted(); 105 | } 106 | 107 | public void testCreateQueryObservesUpdate() { 108 | contentResolver.insert(TABLE, values("key1", "val1")); 109 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 110 | o.assertCursor().hasRow("key1", "val1").isExhausted(); 111 | 112 | contentResolver.update(TABLE, values("key1", "val2"), null, null); 113 | o.assertCursor().hasRow("key1", "val2").isExhausted(); 114 | } 115 | 116 | public void testCreateQueryObservesDelete() { 117 | contentResolver.insert(TABLE, values("key1", "val1")); 118 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 119 | o.assertCursor().hasRow("key1", "val1").isExhausted(); 120 | 121 | contentResolver.delete(TABLE, null, null); 122 | o.assertCursor().isExhausted(); 123 | } 124 | 125 | public void testUnsubscribeDoesNotTrigger() { 126 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 127 | o.assertCursor().isExhausted(); 128 | o.dispose(); 129 | 130 | contentResolver.insert(TABLE, values("key1", "val1")); 131 | o.assertNoMoreEvents(); 132 | assertThat(logs).isEmpty(); 133 | } 134 | 135 | public void testQueryNotNotifiedWhenQueryTransformerDisposed() { 136 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 137 | o.assertCursor().isExhausted(); 138 | 139 | killSwitch.onNext("kill"); 140 | o.assertIsCompleted(); 141 | 142 | contentResolver.insert(TABLE, values("key1", "val1")); 143 | o.assertNoMoreEvents(); 144 | } 145 | 146 | public void testInitialValueAndTriggerUsesScheduler() { 147 | scheduler.runTasksImmediately(false); 148 | 149 | db.createQuery(TABLE, null, null, null, null, false).subscribe(o); 150 | o.assertNoMoreEvents(); 151 | scheduler.triggerActions(); 152 | o.assertCursor().isExhausted(); 153 | 154 | contentResolver.insert(TABLE, values("key1", "val1")); 155 | o.assertNoMoreEvents(); 156 | scheduler.triggerActions(); 157 | o.assertCursor().hasRow("key1", "val1").isExhausted(); 158 | } 159 | 160 | private ContentValues values(String key, String value) { 161 | ContentValues result = new ContentValues(); 162 | result.put(KEY, key); 163 | result.put(VALUE, value); 164 | return result; 165 | } 166 | 167 | public static final class TestContentProvider extends MockContentProvider { 168 | private final Map storage = new LinkedHashMap<>(); 169 | 170 | private ContentResolver contentResolver; 171 | 172 | void init(ContentResolver contentResolver) { 173 | this.contentResolver = contentResolver; 174 | } 175 | 176 | @Override public Uri insert(Uri uri, ContentValues values) { 177 | storage.put(values.getAsString(KEY), values.getAsString(VALUE)); 178 | contentResolver.notifyChange(uri, null); 179 | return Uri.parse(AUTHORITY + "/" + values.getAsString(KEY)); 180 | } 181 | 182 | @Override public int update(Uri uri, ContentValues values, String selection, 183 | String[] selectionArgs) { 184 | for (String key : storage.keySet()) { 185 | storage.put(key, values.getAsString(VALUE)); 186 | } 187 | contentResolver.notifyChange(uri, null); 188 | return storage.size(); 189 | } 190 | 191 | @Override public int delete(Uri uri, String selection, String[] selectionArgs) { 192 | int result = storage.size(); 193 | storage.clear(); 194 | contentResolver.notifyChange(uri, null); 195 | return result; 196 | } 197 | 198 | @Override public Cursor query(Uri uri, String[] projection, String selection, 199 | String[] selectionArgs, String sortOrder) { 200 | MatrixCursor result = new MatrixCursor(new String[] { KEY, VALUE }); 201 | for (Map.Entry entry : storage.entrySet()) { 202 | result.addRow(new Object[] { entry.getKey(), entry.getValue() }); 203 | } 204 | return result; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/QueryObservableTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.database.Cursor; 19 | import android.database.MatrixCursor; 20 | import com.squareup.sqlbrite3.QueryObservable; 21 | import com.squareup.sqlbrite3.SqlBrite.Query; 22 | import io.reactivex.Observable; 23 | import io.reactivex.functions.Function; 24 | import org.junit.Test; 25 | 26 | public final class QueryObservableTest { 27 | @Test public void mapToListThrowsFromQueryRun() { 28 | final IllegalStateException error = new IllegalStateException("test exception"); 29 | Query query = new Query() { 30 | @Override public Cursor run() { 31 | throw error; 32 | } 33 | }; 34 | new QueryObservable(Observable.just(query)) // 35 | .mapToList(new Function() { 36 | @Override public Object apply(Cursor cursor) { 37 | throw new AssertionError("Must not be called"); 38 | } 39 | }) // 40 | .test() // 41 | .assertNoValues() // 42 | .assertError(error); 43 | } 44 | 45 | @Test public void mapToListThrowsFromMapFunction() { 46 | Query query = new Query() { 47 | @Override public Cursor run() { 48 | MatrixCursor cursor = new MatrixCursor(new String[] { "col1" }); 49 | cursor.addRow(new Object[] { "value1" }); 50 | return cursor; 51 | } 52 | }; 53 | 54 | final IllegalStateException error = new IllegalStateException("test exception"); 55 | new QueryObservable(Observable.just(query)) // 56 | .mapToList(new Function() { 57 | @Override public Object apply(Cursor cursor) { 58 | throw error; 59 | } 60 | }) // 61 | .test() // 62 | .assertNoValues() // 63 | .assertError(error); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/QueryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 19 | import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration; 20 | import android.arch.persistence.db.SupportSQLiteOpenHelper.Factory; 21 | import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; 22 | import android.database.Cursor; 23 | import android.os.Build; 24 | import android.support.annotation.Nullable; 25 | import android.support.test.InstrumentationRegistry; 26 | import android.support.test.filters.SdkSuppress; 27 | import com.squareup.sqlbrite3.SqlBrite.Query; 28 | import com.squareup.sqlbrite3.TestDb.Employee; 29 | import io.reactivex.Observable; 30 | import io.reactivex.functions.Function; 31 | import io.reactivex.observers.TestObserver; 32 | import io.reactivex.schedulers.Schedulers; 33 | import java.util.List; 34 | import java.util.Optional; 35 | import org.junit.Before; 36 | import org.junit.Test; 37 | 38 | import static com.google.common.truth.Truth.assertThat; 39 | import static com.squareup.sqlbrite3.TestDb.Employee.MAPPER; 40 | import static com.squareup.sqlbrite3.TestDb.SELECT_EMPLOYEES; 41 | import static com.squareup.sqlbrite3.TestDb.TABLE_EMPLOYEE; 42 | import static org.junit.Assert.fail; 43 | 44 | public final class QueryTest { 45 | private BriteDatabase db; 46 | 47 | @Before public void setUp() { 48 | Configuration configuration = Configuration.builder(InstrumentationRegistry.getContext()) 49 | .callback(new TestDb()) 50 | .build(); 51 | 52 | Factory factory = new FrameworkSQLiteOpenHelperFactory(); 53 | SupportSQLiteOpenHelper helper = factory.create(configuration); 54 | 55 | SqlBrite sqlBrite = new SqlBrite.Builder().build(); 56 | db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.trampoline()); 57 | } 58 | 59 | @Test public void mapToOne() { 60 | Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") 61 | .lift(Query.mapToOne(MAPPER)) 62 | .blockingFirst(); 63 | assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); 64 | } 65 | 66 | @Test public void mapToOneThrowsWhenMapperReturnsNull() { 67 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") 68 | .lift(Query.mapToOne(new Function() { 69 | @Override public Employee apply(Cursor cursor) throws Exception { 70 | return null; 71 | } 72 | })) 73 | .test() 74 | .assertError(NullPointerException.class) 75 | .assertErrorMessage("QueryToOne mapper returned null"); 76 | } 77 | 78 | @Test public void mapToOneThrowsOnMultipleRows() { 79 | Observable employees = 80 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") // 81 | .lift(Query.mapToOne(MAPPER)); 82 | try { 83 | employees.blockingFirst(); 84 | fail(); 85 | } catch (IllegalStateException e) { 86 | assertThat(e).hasMessage("Cursor returned more than 1 row"); 87 | } 88 | } 89 | 90 | @Test public void mapToOneIgnoresNullCursor() { 91 | Query nully = new Query() { 92 | @Nullable @Override public Cursor run() { 93 | return null; 94 | } 95 | }; 96 | 97 | TestObserver observer = new TestObserver<>(); 98 | Observable.just(nully) 99 | .lift(Query.mapToOne(MAPPER)) 100 | .subscribe(observer); 101 | 102 | observer.assertNoValues(); 103 | observer.assertComplete(); 104 | } 105 | 106 | @Test public void mapToOneOrDefault() { 107 | Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") 108 | .lift(Query.mapToOneOrDefault( 109 | MAPPER, new Employee("fred", "Fred Frederson"))) 110 | .blockingFirst(); 111 | assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); 112 | } 113 | 114 | @Test public void mapToOneOrDefaultDisallowsNullDefault() { 115 | try { 116 | Query.mapToOneOrDefault(MAPPER, null); 117 | fail(); 118 | } catch (NullPointerException e) { 119 | assertThat(e).hasMessage("defaultValue == null"); 120 | } 121 | } 122 | 123 | @Test public void mapToOneOrDefaultThrowsWhenMapperReturnsNull() { 124 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") 125 | .lift(Query.mapToOneOrDefault(new Function() { 126 | @Override public Employee apply(Cursor cursor) throws Exception { 127 | return null; 128 | } 129 | }, new Employee("fred", "Fred Frederson"))) 130 | .test() 131 | .assertError(NullPointerException.class) 132 | .assertErrorMessage("QueryToOne mapper returned null"); 133 | } 134 | 135 | @Test public void mapToOneOrDefaultThrowsOnMultipleRows() { 136 | Observable employees = 137 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") // 138 | .lift(Query.mapToOneOrDefault( 139 | MAPPER, new Employee("fred", "Fred Frederson"))); 140 | try { 141 | employees.blockingFirst(); 142 | fail(); 143 | } catch (IllegalStateException e) { 144 | assertThat(e).hasMessage("Cursor returned more than 1 row"); 145 | } 146 | } 147 | 148 | @Test public void mapToOneOrDefaultReturnsDefaultWhenNullCursor() { 149 | Employee defaultEmployee = new Employee("bob", "Bob Bobberson"); 150 | Query nully = new Query() { 151 | @Nullable @Override public Cursor run() { 152 | return null; 153 | } 154 | }; 155 | 156 | TestObserver observer = new TestObserver<>(); 157 | Observable.just(nully) 158 | .lift(Query.mapToOneOrDefault(MAPPER, defaultEmployee)) 159 | .subscribe(observer); 160 | 161 | observer.assertValues(defaultEmployee); 162 | observer.assertComplete(); 163 | } 164 | 165 | @Test public void mapToList() { 166 | List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) 167 | .lift(Query.mapToList(MAPPER)) 168 | .blockingFirst(); 169 | assertThat(employees).containsExactly( // 170 | new Employee("alice", "Alice Allison"), // 171 | new Employee("bob", "Bob Bobberson"), // 172 | new Employee("eve", "Eve Evenson")); 173 | } 174 | 175 | @Test public void mapToListEmptyWhenNoRows() { 176 | List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " WHERE 1=2") 177 | .lift(Query.mapToList(MAPPER)) 178 | .blockingFirst(); 179 | assertThat(employees).isEmpty(); 180 | } 181 | 182 | @Test public void mapToListReturnsNullOnMapperNull() { 183 | Function mapToNull = new Function() { 184 | private int count; 185 | 186 | @Override public Employee apply(Cursor cursor) throws Exception { 187 | return count++ == 2 ? null : MAPPER.apply(cursor); 188 | } 189 | }; 190 | List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) // 191 | .lift(Query.mapToList(mapToNull)) // 192 | .blockingFirst(); 193 | 194 | assertThat(employees).containsExactly( 195 | new Employee("alice", "Alice Allison"), 196 | new Employee("bob", "Bob Bobberson"), 197 | null); 198 | } 199 | 200 | @Test public void mapToListIgnoresNullCursor() { 201 | Query nully = new Query() { 202 | @Nullable @Override public Cursor run() { 203 | return null; 204 | } 205 | }; 206 | 207 | TestObserver> subscriber = new TestObserver<>(); 208 | Observable.just(nully) 209 | .lift(Query.mapToList(MAPPER)) 210 | .subscribe(subscriber); 211 | 212 | subscriber.assertNoValues(); 213 | subscriber.assertComplete(); 214 | } 215 | 216 | @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 217 | @Test public void mapToOptional() { 218 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") 219 | .lift(Query.mapToOptional(MAPPER)) 220 | .test() 221 | .assertValue(Optional.of(new Employee("alice", "Alice Allison"))); 222 | } 223 | 224 | @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 225 | @Test public void mapToOptionalThrowsWhenMapperReturnsNull() { 226 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") 227 | .lift(Query.mapToOptional(new Function() { 228 | @Override public Employee apply(Cursor cursor) throws Exception { 229 | return null; 230 | } 231 | })) 232 | .test() 233 | .assertError(NullPointerException.class) 234 | .assertErrorMessage("QueryToOne mapper returned null"); 235 | } 236 | 237 | @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 238 | @Test public void mapToOptionalThrowsOnMultipleRows() { 239 | db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") // 240 | .lift(Query.mapToOptional(MAPPER)) 241 | .test() 242 | .assertError(IllegalStateException.class) 243 | .assertErrorMessage("Cursor returned more than 1 row"); 244 | } 245 | 246 | @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 247 | @Test public void mapToOptionalIgnoresNullCursor() { 248 | Query nully = new Query() { 249 | @Nullable @Override public Cursor run() { 250 | return null; 251 | } 252 | }; 253 | 254 | Observable.just(nully) 255 | .lift(Query.mapToOptional(MAPPER)) 256 | .test() 257 | .assertValue(Optional.empty()); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/RecordingObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.database.Cursor; 19 | import android.util.Log; 20 | import io.reactivex.observers.DisposableObserver; 21 | import java.util.concurrent.BlockingDeque; 22 | import java.util.concurrent.LinkedBlockingDeque; 23 | 24 | import static com.google.common.truth.Truth.assertThat; 25 | import static com.squareup.sqlbrite3.SqlBrite.Query; 26 | 27 | class RecordingObserver extends DisposableObserver { 28 | private static final Object COMPLETED = ""; 29 | private static final String TAG = RecordingObserver.class.getSimpleName(); 30 | 31 | final BlockingDeque events = new LinkedBlockingDeque<>(); 32 | 33 | @Override public final void onComplete() { 34 | Log.d(TAG, "onCompleted"); 35 | events.add(COMPLETED); 36 | } 37 | 38 | @Override public final void onError(Throwable e) { 39 | Log.d(TAG, "onError " + e.getClass().getSimpleName() + " " + e.getMessage()); 40 | events.add(e); 41 | } 42 | 43 | @Override public final void onNext(Query value) { 44 | Log.d(TAG, "onNext " + value); 45 | events.add(value.run()); 46 | } 47 | 48 | protected Object takeEvent() { 49 | Object item = events.removeFirst(); 50 | if (item == null) { 51 | throw new AssertionError("No items."); 52 | } 53 | return item; 54 | } 55 | 56 | public final CursorAssert assertCursor() { 57 | Object event = takeEvent(); 58 | assertThat(event).isInstanceOf(Cursor.class); 59 | return new CursorAssert((Cursor) event); 60 | } 61 | 62 | public final void assertErrorContains(String expected) { 63 | Object event = takeEvent(); 64 | assertThat(event).isInstanceOf(Throwable.class); 65 | assertThat(((Throwable) event).getMessage()).contains(expected); 66 | } 67 | 68 | public final void assertIsCompleted() { 69 | Object event = takeEvent(); 70 | assertThat(event).isEqualTo(COMPLETED); 71 | } 72 | 73 | public void assertNoMoreEvents() { 74 | assertThat(events).isEmpty(); 75 | } 76 | 77 | static final class CursorAssert { 78 | private final Cursor cursor; 79 | private int row = 0; 80 | 81 | CursorAssert(Cursor cursor) { 82 | this.cursor = cursor; 83 | } 84 | 85 | public CursorAssert hasRow(Object... values) { 86 | assertThat(cursor.moveToNext()).named("row " + (row + 1) + " exists").isTrue(); 87 | row += 1; 88 | assertThat(cursor.getColumnCount()).named("column count").isEqualTo(values.length); 89 | for (int i = 0; i < values.length; i++) { 90 | assertThat(cursor.getString(i)) 91 | .named("row " + row + " column '" + cursor.getColumnName(i) + "'") 92 | .isEqualTo(values[i]); 93 | } 94 | return this; 95 | } 96 | 97 | public void isExhausted() { 98 | if (cursor.moveToNext()) { 99 | StringBuilder data = new StringBuilder(); 100 | for (int i = 0; i < cursor.getColumnCount(); i++) { 101 | if (i > 0) data.append(", "); 102 | data.append(cursor.getString(i)); 103 | } 104 | throw new AssertionError("Expected no more rows but was: " + data); 105 | } 106 | cursor.close(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/SqlBriteTest.java: -------------------------------------------------------------------------------- 1 | package com.squareup.sqlbrite3; 2 | 3 | import android.database.Cursor; 4 | import android.database.MatrixCursor; 5 | import android.support.annotation.Nullable; 6 | import android.support.test.runner.AndroidJUnit4; 7 | import com.squareup.sqlbrite3.SqlBrite.Query; 8 | import io.reactivex.functions.Function; 9 | import java.util.List; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | import static com.google.common.truth.Truth.assertThat; 15 | import static org.junit.Assert.fail; 16 | 17 | @RunWith(AndroidJUnit4.class) 18 | @SuppressWarnings("CheckResult") 19 | public final class SqlBriteTest { 20 | private static final String FIRST_NAME = "first_name"; 21 | private static final String LAST_NAME = "last_name"; 22 | private static final String[] COLUMN_NAMES = { FIRST_NAME, LAST_NAME }; 23 | 24 | @Test public void builderDisallowsNull() { 25 | SqlBrite.Builder builder = new SqlBrite.Builder(); 26 | try { 27 | builder.logger(null); 28 | fail(); 29 | } catch (NullPointerException e) { 30 | assertThat(e).hasMessage("logger == null"); 31 | } 32 | try { 33 | builder.queryTransformer(null); 34 | fail(); 35 | } catch (NullPointerException e) { 36 | assertThat(e).hasMessage("queryTransformer == null"); 37 | } 38 | } 39 | 40 | @Test public void asRowsEmpty() { 41 | MatrixCursor cursor = new MatrixCursor(COLUMN_NAMES); 42 | Query query = new CursorQuery(cursor); 43 | List names = query.asRows(Name.MAP).toList().blockingGet(); 44 | assertThat(names).isEmpty(); 45 | } 46 | 47 | @Test public void asRows() { 48 | MatrixCursor cursor = new MatrixCursor(COLUMN_NAMES); 49 | cursor.addRow(new Object[] { "Alice", "Allison" }); 50 | cursor.addRow(new Object[] { "Bob", "Bobberson" }); 51 | 52 | Query query = new CursorQuery(cursor); 53 | List names = query.asRows(Name.MAP).toList().blockingGet(); 54 | assertThat(names).containsExactly(new Name("Alice", "Allison"), new Name("Bob", "Bobberson")); 55 | } 56 | 57 | @Test public void asRowsStopsWhenUnsubscribed() { 58 | MatrixCursor cursor = new MatrixCursor(COLUMN_NAMES); 59 | cursor.addRow(new Object[] { "Alice", "Allison" }); 60 | cursor.addRow(new Object[] { "Bob", "Bobberson" }); 61 | 62 | Query query = new CursorQuery(cursor); 63 | final AtomicInteger count = new AtomicInteger(); 64 | query.asRows(new Function() { 65 | @Override public Name apply(Cursor cursor) throws Exception { 66 | count.incrementAndGet(); 67 | return Name.MAP.apply(cursor); 68 | } 69 | }).take(1).blockingFirst(); 70 | assertThat(count.get()).isEqualTo(1); 71 | } 72 | 73 | @Test public void asRowsEmptyWhenNullCursor() { 74 | Query nully = new Query() { 75 | @Nullable @Override public Cursor run() { 76 | return null; 77 | } 78 | }; 79 | 80 | final AtomicInteger count = new AtomicInteger(); 81 | nully.asRows(new Function() { 82 | @Override public Name apply(Cursor cursor) throws Exception { 83 | count.incrementAndGet(); 84 | return Name.MAP.apply(cursor); 85 | } 86 | }).test().assertNoValues().assertComplete(); 87 | 88 | assertThat(count.get()).isEqualTo(0); 89 | } 90 | 91 | static final class Name { 92 | static final Function MAP = new Function() { 93 | @Override public Name apply(Cursor cursor) { 94 | return new Name( // 95 | cursor.getString(cursor.getColumnIndexOrThrow(FIRST_NAME)), 96 | cursor.getString(cursor.getColumnIndexOrThrow(LAST_NAME))); 97 | } 98 | }; 99 | 100 | final String first; 101 | final String last; 102 | 103 | Name(String first, String last) { 104 | this.first = first; 105 | this.last = last; 106 | } 107 | 108 | @Override public boolean equals(Object o) { 109 | if (o == this) return true; 110 | if (!(o instanceof Name)) return false; 111 | Name other = (Name) o; 112 | return first.equals(other.first) && last.equals(other.last); 113 | } 114 | 115 | @Override public int hashCode() { 116 | return first.hashCode() * 17 + last.hashCode(); 117 | } 118 | 119 | @Override public String toString() { 120 | return "Name[" + first + ' ' + last + ']'; 121 | } 122 | } 123 | 124 | static final class CursorQuery extends Query { 125 | private final Cursor cursor; 126 | 127 | CursorQuery(Cursor cursor) { 128 | this.cursor = cursor; 129 | } 130 | 131 | @Override public Cursor run() { 132 | return cursor; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/TestDb.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.arch.persistence.db.SupportSQLiteDatabase; 19 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | import android.support.annotation.NonNull; 23 | import io.reactivex.functions.Function; 24 | import java.util.Arrays; 25 | import java.util.Collection; 26 | 27 | import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL; 28 | import static com.squareup.sqlbrite3.TestDb.EmployeeTable.ID; 29 | import static com.squareup.sqlbrite3.TestDb.EmployeeTable.NAME; 30 | import static com.squareup.sqlbrite3.TestDb.EmployeeTable.USERNAME; 31 | import static com.squareup.sqlbrite3.TestDb.ManagerTable.EMPLOYEE_ID; 32 | import static com.squareup.sqlbrite3.TestDb.ManagerTable.MANAGER_ID; 33 | 34 | final class TestDb extends SupportSQLiteOpenHelper.Callback { 35 | static final String TABLE_EMPLOYEE = "employee"; 36 | static final String TABLE_MANAGER = "manager"; 37 | 38 | static final String SELECT_EMPLOYEES = 39 | "SELECT " + USERNAME + ", " + NAME + " FROM " + TABLE_EMPLOYEE; 40 | static final String SELECT_MANAGER_LIST = "" 41 | + "SELECT e." + NAME + ", m." + NAME + " " 42 | + "FROM " + TABLE_MANAGER + " AS manager " 43 | + "JOIN " + TABLE_EMPLOYEE + " AS e " 44 | + "ON manager." + EMPLOYEE_ID + " = e." + ID + " " 45 | + "JOIN " + TABLE_EMPLOYEE + " as m " 46 | + "ON manager." + MANAGER_ID + " = m." + ID; 47 | static final Collection BOTH_TABLES = 48 | Arrays.asList(TABLE_EMPLOYEE, TABLE_MANAGER); 49 | 50 | interface EmployeeTable { 51 | String ID = "_id"; 52 | String USERNAME = "username"; 53 | String NAME = "name"; 54 | } 55 | 56 | static final class Employee { 57 | static final Function MAPPER = new Function() { 58 | @Override public Employee apply(Cursor cursor) { 59 | return new Employee( // 60 | cursor.getString(cursor.getColumnIndexOrThrow(EmployeeTable.USERNAME)), 61 | cursor.getString(cursor.getColumnIndexOrThrow(EmployeeTable.NAME))); 62 | } 63 | }; 64 | 65 | final String username; 66 | final String name; 67 | 68 | Employee(String username, String name) { 69 | this.username = username; 70 | this.name = name; 71 | } 72 | 73 | @Override public boolean equals(Object o) { 74 | if (o == this) return true; 75 | if (!(o instanceof Employee)) return false; 76 | Employee other = (Employee) o; 77 | return username.equals(other.username) && name.equals(other.name); 78 | } 79 | 80 | @Override public int hashCode() { 81 | return username.hashCode() * 17 + name.hashCode(); 82 | } 83 | 84 | @Override public String toString() { 85 | return "Employee[" + username + ' ' + name + ']'; 86 | } 87 | } 88 | 89 | interface ManagerTable { 90 | String ID = "_id"; 91 | String EMPLOYEE_ID = "employee_id"; 92 | String MANAGER_ID = "manager_id"; 93 | } 94 | 95 | private static final String CREATE_EMPLOYEE = "CREATE TABLE " + TABLE_EMPLOYEE + " (" 96 | + EmployeeTable.ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " 97 | + EmployeeTable.USERNAME + " TEXT NOT NULL UNIQUE, " 98 | + EmployeeTable.NAME + " TEXT NOT NULL)"; 99 | private static final String CREATE_MANAGER = "CREATE TABLE " + TABLE_MANAGER + " (" 100 | + ManagerTable.ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " 101 | + ManagerTable.EMPLOYEE_ID + " INTEGER NOT NULL UNIQUE REFERENCES " + TABLE_EMPLOYEE + "(" + EmployeeTable.ID + "), " 102 | + ManagerTable.MANAGER_ID + " INTEGER NOT NULL REFERENCES " + TABLE_EMPLOYEE + "(" + EmployeeTable.ID + "))"; 103 | 104 | long aliceId; 105 | long bobId; 106 | long eveId; 107 | 108 | TestDb() { 109 | super(1); 110 | } 111 | 112 | @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { 113 | db.execSQL("PRAGMA foreign_keys=ON"); 114 | 115 | db.execSQL(CREATE_EMPLOYEE); 116 | aliceId = db.insert(TABLE_EMPLOYEE, CONFLICT_FAIL, employee("alice", "Alice Allison")); 117 | bobId = db.insert(TABLE_EMPLOYEE, CONFLICT_FAIL, employee("bob", "Bob Bobberson")); 118 | eveId = db.insert(TABLE_EMPLOYEE, CONFLICT_FAIL, employee("eve", "Eve Evenson")); 119 | 120 | db.execSQL(CREATE_MANAGER); 121 | db.insert(TABLE_MANAGER, CONFLICT_FAIL, manager(eveId, aliceId)); 122 | } 123 | 124 | static ContentValues employee(String username, String name) { 125 | ContentValues values = new ContentValues(); 126 | values.put(EmployeeTable.USERNAME, username); 127 | values.put(EmployeeTable.NAME, name); 128 | return values; 129 | } 130 | 131 | static ContentValues manager(long employeeId, long managerId) { 132 | ContentValues values = new ContentValues(); 133 | values.put(ManagerTable.EMPLOYEE_ID, employeeId); 134 | values.put(ManagerTable.MANAGER_ID, managerId); 135 | return values; 136 | } 137 | 138 | @Override 139 | public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion, int newVersion) { 140 | throw new AssertionError(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/TestScheduler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import io.reactivex.Scheduler; 19 | import io.reactivex.annotations.NonNull; 20 | import io.reactivex.disposables.Disposable; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | final class TestScheduler extends Scheduler { 24 | private final io.reactivex.schedulers.TestScheduler delegate = 25 | new io.reactivex.schedulers.TestScheduler(); 26 | 27 | private boolean runTasksImmediately = true; 28 | 29 | public void runTasksImmediately(boolean runTasksImmediately) { 30 | this.runTasksImmediately = runTasksImmediately; 31 | } 32 | 33 | public void triggerActions() { 34 | delegate.triggerActions(); 35 | } 36 | 37 | @Override public Worker createWorker() { 38 | return new TestWorker(); 39 | } 40 | 41 | class TestWorker extends Worker { 42 | private final Worker delegateWorker = delegate.createWorker(); 43 | 44 | @Override 45 | public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { 46 | Disposable disposable = delegateWorker.schedule(run, delay, unit); 47 | if (runTasksImmediately) { 48 | triggerActions(); 49 | } 50 | return disposable; 51 | } 52 | 53 | @Override public void dispose() { 54 | delegateWorker.dispose(); 55 | } 56 | 57 | @Override public boolean isDisposed() { 58 | return delegateWorker.isDisposed(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sqlbrite/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sqlbrite/src/main/java/com/squareup/sqlbrite3/BriteContentResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.content.ContentResolver; 19 | import android.database.ContentObserver; 20 | import android.database.Cursor; 21 | import android.net.Uri; 22 | import android.os.Handler; 23 | import android.os.Looper; 24 | import android.support.annotation.CheckResult; 25 | import android.support.annotation.NonNull; 26 | import android.support.annotation.Nullable; 27 | import com.squareup.sqlbrite3.SqlBrite.Logger; 28 | import com.squareup.sqlbrite3.SqlBrite.Query; 29 | import io.reactivex.Observable; 30 | import io.reactivex.ObservableEmitter; 31 | import io.reactivex.ObservableOnSubscribe; 32 | import io.reactivex.ObservableTransformer; 33 | import io.reactivex.Scheduler; 34 | import io.reactivex.functions.Cancellable; 35 | import java.util.Arrays; 36 | 37 | import static com.squareup.sqlbrite3.QueryObservable.QUERY_OBSERVABLE; 38 | import static java.lang.System.nanoTime; 39 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 40 | 41 | /** 42 | * A lightweight wrapper around {@link ContentResolver} which allows for continuously observing 43 | * the result of a query. Create using a {@link SqlBrite} instance. 44 | */ 45 | public final class BriteContentResolver { 46 | final Handler contentObserverHandler = new Handler(Looper.getMainLooper()); 47 | 48 | final ContentResolver contentResolver; 49 | private final Logger logger; 50 | private final Scheduler scheduler; 51 | private final ObservableTransformer queryTransformer; 52 | 53 | volatile boolean logging; 54 | 55 | BriteContentResolver(ContentResolver contentResolver, Logger logger, Scheduler scheduler, 56 | ObservableTransformer queryTransformer) { 57 | this.contentResolver = contentResolver; 58 | this.logger = logger; 59 | this.scheduler = scheduler; 60 | this.queryTransformer = queryTransformer; 61 | } 62 | 63 | /** Control whether debug logging is enabled. */ 64 | public void setLoggingEnabled(boolean enabled) { 65 | logging = enabled; 66 | } 67 | 68 | /** 69 | * Create an observable which will notify subscribers with a {@linkplain Query query} for 70 | * execution. Subscribers are responsible for always closing {@link Cursor} instance 71 | * returned from the {@link Query}. 72 | *

73 | * Subscribers will receive an immediate notification for initial data as well as subsequent 74 | * notifications for when the supplied {@code uri}'s data changes. Unsubscribe when you no longer 75 | * want updates to a query. 76 | *

77 | * Since content resolver triggers are inherently asynchronous, items emitted from the returned 78 | * observable use the {@link Scheduler} supplied to {@link SqlBrite#wrapContentProvider}. For 79 | * consistency, the immediate notification sent on subscribe also uses this scheduler. As such, 80 | * calling {@link Observable#subscribeOn subscribeOn} on the returned observable has no effect. 81 | *

82 | * Note: To skip the immediate notification and only receive subsequent notifications when data 83 | * has changed call {@code skip(1)} on the returned observable. 84 | *

85 | * Warning: this method does not perform the query! Only by subscribing to the returned 86 | * {@link Observable} will the operation occur. 87 | * 88 | * @see ContentResolver#query(Uri, String[], String, String[], String) 89 | * @see ContentResolver#registerContentObserver(Uri, boolean, ContentObserver) 90 | */ 91 | @CheckResult @NonNull 92 | public QueryObservable createQuery(@NonNull final Uri uri, @Nullable final String[] projection, 93 | @Nullable final String selection, @Nullable final String[] selectionArgs, @Nullable 94 | final String sortOrder, final boolean notifyForDescendents) { 95 | final Query query = new Query() { 96 | @Override public Cursor run() { 97 | long startNanos = nanoTime(); 98 | Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder); 99 | 100 | if (logging) { 101 | long tookMillis = NANOSECONDS.toMillis(nanoTime() - startNanos); 102 | log("QUERY (%sms)\n uri: %s\n projection: %s\n selection: %s\n selectionArgs: %s\n " 103 | + "sortOrder: %s\n notifyForDescendents: %s", tookMillis, uri, 104 | Arrays.toString(projection), selection, Arrays.toString(selectionArgs), sortOrder, 105 | notifyForDescendents); 106 | } 107 | 108 | return cursor; 109 | } 110 | }; 111 | Observable queries = Observable.create(new ObservableOnSubscribe() { 112 | @Override public void subscribe(final ObservableEmitter e) throws Exception { 113 | final ContentObserver observer = new ContentObserver(contentObserverHandler) { 114 | @Override public void onChange(boolean selfChange) { 115 | if (!e.isDisposed()) { 116 | e.onNext(query); 117 | } 118 | } 119 | }; 120 | contentResolver.registerContentObserver(uri, notifyForDescendents, observer); 121 | e.setCancellable(new Cancellable() { 122 | @Override public void cancel() throws Exception { 123 | contentResolver.unregisterContentObserver(observer); 124 | } 125 | }); 126 | 127 | if (!e.isDisposed()) { 128 | e.onNext(query); // Trigger initial query. 129 | } 130 | } 131 | }); 132 | return queries // 133 | .observeOn(scheduler) // 134 | .compose(queryTransformer) // Apply the user's query transformer. 135 | .to(QUERY_OBSERVABLE); 136 | } 137 | 138 | void log(String message, Object... args) { 139 | if (args.length > 0) message = String.format(message, args); 140 | logger.log(message); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryObservable.java: -------------------------------------------------------------------------------- 1 | package com.squareup.sqlbrite3; 2 | 3 | import android.database.Cursor; 4 | import android.os.Build; 5 | import android.support.annotation.CheckResult; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.RequiresApi; 8 | import com.squareup.sqlbrite3.SqlBrite.Query; 9 | import io.reactivex.Observable; 10 | import io.reactivex.Observer; 11 | import io.reactivex.functions.Function; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | /** An {@link Observable} of {@link Query} which offers query-specific convenience operators. */ 16 | public final class QueryObservable extends Observable { 17 | static final Function, QueryObservable> QUERY_OBSERVABLE = 18 | new Function, QueryObservable>() { 19 | @Override public QueryObservable apply(Observable queryObservable) { 20 | return new QueryObservable(queryObservable); 21 | } 22 | }; 23 | 24 | private final Observable upstream; 25 | 26 | public QueryObservable(Observable upstream) { 27 | this.upstream = upstream; 28 | } 29 | 30 | @Override protected void subscribeActual(Observer observer) { 31 | upstream.subscribe(observer); 32 | } 33 | 34 | /** 35 | * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each 36 | * emitted {@link Query} which returns a single row to {@code T}. 37 | *

38 | * It is an error for a query to pass through this operator with more than 1 row in its result 39 | * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows 40 | * do not emit an item. 41 | *

42 | * This method is equivalent to: 43 | *

{@code
 44 |    * flatMap(q -> q.asRows(mapper).take(1))
 45 |    * }
46 | * and a convenience operator for: 47 | *
{@code
 48 |    * lift(Query.mapToOne(mapper))
 49 |    * }
50 | * 51 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 52 | */ 53 | @CheckResult @NonNull 54 | public final Observable mapToOne(@NonNull Function mapper) { 55 | return lift(Query.mapToOne(mapper)); 56 | } 57 | 58 | /** 59 | * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each 60 | * emitted {@link Query} which returns a single row to {@code T}. 61 | *

62 | * It is an error for a query to pass through this operator with more than 1 row in its result 63 | * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows 64 | * emit {@code defaultValue}. 65 | *

66 | * This method is equivalent to: 67 | *

{@code
 68 |    * flatMap(q -> q.asRows(mapper).take(1).defaultIfEmpty(defaultValue))
 69 |    * }
70 | * and a convenience operator for: 71 | *
{@code
 72 |    * lift(Query.mapToOneOrDefault(mapper, defaultValue))
 73 |    * }
74 | * 75 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 76 | * @param defaultValue Value returned if result set is empty 77 | */ 78 | @CheckResult @NonNull 79 | public final Observable mapToOneOrDefault(@NonNull Function mapper, 80 | @NonNull T defaultValue) { 81 | return lift(Query.mapToOneOrDefault(mapper, defaultValue)); 82 | } 83 | 84 | /** 85 | * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each 86 | * emitted {@link Query} which returns a single row to {@code Optional}. 87 | *

88 | * It is an error for a query to pass through this operator with more than 1 row in its result 89 | * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows 90 | * emit {@link Optional#empty() Optional.empty()} 91 | *

92 | * This method is equivalent to: 93 | *

{@code
 94 |    * flatMap(q -> q.asRows(mapper).take(1).map(Optional::of).defaultIfEmpty(Optional.empty())
 95 |    * }
96 | * and a convenience operator for: 97 | *
{@code
 98 |    * lift(Query.mapToOptional(mapper))
 99 |    * }
100 | * 101 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 102 | */ 103 | @RequiresApi(Build.VERSION_CODES.N) 104 | @CheckResult @NonNull 105 | public final Observable> mapToOptional(@NonNull Function mapper) { 106 | return lift(Query.mapToOptional(mapper)); 107 | } 108 | 109 | /** 110 | * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each 111 | * emitted {@link Query} to a {@code List}. 112 | *

113 | * Be careful using this operator as it will always consume the entire cursor and create objects 114 | * for each row, every time this observable emits a new query. On tables whose queries update 115 | * frequently or very large result sets this can result in the creation of many objects. 116 | *

117 | * This method is equivalent to: 118 | *

{@code
119 |    * flatMap(q -> q.asRows(mapper).toList())
120 |    * }
121 | * and a convenience operator for: 122 | *
{@code
123 |    * lift(Query.mapToList(mapper))
124 |    * }
125 | *

126 | * Consider using {@link Query#asRows} if you need to limit or filter in memory. 127 | * 128 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 129 | */ 130 | @CheckResult @NonNull 131 | public final Observable> mapToList(@NonNull Function mapper) { 132 | return lift(Query.mapToList(mapper)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryToListOperator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.database.Cursor; 19 | import io.reactivex.ObservableOperator; 20 | import io.reactivex.Observer; 21 | import io.reactivex.exceptions.Exceptions; 22 | import io.reactivex.functions.Function; 23 | import io.reactivex.observers.DisposableObserver; 24 | import io.reactivex.plugins.RxJavaPlugins; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | final class QueryToListOperator implements ObservableOperator, SqlBrite.Query> { 29 | private final Function mapper; 30 | 31 | QueryToListOperator(Function mapper) { 32 | this.mapper = mapper; 33 | } 34 | 35 | @Override public Observer apply(Observer> observer) { 36 | return new MappingObserver<>(observer, mapper); 37 | } 38 | 39 | static final class MappingObserver extends DisposableObserver { 40 | private final Observer> downstream; 41 | private final Function mapper; 42 | 43 | MappingObserver(Observer> downstream, Function mapper) { 44 | this.downstream = downstream; 45 | this.mapper = mapper; 46 | } 47 | 48 | @Override protected void onStart() { 49 | downstream.onSubscribe(this); 50 | } 51 | 52 | @Override public void onNext(SqlBrite.Query query) { 53 | try { 54 | Cursor cursor = query.run(); 55 | if (cursor == null || isDisposed()) { 56 | return; 57 | } 58 | List items = new ArrayList<>(cursor.getCount()); 59 | try { 60 | while (cursor.moveToNext()) { 61 | items.add(mapper.apply(cursor)); 62 | } 63 | } finally { 64 | cursor.close(); 65 | } 66 | if (!isDisposed()) { 67 | downstream.onNext(items); 68 | } 69 | } catch (Throwable e) { 70 | Exceptions.throwIfFatal(e); 71 | onError(e); 72 | } 73 | } 74 | 75 | @Override public void onComplete() { 76 | if (!isDisposed()) { 77 | downstream.onComplete(); 78 | } 79 | } 80 | 81 | @Override public void onError(Throwable e) { 82 | if (isDisposed()) { 83 | RxJavaPlugins.onError(e); 84 | } else { 85 | downstream.onError(e); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryToOneOperator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.database.Cursor; 19 | import android.support.annotation.Nullable; 20 | import io.reactivex.ObservableOperator; 21 | import io.reactivex.Observer; 22 | import io.reactivex.exceptions.Exceptions; 23 | import io.reactivex.functions.Function; 24 | import io.reactivex.observers.DisposableObserver; 25 | import io.reactivex.plugins.RxJavaPlugins; 26 | 27 | final class QueryToOneOperator implements ObservableOperator { 28 | private final Function mapper; 29 | private final T defaultValue; 30 | 31 | /** A null {@code defaultValue} means nothing will be emitted when empty. */ 32 | QueryToOneOperator(Function mapper, @Nullable T defaultValue) { 33 | this.mapper = mapper; 34 | this.defaultValue = defaultValue; 35 | } 36 | 37 | @Override public Observer apply(Observer observer) { 38 | return new MappingObserver<>(observer, mapper, defaultValue); 39 | } 40 | 41 | static final class MappingObserver extends DisposableObserver { 42 | private final Observer downstream; 43 | private final Function mapper; 44 | private final T defaultValue; 45 | 46 | MappingObserver(Observer downstream, Function mapper, T defaultValue) { 47 | this.downstream = downstream; 48 | this.mapper = mapper; 49 | this.defaultValue = defaultValue; 50 | } 51 | 52 | @Override protected void onStart() { 53 | downstream.onSubscribe(this); 54 | } 55 | 56 | @Override public void onNext(SqlBrite.Query query) { 57 | try { 58 | T item = null; 59 | Cursor cursor = query.run(); 60 | if (cursor != null) { 61 | try { 62 | if (cursor.moveToNext()) { 63 | item = mapper.apply(cursor); 64 | if (item == null) { 65 | downstream.onError(new NullPointerException("QueryToOne mapper returned null")); 66 | return; 67 | } 68 | if (cursor.moveToNext()) { 69 | throw new IllegalStateException("Cursor returned more than 1 row"); 70 | } 71 | } 72 | } finally { 73 | cursor.close(); 74 | } 75 | } 76 | if (!isDisposed()) { 77 | if (item != null) { 78 | downstream.onNext(item); 79 | } else if (defaultValue != null) { 80 | downstream.onNext(defaultValue); 81 | } 82 | } 83 | } catch (Throwable e) { 84 | Exceptions.throwIfFatal(e); 85 | onError(e); 86 | } 87 | } 88 | 89 | @Override public void onComplete() { 90 | if (!isDisposed()) { 91 | downstream.onComplete(); 92 | } 93 | } 94 | 95 | @Override public void onError(Throwable e) { 96 | if (isDisposed()) { 97 | RxJavaPlugins.onError(e); 98 | } else { 99 | downstream.onError(e); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryToOptionalOperator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.database.Cursor; 19 | import android.os.Build; 20 | import android.support.annotation.RequiresApi; 21 | import io.reactivex.ObservableOperator; 22 | import io.reactivex.Observer; 23 | import io.reactivex.exceptions.Exceptions; 24 | import io.reactivex.functions.Function; 25 | import io.reactivex.observers.DisposableObserver; 26 | import io.reactivex.plugins.RxJavaPlugins; 27 | import java.util.Optional; 28 | 29 | @RequiresApi(Build.VERSION_CODES.N) 30 | final class QueryToOptionalOperator implements ObservableOperator, SqlBrite.Query> { 31 | private final Function mapper; 32 | 33 | QueryToOptionalOperator(Function mapper) { 34 | this.mapper = mapper; 35 | } 36 | 37 | @Override public Observer apply(Observer> observer) { 38 | return new MappingObserver<>(observer, mapper); 39 | } 40 | 41 | static final class MappingObserver extends DisposableObserver { 42 | private final Observer> downstream; 43 | private final Function mapper; 44 | 45 | MappingObserver(Observer> downstream, Function mapper) { 46 | this.downstream = downstream; 47 | this.mapper = mapper; 48 | } 49 | 50 | @Override protected void onStart() { 51 | downstream.onSubscribe(this); 52 | } 53 | 54 | @Override public void onNext(SqlBrite.Query query) { 55 | try { 56 | T item = null; 57 | Cursor cursor = query.run(); 58 | if (cursor != null) { 59 | try { 60 | if (cursor.moveToNext()) { 61 | item = mapper.apply(cursor); 62 | if (item == null) { 63 | downstream.onError(new NullPointerException("QueryToOne mapper returned null")); 64 | return; 65 | } 66 | if (cursor.moveToNext()) { 67 | throw new IllegalStateException("Cursor returned more than 1 row"); 68 | } 69 | } 70 | } finally { 71 | cursor.close(); 72 | } 73 | } 74 | if (!isDisposed()) { 75 | downstream.onNext(Optional.ofNullable(item)); 76 | } 77 | } catch (Throwable e) { 78 | Exceptions.throwIfFatal(e); 79 | onError(e); 80 | } 81 | } 82 | 83 | @Override public void onComplete() { 84 | if (!isDisposed()) { 85 | downstream.onComplete(); 86 | } 87 | } 88 | 89 | @Override public void onError(Throwable e) { 90 | if (isDisposed()) { 91 | RxJavaPlugins.onError(e); 92 | } else { 93 | downstream.onError(e); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sqlbrite/src/main/java/com/squareup/sqlbrite3/SqlBrite.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 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 com.squareup.sqlbrite3; 17 | 18 | import android.arch.persistence.db.SupportSQLiteOpenHelper; 19 | import android.content.ContentResolver; 20 | import android.database.Cursor; 21 | import android.os.Build; 22 | import android.support.annotation.CheckResult; 23 | import android.support.annotation.NonNull; 24 | import android.support.annotation.Nullable; 25 | import android.support.annotation.RequiresApi; 26 | import android.support.annotation.WorkerThread; 27 | import android.util.Log; 28 | import io.reactivex.Observable; 29 | import io.reactivex.ObservableEmitter; 30 | import io.reactivex.ObservableOnSubscribe; 31 | import io.reactivex.ObservableOperator; 32 | import io.reactivex.ObservableTransformer; 33 | import io.reactivex.Scheduler; 34 | import io.reactivex.functions.Function; 35 | import java.util.List; 36 | import java.util.Optional; 37 | 38 | /** 39 | * A lightweight wrapper around {@link SupportSQLiteOpenHelper} which allows for continuously 40 | * observing the result of a query. 41 | */ 42 | public final class SqlBrite { 43 | static final Logger DEFAULT_LOGGER = new Logger() { 44 | @Override public void log(String message) { 45 | Log.d("SqlBrite", message); 46 | } 47 | }; 48 | static final ObservableTransformer DEFAULT_TRANSFORMER = 49 | new ObservableTransformer() { 50 | @Override public Observable apply(Observable queryObservable) { 51 | return queryObservable; 52 | } 53 | }; 54 | 55 | public static final class Builder { 56 | private Logger logger = DEFAULT_LOGGER; 57 | private ObservableTransformer queryTransformer = DEFAULT_TRANSFORMER; 58 | 59 | @CheckResult 60 | public Builder logger(@NonNull Logger logger) { 61 | if (logger == null) throw new NullPointerException("logger == null"); 62 | this.logger = logger; 63 | return this; 64 | } 65 | 66 | @CheckResult 67 | public Builder queryTransformer(@NonNull ObservableTransformer queryTransformer) { 68 | if (queryTransformer == null) throw new NullPointerException("queryTransformer == null"); 69 | this.queryTransformer = queryTransformer; 70 | return this; 71 | } 72 | 73 | @CheckResult 74 | public SqlBrite build() { 75 | return new SqlBrite(logger, queryTransformer); 76 | } 77 | } 78 | 79 | final Logger logger; 80 | final ObservableTransformer queryTransformer; 81 | 82 | SqlBrite(@NonNull Logger logger, @NonNull ObservableTransformer queryTransformer) { 83 | this.logger = logger; 84 | this.queryTransformer = queryTransformer; 85 | } 86 | 87 | /** 88 | * Wrap a {@link SupportSQLiteOpenHelper} for observable queries. 89 | *

90 | * While not strictly required, instances of this class assume that they will be the only ones 91 | * interacting with the underlying {@link SupportSQLiteOpenHelper} and it is required for 92 | * automatic notifications of table changes to work. See {@linkplain BriteDatabase#createQuery the 93 | * query method} for more information on that behavior. 94 | * 95 | * @param scheduler The {@link Scheduler} on which items from {@link BriteDatabase#createQuery} 96 | * will be emitted. 97 | */ 98 | @CheckResult @NonNull public BriteDatabase wrapDatabaseHelper( 99 | @NonNull SupportSQLiteOpenHelper helper, 100 | @NonNull Scheduler scheduler) { 101 | return new BriteDatabase(helper, logger, scheduler, queryTransformer); 102 | } 103 | 104 | /** 105 | * Wrap a {@link ContentResolver} for observable queries. 106 | * 107 | * @param scheduler The {@link Scheduler} on which items from 108 | * {@link BriteContentResolver#createQuery} will be emitted. 109 | */ 110 | @CheckResult @NonNull public BriteContentResolver wrapContentProvider( 111 | @NonNull ContentResolver contentResolver, @NonNull Scheduler scheduler) { 112 | return new BriteContentResolver(contentResolver, logger, scheduler, queryTransformer); 113 | } 114 | 115 | /** An executable query. */ 116 | public static abstract class Query { 117 | /** 118 | * Creates an {@linkplain ObservableOperator operator} which transforms a query returning a 119 | * single row to a {@code T} using {@code mapper}. Use with {@link Observable#lift}. 120 | *

121 | * It is an error for a query to pass through this operator with more than 1 row in its result 122 | * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows 123 | * do not emit an item. 124 | *

125 | * This operator ignores {@code null} cursors returned from {@link #run()}. 126 | * 127 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 128 | */ 129 | @CheckResult @NonNull // 130 | public static ObservableOperator mapToOne(@NonNull Function mapper) { 131 | return new QueryToOneOperator<>(mapper, null); 132 | } 133 | 134 | /** 135 | * Creates an {@linkplain ObservableOperator operator} which transforms a query returning a 136 | * single row to a {@code T} using {@code mapper}. Use with {@link Observable#lift}. 137 | *

138 | * It is an error for a query to pass through this operator with more than 1 row in its result 139 | * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows 140 | * emit {@code defaultValue}. 141 | *

142 | * This operator emits {@code defaultValue} if {@code null} is returned from {@link #run()}. 143 | * 144 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 145 | * @param defaultValue Value returned if result set is empty 146 | */ 147 | @SuppressWarnings("ConstantConditions") // Public API contract. 148 | @CheckResult @NonNull 149 | public static ObservableOperator mapToOneOrDefault( 150 | @NonNull Function mapper, @NonNull T defaultValue) { 151 | if (defaultValue == null) throw new NullPointerException("defaultValue == null"); 152 | return new QueryToOneOperator<>(mapper, defaultValue); 153 | } 154 | 155 | /** 156 | * Creates an {@linkplain ObservableOperator operator} which transforms a query returning a 157 | * single row to a {@code Optional} using {@code mapper}. Use with {@link Observable#lift}. 158 | *

159 | * It is an error for a query to pass through this operator with more than 1 row in its result 160 | * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows 161 | * emit {@link Optional#empty() Optional.empty()}. 162 | *

163 | * This operator ignores {@code null} cursors returned from {@link #run()}. 164 | * 165 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 166 | */ 167 | @RequiresApi(Build.VERSION_CODES.N) // 168 | @CheckResult @NonNull // 169 | public static ObservableOperator, Query> mapToOptional( 170 | @NonNull Function mapper) { 171 | return new QueryToOptionalOperator<>(mapper); 172 | } 173 | 174 | /** 175 | * Creates an {@linkplain ObservableOperator operator} which transforms a query to a 176 | * {@code List} using {@code mapper}. Use with {@link Observable#lift}. 177 | *

178 | * Be careful using this operator as it will always consume the entire cursor and create objects 179 | * for each row, every time this observable emits a new query. On tables whose queries update 180 | * frequently or very large result sets this can result in the creation of many objects. 181 | *

182 | * This operator ignores {@code null} cursors returned from {@link #run()}. 183 | * 184 | * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. 185 | */ 186 | @CheckResult @NonNull 187 | public static ObservableOperator, Query> mapToList( 188 | @NonNull Function mapper) { 189 | return new QueryToListOperator<>(mapper); 190 | } 191 | 192 | /** 193 | * Execute the query on the underlying database and return the resulting cursor. 194 | * 195 | * @return A {@link Cursor} with query results, or {@code null} when the query could not be 196 | * executed due to a problem with the underlying store. Unfortunately it is not well documented 197 | * when {@code null} is returned. It usually involves a problem in communicating with the 198 | * underlying store and should either be treated as failure or ignored for retry at a later 199 | * time. 200 | */ 201 | @CheckResult @WorkerThread 202 | @Nullable 203 | public abstract Cursor run(); 204 | 205 | /** 206 | * Execute the query on the underlying database and return an Observable of each row mapped to 207 | * {@code T} by {@code mapper}. 208 | *

209 | * Standard usage of this operation is in {@code flatMap}: 210 | *

{@code
211 |      * flatMap(q -> q.asRows(Item.MAPPER).toList())
212 |      * }
213 | * However, the above is a more-verbose but identical operation as 214 | * {@link QueryObservable#mapToList}. This {@code asRows} method should be used when you need 215 | * to limit or filter the items separate from the actual query. 216 | *
{@code
217 |      * flatMap(q -> q.asRows(Item.MAPPER).take(5).toList())
218 |      * // or...
219 |      * flatMap(q -> q.asRows(Item.MAPPER).filter(i -> i.isActive).toList())
220 |      * }
221 | *

222 | * Note: Limiting results or filtering will almost always be faster in the database as part of 223 | * a query and should be preferred, where possible. 224 | *

225 | * The resulting observable will be empty if {@code null} is returned from {@link #run()}. 226 | */ 227 | @CheckResult @NonNull 228 | public final Observable asRows(final Function mapper) { 229 | return Observable.create(new ObservableOnSubscribe() { 230 | @Override public void subscribe(ObservableEmitter e) throws Exception { 231 | Cursor cursor = run(); 232 | if (cursor != null) { 233 | try { 234 | while (cursor.moveToNext() && !e.isDisposed()) { 235 | e.onNext(mapper.apply(cursor)); 236 | } 237 | } finally { 238 | cursor.close(); 239 | } 240 | } 241 | if (!e.isDisposed()) { 242 | e.onComplete(); 243 | } 244 | } 245 | }); 246 | } 247 | } 248 | 249 | /** A simple indirection for logging debug messages. */ 250 | public interface Logger { 251 | void log(String message); 252 | } 253 | } 254 | --------------------------------------------------------------------------------